From 2663d1a9bcc0470d186fb4fd9657c8520ec2db25 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Fri, 11 Oct 2024 16:55:11 +0200 Subject: [PATCH 01/93] wip: Started refactoring fs/path/os usage --- package-lock.json | 8 ++++- package.json | 8 ++--- src/extension.ts | 60 +++++++++++++++++------------------ src/lsp-client/clientTypes.ts | 3 +- src/mainBrowser.ts | 34 +++++++++++++++++--- src/mainNode.ts | 2 +- 6 files changed, 74 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f1f8e7a..2ce2d4b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,8 @@ "react-tooltip": "^5.13.1", "throttle-debounce": "^5.0.0", "vscode-languageclient": "^8.1.0", - "vscode-languageserver-types": "^3.17.3" + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.8" }, "devDependencies": { "@lezer/generator": "^1.2.3", @@ -10534,6 +10535,11 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", diff --git a/package.json b/package.json index d84a329a..4c15e920 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "onLanguage:coqmarkdown" ], "contributes": { - "themes": [ + "themes": [ { "label": "Waterproof light theme", "uiTheme": "vs", @@ -176,8 +176,7 @@ "id": "install-dependencies", "title": "Run Automatic Installer", "description": "Waterproof needs additional software to function.\n[Run Automatic Installer](command:waterproof.autoInstall)", - "completionEvents": [ - ], + "completionEvents": [], "media": { "markdown": "walkthrough-data/initial-setup/auto-installer.md" } @@ -509,7 +508,8 @@ "react-tooltip": "^5.13.1", "throttle-debounce": "^5.0.0", "vscode-languageclient": "^8.1.0", - "vscode-languageserver-types": "^3.17.3" + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.8" }, "main": "./out/src/mainNode.js", "browser": "/out/src/mainBrowser.js", diff --git a/src/extension.ts b/src/extension.ts index cff10d6c..d1af7e6c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -30,9 +30,7 @@ import { SymbolsPanel } from "./webviews/standardviews/symbols"; import { TacticsPanel } from "./webviews/standardviews/tactics"; import { VersionChecker } from "./version-checker"; -import { readFile } from "fs"; -import { join as joinPath} from "path"; -import { homedir } from "os"; +import { Utils } from "vscode-uri"; import { WaterproofConfigHelper, WaterproofLogger } from "./helpers"; import { exec } from "child_process" import { resolveSoa } from "dns"; @@ -282,26 +280,27 @@ export class Waterproof implements Disposable { private async waterproofTutorialCommand(): Promise { const defaultUri = workspace.workspaceFolders ? - Uri.file(joinPath(workspace.workspaceFolders[0].uri.fsPath, "waterproof_tutorial.mv")) : - Uri.file(joinPath(homedir(), "waterproof_tutorial.mv")); + Utils.joinPath(workspace.workspaceFolders[0].uri, "waterproof_tutorial.mv") : + // FIXME: Was based on homedir + Uri.parse("waterproof_tutorial.mv"); window.showSaveDialog({filters: {'Waterproof': ["mv", "v"]}, title: "Waterproof Tutorial", defaultUri}).then((uri) => { if (!uri) { window.showErrorMessage("Something went wrong in saving the Waterproof tutorial file"); return; } - const newFilePath = Uri.joinPath(this.context.extensionUri, "misc-includes", "waterproof_tutorial.mv").fsPath; - readFile(newFilePath, (err, data) => { - if (err) { + const newFilePath = Uri.joinPath(this.context.extensionUri, "misc-includes", "waterproof_tutorial.mv"); + workspace.fs.readFile(newFilePath) + .then((data) => { + workspace.fs.writeFile(uri, data).then(() => { + // Open the file using the waterproof editor + // TODO: Hardcoded `coqEditor.coqEditor`. + commands.executeCommand("vscode.openWith", uri, "coqEditor.coqEditor"); + }); + }, (err) => { window.showErrorMessage("Could not a new Waterproof file."); console.error(`Could not read Waterproof tutorial file: ${err}`); - return; - } - workspace.fs.writeFile(uri, data).then(() => { - // Open the file using the waterproof editor - // TODO: Hardcoded `coqEditor.coqEditor`. - commands.executeCommand("vscode.openWith", uri, "coqEditor.coqEditor"); - }); - }) + return; + }) }); } @@ -312,26 +311,27 @@ export class Waterproof implements Disposable { */ private async newFileCommand(): Promise { const defaultUri = workspace.workspaceFolders ? - Uri.file(joinPath(workspace.workspaceFolders[0].uri.fsPath, "new_waterproof_document.mv")) : - Uri.file(joinPath(homedir(), "new_waterproof_document.mv")); + Utils.joinPath(workspace.workspaceFolders[0].uri, "new_waterproof_document.mv") : + // FIXME: Was based on homedir + Uri.parse("waterproof_tutorial.mv"); window.showSaveDialog({filters: {'Waterproof': ["mv", "v"]}, title: "New Waterproof Document", defaultUri}).then((uri) => { if (!uri) { window.showErrorMessage("Something went wrong in creating a new waterproof document"); return; } - const newFilePath = Uri.joinPath(this.context.extensionUri, "misc-includes", "empty_waterproof_document.mv").fsPath; - readFile(newFilePath, (err, data) => { - if (err) { + const newFilePath = Uri.joinPath(this.context.extensionUri, "misc-includes", "empty_waterproof_document.mv"); + workspace.fs.readFile(newFilePath) + .then((data) => { + workspace.fs.writeFile(uri, data).then(() => { + // Open the file using the waterproof editor + // TODO: Hardcoded `coqEditor.coqEditor`. + commands.executeCommand("vscode.openWith", uri, "coqEditor.coqEditor"); + }); + }, (err) => { window.showErrorMessage("Could not a new Waterproof file."); console.error(`Could not read Waterproof tutorial file: ${err}`); - return; - } - workspace.fs.writeFile(uri, data).then(() => { - // Open the file using the waterproof editor - // TODO: Hardcoded `coqEditor.coqEditor`. - commands.executeCommand("vscode.openWith", uri, "coqEditor.coqEditor"); - }); - }) + return; + }) }); } @@ -399,7 +399,7 @@ export class Waterproof implements Disposable { markdown: { isTrusted: true, supportHtml: true }, }; - this.client = this.clientFactory(clientOptions, WaterproofConfigHelper.configuration); + this.client = this.clientFactory(this.context, clientOptions, WaterproofConfigHelper.configuration); return this.client.startWithHandlers(this.webviewManager).then( () => { // show user that LSP is working diff --git a/src/lsp-client/clientTypes.ts b/src/lsp-client/clientTypes.ts index 94512d89..5711ee23 100644 --- a/src/lsp-client/clientTypes.ts +++ b/src/lsp-client/clientTypes.ts @@ -1,4 +1,4 @@ -import { Position, TextDocument, WorkspaceConfiguration } from "vscode"; +import { ExtensionContext, Position, TextDocument, WorkspaceConfiguration } from "vscode"; import { BaseLanguageClient, DocumentSymbol, LanguageClientOptions } from "vscode-languageclient"; import { GoalAnswer, GoalRequest, PpString } from "../../lib/types"; @@ -83,6 +83,7 @@ export type CoqLspClient = BaseLanguageClient & ICoqLspClient; * Type of file language client factory */ export type CoqLspClientFactory = ( + context: ExtensionContext, clientOptions: LanguageClientOptions, wsConfig: WorkspaceConfiguration ) => CoqLspClient; diff --git a/src/mainBrowser.ts b/src/mainBrowser.ts index 4cac1564..608fc9af 100644 --- a/src/mainBrowser.ts +++ b/src/mainBrowser.ts @@ -1,11 +1,37 @@ -import { ExtensionContext } from "vscode"; +import { ExtensionContext, Uri, WorkspaceConfiguration } from "vscode"; +import { CoqLspClientFactory } from "./lsp-client/clientTypes"; +import { LanguageClient, LanguageClientOptions } from "vscode-languageclient/browser"; +import { CoqLspClient } from "./lsp-client/client"; +import { Waterproof } from "./extension"; /** - * The initialization function called when the extension starts. This is currently not implemented - * since coq-lsp cannot be run as a webworker. + * This function is responsible for creating lsp clients with the extended + * functionality specified in the interface CoqFeatures + * + * @param clientOptions the options available for a LanguageClient (see vscode api) + * @param wsConfig the workspace configuration of Waterproof + * @returns an LSP client with the added functionality of `CoqFeatures` */ +const clientFactory: CoqLspClientFactory = (context: ExtensionContext, clientOptions: LanguageClientOptions, wsConfig: WorkspaceConfiguration) => { + const serverMain = Uri.joinPath(context.extensionUri, 'out/src/mainBrowser.js'); + const worker = new Worker(serverMain.toString(true)); + return new (CoqLspClient(LanguageClient))( + "waterproof", + "Waterproof Document Checker", + clientOptions, + worker + ); +}; + + export function activate(context: ExtensionContext): void { + let extension: Waterproof = new Waterproof(context, clientFactory); + context.subscriptions.push(extension); + // start the lsp client + extension.initializeClient(); } -export function deactivate() { +export function deactivate(): void { + // TODO: stop client + return; } diff --git a/src/mainNode.ts b/src/mainNode.ts index 2974305a..698a5895 100644 --- a/src/mainNode.ts +++ b/src/mainNode.ts @@ -12,7 +12,7 @@ import { CoqLspClientFactory } from "./lsp-client/clientTypes"; * @param wsConfig the workspace configuration of Waterproof * @returns an LSP client with the added functionality of `CoqFeatures` */ -const clientFactory: CoqLspClientFactory = (clientOptions: LanguageClientOptions, wsConfig: WorkspaceConfiguration) => { +const clientFactory: CoqLspClientFactory = (context : ExtensionContext, clientOptions: LanguageClientOptions, wsConfig: WorkspaceConfiguration) => { const serverOptions: ServerOptions = { command: wsConfig.path, args: wsConfig.args, From dc9174769f0bb9eb5c7785dbdd94ccf551e342b9 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 15 Oct 2024 20:43:34 +0200 Subject: [PATCH 02/93] Fixes --- .vscode/launch.json | 3 ++- .vscode/settings.json | 22 +++++++++++++++++++++- src/extension.ts | 29 +++++++++++++---------------- src/webviewManager.ts | 28 ++++++++++++++++++---------- 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 046e52a6..7c915d10 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,8 +21,9 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": [ + "--extensionPath=/home/pim/Downloads/coq-lsp_worker and front-end", "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionDevelopmentKind=web" + "--extensionDevelopmentKind=web", ], "outFiles": ["${workspaceFolder}/out/**/*.js"], "preLaunchTask": "${defaultBuildTask}" diff --git a/.vscode/settings.json b/.vscode/settings.json index 9101a780..27fc2b66 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,23 @@ { - "js/ts.implicitProjectConfig.target": "ESNext" + "js/ts.implicitProjectConfig.target": "ESNext", + "files.exclude": { + "**/*.vo": true, + "**/*.vok": true, + "**/*.vos": true, + "**/*.aux": true, + "**/*.glob": true, + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + }, + "terminal.integrated.profiles.linux": { + "bash": { + "path": "bash", + "icon": "terminal-bash", + "args": ["-i"] + } + }, } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index d1af7e6c..f01239c1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -32,7 +32,6 @@ import { TacticsPanel } from "./webviews/standardviews/tactics"; import { VersionChecker } from "./version-checker"; import { Utils } from "vscode-uri"; import { WaterproofConfigHelper, WaterproofLogger } from "./helpers"; -import { exec } from "child_process" import { resolveSoa } from "dns"; @@ -267,22 +266,23 @@ export class Waterproof implements Disposable { */ private async autoInstall(command: string): Promise { return new Promise((resolve, reject) => { - exec(command, (err, stdout, stderr) => { - if (err) { - // Simple fixed scripts are run, the user is able to stop these but they are not considered errors - // as the user has freedom to choose the steps and can rerun the command. + const myTerm = window.createTerminal(`AutoInstall Waterproof`) + window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration}) => { + if (terminal === myTerm) { + const execution = shellIntegration.executeCommand(command); + window.onDidEndTerminalShellExecution(event => { + if (event.execution === execution) { + this.initializeClient(); + resolve(true); + } + }) } - this.initializeClient(); - resolve(true) - }); + }) }); } private async waterproofTutorialCommand(): Promise { - const defaultUri = workspace.workspaceFolders ? - Utils.joinPath(workspace.workspaceFolders[0].uri, "waterproof_tutorial.mv") : - // FIXME: Was based on homedir - Uri.parse("waterproof_tutorial.mv"); + const defaultUri = Utils.joinPath(workspace.workspaceFolders![0].uri, "new_waterproof_document.mv"); window.showSaveDialog({filters: {'Waterproof': ["mv", "v"]}, title: "Waterproof Tutorial", defaultUri}).then((uri) => { if (!uri) { window.showErrorMessage("Something went wrong in saving the Waterproof tutorial file"); @@ -310,10 +310,7 @@ export class Waterproof implements Disposable { * or by using the File -> New File... option. */ private async newFileCommand(): Promise { - const defaultUri = workspace.workspaceFolders ? - Utils.joinPath(workspace.workspaceFolders[0].uri, "new_waterproof_document.mv") : - // FIXME: Was based on homedir - Uri.parse("waterproof_tutorial.mv"); + const defaultUri = Utils.joinPath(workspace.workspaceFolders![0].uri, "new_waterproof_document.mv"); window.showSaveDialog({filters: {'Waterproof': ["mv", "v"]}, title: "New Waterproof Document", defaultUri}).then((uri) => { if (!uri) { window.showErrorMessage("Something went wrong in creating a new waterproof document"); diff --git a/src/webviewManager.ts b/src/webviewManager.ts index 00bf6e7c..f2a387fa 100644 --- a/src/webviewManager.ts +++ b/src/webviewManager.ts @@ -1,5 +1,5 @@ -import { EventEmitter } from "stream"; -import { TextDocument, Uri, window } from "vscode"; + +import { EventEmitter, TextDocument, Uri, window } from "vscode"; import { Message, MessageType } from "../shared"; import { IExecutor, ILineNumberComponent } from "./components"; @@ -69,7 +69,7 @@ class ActiveWebviews { /** * The webview manager that manages communication between views */ -export class WebviewManager extends EventEmitter { +export class WebviewManager extends EventTarget { // Tool webviews (UI such as panels), stores the view based on name private readonly _toolWebviews: Map = new Map; @@ -117,7 +117,8 @@ export class WebviewManager extends EventEmitter { }); webview.on(WebviewEvents.change, (state) => { if (state == WebviewState.focus && webview.supportInsert) this._active.insert(name); - this.emit(WebviewManagerEvents.updateButton, { name, open: webview.isOpened}); + const event = new CustomEvent(WebviewManagerEvents.updateButton, {'detail' : {name, open: webview.isOpened}}); + this.dispatchEvent(event); }); } @@ -141,12 +142,14 @@ export class WebviewManager extends EventEmitter { webview.on(WebviewEvents.change, (state) => { if (state == WebviewState.focus) { this._active.insert(webview.document.uri.toString()); - this.emit(WebviewManagerEvents.focus, webview.document); + const event = new CustomEvent(WebviewManagerEvents.focus, {'detail' : webview.document}); + this.dispatchEvent(event); } }); this._active.insert(webview.document.uri.toString()); - this.emit(WebviewManagerEvents.focus, webview.document); + const event = new CustomEvent(WebviewManagerEvents.focus, {'detail' : webview.document}); + this.dispatchEvent(event); } /** @@ -229,7 +232,8 @@ export class WebviewManager extends EventEmitter { callback?.(message.body); break; case MessageType.editorReady: - this.emit(WebviewManagerEvents.editorReady, document); + const event = new CustomEvent(WebviewManagerEvents.editorReady, {'detail' : document}); + this.dispatchEvent(event); break; case MessageType.cursorChange: var pos = document.positionAt(message.body); @@ -238,11 +242,13 @@ export class WebviewManager extends EventEmitter { const webview = this._pmWebviews.get(document.uri.toString()); if (!webview) break; if (webview.documentIsUpToDate) { - this.emit(WebviewManagerEvents.cursorChange, document, pos); + const event = new CustomEvent(WebviewManagerEvents.cursorChange, {'detail' : {document, pos}}); + this.dispatchEvent(event); } else { // Document is updating wait for completion const callback = () => { - this.emit(WebviewManagerEvents.cursorChange, document, pos); + const event = new CustomEvent(WebviewManagerEvents.cursorChange, {'detail' : {document, pos}}); + this.dispatchEvent(event); }; if(webview.listeners(WebviewEvents.finishUpdate).length == 0) webview.once(WebviewEvents.finishUpdate, callback); } @@ -276,7 +282,9 @@ export class WebviewManager extends EventEmitter { this.postMessage(id, { type: MessageType.insert, body: msg.body }); break; case MessageType.command: - this.emit(WebviewManagerEvents.command, this._toolWebviews.get(id), msg.body); + const event = new CustomEvent(WebviewManagerEvents.command, + {'detail' : {'toolWebview': this._toolWebviews.get(id), 'body': msg.body}}); + this.dispatchEvent(event); break; default: console.error("The message type " + msg.type + " is not currently supported by webview manager"); From 0fa6972571a8fe8049e9e6345520a7af802933f9 Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:57:27 +0100 Subject: [PATCH 03/93] Proof of concept list of tactics --- .vscodeignore | 3 +- scripts/createmd.js | 14 + scripts/tactics.md | 622 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 638 insertions(+), 1 deletion(-) create mode 100644 scripts/createmd.js create mode 100644 scripts/tactics.md diff --git a/.vscodeignore b/.vscodeignore index 01e470d8..91cc25c5 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -21,4 +21,5 @@ tsconfig-base.json tsconfig.json .github/ ExampleFiles/ -developer-resources/ \ No newline at end of file +developer-resources/ +scripts/ diff --git a/scripts/createmd.js b/scripts/createmd.js new file mode 100644 index 00000000..357fb585 --- /dev/null +++ b/scripts/createmd.js @@ -0,0 +1,14 @@ +const { writeFileSync } = require("fs"); +const tactics = require("../shared/completions/tactics.json"); +const pdfLocation = "tactics.md"; + +let mdContent = `# Waterproof Tactics\n\n`; + +for (const tactic of tactics) { + mdContent += `## \`${tactic.label}\`\n\n${tactic.description.replaceAll("*", "\\*")}\n\n`; + + if (tactic.example) + mdContent += "```coq\n" + tactic.example + "\n```\n\n"; +} + +writeFileSync(pdfLocation, mdContent); diff --git a/scripts/tactics.md b/scripts/tactics.md new file mode 100644 index 00000000..f3f3a591 --- /dev/null +++ b/scripts/tactics.md @@ -0,0 +1,622 @@ +# Waterproof Tactics + +## `Help.` + +Tries to give you a hint on what to do next. + +```coq +Lemma example_help : + 0 = 0. +Proof. +Help. +We conclude that (0 = 0). +Qed. +``` + +## `Take (*name*) : ((*type*)).` + +Take an arbitrary element from (\*type\*) and call it (\*name\*). + +```coq +Lemma example_take : + for all x : ℝ, + x = x. +Proof. +Take x : (ℝ). +We conclude that (x = x). +Qed. +``` + +## `Take (*name*) ∈ ((*set*)).` + +Take an arbitrary element from (\*set\*) and call it (\*name\*). + +```coq +Lemma example_take : + ∀ x ∈ ℝ, + x = x. +Proof. +Take x ∈ (ℝ). +We conclude that (x = x). +Qed. +``` + +## `Take (*name*) > ((*number*)).` + +Take an arbitrary element larger than (\*number\*) and call it (\*name\*). + +```coq +Lemma example_take : + ∀ x > 3, + x = x. +Proof. +Take x > (3). +We conclude that (x = x). +Qed. +``` + +## `Take (*name*) ≥ ((*number*)).` + +Take an arbitrary element larger than or equal to (\*number\*) and call it (\*name\*). + +```coq +Lemma example_take : + ∀ x ≥ 5, + x = x. +Proof. +Take x ≥ (5). +We conclude that (x = x). +Qed. +``` + +## `We need to show that ((*(alternative) formulation of current goal*)).` + +Generally makes a proof more readable. Has the additional functionality that you can write a slightly different but equivalent formulation of the goal: you can for instance change names of certain variables. + +```coq +Lemma example_we_need_to_show_that : + 0 = 0. +Proof. +We need to show that (0 = 0). +We conclude that (0 = 0). +Qed. +``` + +## `We conclude that ((*current goal*)).` + +Tries to automatically prove the current goal. + +```coq +Lemma example_we_conclude_that : + 0 = 0. +Proof. +We conclude that (0 = 0). +Qed. +``` + +## `We conclude that ((*(alternative) formulation of current goal*)).` + +Tries to automatically prove the current goal. + +```coq +Lemma example_we_conclude_that : + 0 = 0. +Proof. +We conclude that (0 = 0). +Qed. +``` + +## `Choose (*name_var*) := ((*expression*)).` + +You can use this tactic when you need to show that there exists an x such that a certain property holds. You do this by proposing (\*expression\*) as a choice for x, giving it the name (\*name_var\*). + +```coq +Lemma example_choose : + ∃ y ∈ ℝ, + y < 3. +Proof. +Choose y := (2). +* Indeed, (y ∈ ℝ). +* We conclude that (y < 3). +Qed. +``` + +## `Assume that ((*statement*)).` + +If you need to prove (\*statement\*) ⇒ B, this allows you to assume that (\*statement\*) holds. + +```coq +Lemma example_assume : + ∀ a ∈ ℝ, a < 0 ⇒ - a > 0. +Proof. +Take a ∈ (ℝ). +Assume that (a < 0). +We conclude that (- a > 0). +Qed. +``` + +## `Assume that ((*statement*)) ((*label*)).` + +If you need to prove (\*statement\*) ⇒ B, this allows you to assume that (\*statement\*) holds, giving it the label (\*label\*). You can leave out ((\*label\*)) if you don't wish to name your assumption. + +```coq +Lemma example_assume : + ∀ a ∈ ℝ, a < 0 ⇒ - a > 0. +Proof. +Take a ∈ (ℝ). +Assume that (a < 0) (a_less_than_0). +We conclude that (- a > 0). +Qed. +``` + +## `(& 3 < 5 = 2 + 3 ≤ 7) (chain of (in)equalities, with opening parenthesis)` + +Example of a chain of (in)equalities in which every inequality should. + +```coq +Lemma example_inequalities : + ∀ ε > 0, Rmin(ε,1) < 2. +Proof. +Take ε > 0. +We conclude that (& Rmin(ε,1) ≤ 1 < 2). +Qed. +``` + +## `& 3 < 5 = 2 + 3 ≤ 7 (chain of (in)equalities)` + +Example of a chain of (in)equalities in which every inequality should. + +```coq +Lemma example_inequalities : + ∀ ε > 0, Rmin(ε,1) < 2. +Proof. +Take ε : (ℝ). +Assume that (ε > 0). +We conclude that (& Rmin(ε,1) ≤ 1 < 2). +Qed. +``` + +## `Obtain such a (*name_var*)` + +In case a hypothesis that you just proved starts with 'there exists' s.t. some property holds, then you can use this tactic to select such a variable. The variable will be named (\*name_var\*). + +```coq +Lemma example_obtain : + ∀ x ∈ ℝ, + (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒ + 10 < x. +Proof. +Take x ∈ (ℝ). +Assume that (∃ y ∈ ℝ, 10 < y ∧ y < x) (i). +Obtain such a y. +Qed. +``` + +## `Obtain (*name_var*) according to ((*name_hyp*)).` + +In case the hypothesis with name (\*name_hyp\*) starts with 'there exists' s.t. some property holds, then you can use this tactic to select such a variable. The variable will be named (\*name_var\*). + +```coq +Lemma example_obtain : + ∀ x ∈ ℝ, + (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒ + 10 < x. +Proof. +Take x ∈ (ℝ). +Assume that (∃ y ∈ ℝ, 10 < y ∧ y < x) (i). +Obtain y according to (i). +Qed. +``` + +## `It suffices to show that ((*statement*)).` + +Waterproof tries to verify automatically whether it is indeed enough to show (\*statement\*) to prove the current goal. If so, (\*statement\*) becomes the new goal. + +```coq +Lemma example_it_suffices_to_show_that : + ∀ ε > 0, + 3 + Rmax(ε,2) ≥ 3. +Proof. +Take ε > 0. +It suffices to show that (Rmax(ε,2) ≥ 0). +We conclude that (& Rmax(ε,2) ≥ 2 ≥ 0). +Qed. +``` + +## `By ((*lemma or assumption*)) it suffices to show that ((*statement*)).` + +Waterproof tries to verify automatically whether it is indeed enough to show (\*statement\*) to prove the current goal, using (\*lemma or assumption\*). If so, (\*statement\*) becomes the new goal. + +```coq +Lemma example_it_suffices_to_show_that : + ∀ ε ∈ ℝ, + ε > 0 ⇒ + 3 + Rmax(ε,2) ≥ 3. +Proof. +Take ε ∈ (ℝ). +Assume that (ε > 0) (i). +By (i) it suffices to show that (Rmax(ε,2) ≥ 0). +We conclude that (& Rmax(ε,2) ≥ 2 ≥ 0). +Qed. +``` + +## `It holds that ((*statement*)) ((*label*)).` + +Tries to automatically prove (\*statement\*). If that works, (\*statement\*) is added as a hypothesis with name (\*optional_label\*). + +```coq +Lemma example_it_holds_that : + ∀ ε > 0, + 4 - Rmax(ε,1) ≤ 3. + +Proof. +Take ε > 0. +It holds that (Rmax(ε,1) ≥ 1) (i). +We conclude that (4 - Rmax(ε,1) ≤ 3). +Qed. +``` + +## `It holds that ((*statement*)).` + +Tries to automatically prove (\*statement\*). If that works, (\*statement\*) is added as a hypothesis. + +```coq +Lemma example_it_holds_that : + ∀ ε > 0, + 4 - Rmax(ε,1) ≤ 3. + +Proof. +Take ε > 0. +It holds that (Rmax(ε,1) ≥ 1). +We conclude that (4 - Rmax(ε,1) ≤ 3). +Qed. +``` + +## `By ((*lemma or assumption*)) it holds that ((*statement*)) ((*label*)).` + +Tries to prove (\*statement\*) using (\*lemma\*) or (\*assumption\*). If that works, (\*statement\*) is added as a hypothesis with name (\*optional_label\*). You can leave out ((\*optional_label\*)) if you don't wish to name the statement. + +```coq +Lemma example_forwards : + 7 < f(-1) ⇒ 2 < f(6). +Proof. +Assume that (7 < f(-1)). +By (f_increasing) it holds that (f(-1) ≤ f(6)) (i). +We conclude that (2 < f(6)). +Qed. +``` + +## `By ((*lemma or assumption*)) it holds that ((*statement*)).` + +Tries to prove (\*statement\*) using (\*lemma\*) or (\*assumption\*). If that works, (\*statement\*) is added as a hypothesis with name (\*optional_label\*). You can leave out ((\*optional_label\*)) if you don't wish to name the statement. + +```coq +Lemma example_forwards : + 7 < f(-1) ⇒ 2 < f(6). +Proof. +Assume that (7 < f(-1)). +By (f_increasing) it holds that (f(-1) ≤ f(6)) (i). +We conclude that (2 < f(6)). +Qed. +``` + +## `We claim that ((*statement*)).` + +Lets you first show (\*statement\*) before continuing with the rest of the proof. After you showed (\*statement\*), it will be available as a hypothesis with name (\*optional_name\*). + +```coq +We claim that (2 = 2) (two_is_two). +``` + +## `We claim that ((*statement*)) ((*label*)).` + +Lets you first show (\*statement\*) before continuing with the rest of the proof. After you showed (\*statement\*), it will be available as a hypothesis with name (\*label\*). + +```coq +We claim that (2 = 2) (two_is_two). +``` + +## `We argue by contradiction.` + +Assumes the opposite of what you need to show. Afterwards, you need to make substeps that show that both A and ¬ A (i.e. not A) for some statement A. Finally, you can finish your proof with 'Contradiction.' + +```coq +Lemma example_contradicition : + ∀ x ∈ ℝ, + (∀ ε > 0, x > 1 - ε) ⇒ + x ≥ 1. +Proof. +Take x ∈ (ℝ). +Assume that (∀ ε > 0, x > 1 - ε) (i). +We need to show that (x ≥ 1). +We argue by contradiction. +Assume that (¬ (x ≥ 1)). +It holds that ((1 - x) > 0). +By (i) it holds that (x > 1 - (1 - x)). +Contradiction. +Qed. +``` + +## `Contradiction` + +If you have shown both A and not A for some statement A, you can write "Contradiction" to finish the proof of the current goal. + +```coq +Contradiction. +``` + +## `Because ((*name_combined_hyp*)) both ((*statement_1*)) and ((*statement_2*)).` + +If you currently have a hypothesis with name (\*name_combined_hyp\*) which is in fact of the form H1 ∧ H2, then this tactic splits it up in two separate hypotheses. + +```coq +Lemma and_example : for all A B : Prop, A ∧ B ⇒ A. +Take A : Prop. Take B : Prop. +Assume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii). +``` + +## `Because ((*name_combined_hyp*)) both ((*statement_1*)) ((*label_1*)) and ((*statement_2*)) ((*label_2*)).` + +If you currently have a hypothesis with name (\*name_combined_hyp\*) which is in fact of the form H1 ∧ H2, then this tactic splits it up in two separate hypotheses. + +```coq +Lemma and_example : for all A B : Prop, A ∧ B ⇒ A. +Take A : Prop. Take B : Prop. +Assume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii). +``` + +## `Either ((*case_1*)) or ((*case_2*)).` + +Split in two cases (\*case_1\*) and (\*case_2\*). + +```coq +Lemma example_cases : + ∀ x ∈ ℝ, ∀ y ∈ ℝ, + Rmax(x,y) = x ∨ Rmax(x,y) = y. +Proof. +Take x ∈ ℝ. Take y ∈ ℝ. +Either (x < y) or (x ≥ y). +- Case (x < y). + It suffices to show that (Rmax(x,y) = y). + We conclude that (Rmax(x,y) = y). +- Case (x ≥ y). + It suffices to show that (Rmax(x,y) = x). + We conclude that (Rmax(x,y) = x). +Qed. +``` + +## `Expand the definition of (*name_kw*).` + +Expands the definition of the keyword (\*name_kw\*) in relevant statements in the proof, and gives suggestions on how to use them. + +```coq +Expand the definition of upper bound. +``` + +## `Expand the definition of (*name_kw*) in ((*expression*)).` + +Expands the definition of the keyword (\*name_kw\*) in the statement (\*expression\*). + +```coq +Expand the definition of upper bound in (4 is an upper bound for [0, 3)). +``` + +## `We show both statements.` + +Splits the goal in two separate goals, if it is of the form A ∧ B + +```coq +Lemma example_both_statements: + ∀ x ∈ ℝ, (x^2 ≥ 0) ∧ (| x | ≥ 0). +Proof. +Take x ∈ (ℝ). +We show both statements. +- We conclude that (x^2 ≥ 0). +- We conclude that (| x | ≥ 0). +Qed. +``` + +## `We show both directions.` + +Splits a goal of the form A ⇔ B, into the goals (A ⇒ B) and (B ⇒ A) + +```coq +Lemma example_both_directions: + ∀ x ∈ ℝ, ∀ y ∈ ℝ, + x < y ⇔ y > x. +Proof. +Take x ∈ (ℝ). Take y ∈ (ℝ). +We show both directions. +- We need to show that (x < y ⇒ y > x). + Assume that (x < y). + We conclude that (y > x). +- We need to show that (y > x ⇒ x < y). + Assume that (y > x). + We conclude that (x < y). +Qed. +``` + +## `We use induction on (*name_var*).` + +Prove a statement by induction on the variable with (\*name_var\*). + +```coq +Lemma example_induction : + ∀ n : ℕ → ℕ, (∀ k ∈ ℕ, n(k) < n(k + 1))%nat ⇒ + ∀ k ∈ ℕ, (k ≤ n(k))%nat. +Proof. +Take n : (ℕ → ℕ). +Assume that (∀ k ∈ ℕ, n(k) < n(k + 1))%nat (i). +We use induction on k. +- We first show the base case, namely (0 ≤ n(0))%nat. + We conclude that (0 ≤ n(0))%nat. +- We now show the induction step. + Take k ∈ ℕ. + Assume that (k ≤ n(k))%nat. + By (i) it holds that (n(k) < n(k + 1))%nat. + We conclude that (& k + 1 ≤ n(k) + 1 ≤ n(k + 1))%nat. +Qed. +``` + +## `By ((*lemma or assumption*)) we conclude that ((*(alternative) formulation of current goal*)).` + +Tries to directly prove the goal using (\*lemma or assumption\*) when the goal corresponds to (\*statement\*). + +## `Define (*name*) := ((*expression*)).` + +Temporarily give the name (\*name\*) to the expression (\*expression\*) + +## `Since ((*extra_statement*)) it holds that ((*statement*)).` + +Tries to first verify (\*extra_statement\*) after it uses that to verify (\*statement\*). The statement gets added as a hypothesis. + +```coq +Since (x = y) it holds that (x = z). +``` + +## `Since ((*extra_statement*)) it holds that ((*statement*)) ((*label*)).` + +Tries to first verify (\*extra_statement\*) after it uses that to verify (\*statement\*). The statement gets added as a hypothesiwe need to show{s, optionally with the name (\*optional_label\*). + +```coq +Since (x = y) it holds that (x = z). +``` + +## `Since ((*extra_statement*)) we conclude that ((*(alternative) formulation of current goal*)).` + +Tries to automatically prove the current goal, after first trying to prove (\*extra_statement\*). + +```coq +Since (x = y) we conclude that (x = z). +``` + +## `Since ((*extra_statement*)) it suffices to show that ((*statement*)).` + +Waterproof tries to verify automatically whether it is indeed enough to show (\*statement\*) to prove the current goal, after first trying to prove (\*extra_statement\*). If so, (\*statement\*) becomes the new goal. + +```coq +Lemma example_backwards : + 3 < f(0) ⇒ 2 < f(5). +Proof. +Assume that (3 < f(0)). +It suffices to show that (f(0) ≤ f(5)). +By (f_increasing) we conclude that (f(0) ≤ f(5)). +Qed. +``` + +## `Use (*name*) := ((*expression*)) in ((*label*)).` + +Use a forall statement, i.e. apply it to a particular expression. + +```coq +Lemma example_use_for_all : + ∀ x ∈ ℝ, + (∀ ε > 0, x < ε) ⇒ + x + 1/2 < 1. +Proof. +Take x ∈ ℝ. +Assume that (∀ ε > 0, x < ε) (i). +Use ε := (1/2) in (i). +* Indeed, (1 / 2 > 0). +* It holds that (x < 1 / 2). + We conclude that (x + 1/2 < 1). +Qed. +``` + +## `Indeed, ((*statement*)).` + +A synonym for "We conclude that ((\*statement\*))". + +```coq +Lemma example_choose : + ∃ y ∈ ℝ, + y < 3. +Proof. +Choose y := (2). +* Indeed, (y ∈ ℝ). +* We conclude that (y < 3). +Qed. +``` + +## `We need to verify that ((*statement*)).` + +Used to indicate what to check after using the "Choose" or "Use" tactic. + +```coq +Lemma example_choose : + ∃ y ∈ ℝ, + y < 3. +Proof. +Choose y := (2). +* We need to verify that (y ∈ ℝ). +We conclude that (y ∈ ℝ). +* We conclude that (y < 3). +Qed. +``` + +## `By magic it holds that ((*statement*)) ((*label*)).` + +Postpones the proof of (\*statement\*), and (\*statement\*) is added as a hypothesis with name (\*optional_label\*). You can leave out ((\*optional_label\*)) if you don't wish to name the statement. + +```coq +Lemma example_forwards : + 7 < f(-1) ⇒ 2 < f(6). +Proof. +Assume that (7 < f(-1)). +By magic it holds that (f(-1) ≤ f(6)) (i). +We conclude that (2 < f(6)). +Qed. +``` + +## `By magic it holds that ((*statement*)).` + +Postpones the proof of (\*statement\*), and (\*statement\*) is added as a hypothesis. + +```coq +Lemma example_forwards : + 7 < f(-1) ⇒ 2 < f(6). +Proof. +Assume that (7 < f(-1)). +By magic it holds that (f(-1) ≤ f(6)) (i). +We conclude that (2 < f(6)). +Qed. +``` + +## `By magic we conclude that ((*(alternative) formulation of current goal*)).` + +Postpones for now the proof of (a possible alternative formulation of) the current goal. + +## `By magic it suffices to show that ((*statement*)).` + +Postpones for now the proof that (\*statement\*) is enough to prove the current goal. Now, (\*statement\*) becomes the new goal. + +```coq +Lemma example_backwards : + 3 < f(0) ⇒ 2 < f(5). +Proof. +Assume that (3 < f(0)). +By magic it suffices to show that (f(0) ≤ f(5)). +By (f_increasing) we conclude that (f(0) ≤ f(5)). +Qed. +``` + +## `Case ((*statement*)).` + +Used to indicate the case after an "Either" sentence. + +```coq +Lemma example_cases : + ∀ x ∈ ℝ, ∀ y ∈ ℝ, + Rmax(x,y) = x ∨ Rmax(x,y) = y. +Proof. +Take x ∈ ℝ. Take y ∈ ℝ. +Either (x < y) or (x ≥ y). +- Case (x < y). + It suffices to show that (Rmax(x,y) = y). + We conclude that (Rmax(x,y) = y). +- Case (x ≥ y). + It suffices to show that (Rmax(x,y) = x). + We conclude that (Rmax(x,y) = x). +Qed. +``` + From d36892b2fd7b5230f1c8db66540e170f42847be5 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Fri, 7 Feb 2025 17:41:22 +0100 Subject: [PATCH 04/93] Hacky stuff to try to enable web extension --- package-lock.json | 1119 +----------------------- package.json | 4 +- src/pm-editor/pmWebview.ts | 11 +- src/version-checker/version-checker.ts | 49 +- src/webviews/coqWebview.ts | 2 +- 5 files changed, 72 insertions(+), 1113 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca7ea87f..b78278b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@types/markdown-it": "12.2.3", "@vscode/webview-ui-toolkit": "1.4.0", "eventemitter2": "6.4.9", + "events": "^3.3.0", "jquery": "3.7.1", "katex": "0.16.11", "markdown-it": "13.0.2", @@ -41,7 +42,8 @@ "react-tooltip": "5.28.0", "throttle-debounce": "5.0.2", "vscode-languageclient": "8.1.0", - "vscode-languageserver-types": "3.17.5" + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" }, "devDependencies": { "@lezer/generator": "^1.2.3", @@ -982,155 +984,6 @@ "node": ">=12" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.5", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@eslint/js": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", - "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.10.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@floating-ui/core": { "version": "1.6.8", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", @@ -1153,67 +1006,6 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2086,41 +1878,6 @@ "exenv-es6": "^1.1.1" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2234,12 +1991,6 @@ "resolved": "https://registry.npmjs.org/@types/double-ended-queue/-/double-ended-queue-2.1.7.tgz", "integrity": "sha512-OVbdb2iOJakEg/Ou6dVZsH0LLxlO+GFjc1FB2W8/jT7bnhoFVJwnZOqi/H26ospeMBaEbGiX3Qy2a7r6pfZKXQ==" }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true - }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -2319,12 +2070,6 @@ "parse5": "^7.0.0" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -2479,202 +2224,6 @@ "@types/node": "*" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", - "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/type-utils": "8.20.0", - "@typescript-eslint/utils": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz", - "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/typescript-estree": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", - "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz", - "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "8.20.0", - "@typescript-eslint/utils": "8.20.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", - "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", - "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", - "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/typescript-estree": "8.20.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", - "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.20.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@vscode/test-electron": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz", @@ -2846,15 +2395,6 @@ "acorn-walk": "^8.0.2" } }, - "node_modules/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, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -2892,22 +2432,6 @@ "node": ">=8" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -4780,12 +4304,6 @@ "node": ">=4.0.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -5124,293 +4642,49 @@ "@esbuild/linux-x64": "0.16.17", "@esbuild/netbsd-x64": "0.16.17", "@esbuild/openbsd-x64": "0.16.17", - "@esbuild/sunos-x64": "0.16.17", - "@esbuild/win32-arm64": "0.16.17", - "@esbuild/win32-ia32": "0.16.17", - "@esbuild/win32-x64": "0.16.17" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/eslint": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", - "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.10.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.18.0", - "@eslint/plugin-kit": "^0.2.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" } }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8.0" } }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "optionalDependencies": { + "source-map": "~0.6.1" } }, "node_modules/esprima": { @@ -5426,30 +4700,6 @@ "node": ">=4" } }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -5477,7 +4727,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "engines": { "node": ">=0.8.x" } @@ -5607,49 +4856,12 @@ "node >=0.6.0" ] }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -5683,18 +4895,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -5759,25 +4959,6 @@ "flat": "cli.js" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true - }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -6081,12 +5262,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -6323,46 +5498,12 @@ } ] }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "dev": true }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -8626,18 +7767,6 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -8845,19 +7974,6 @@ "node": ">=6" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -8974,12 +8090,6 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -9306,15 +8416,6 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -9863,23 +8964,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/ora": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", @@ -10068,18 +9152,6 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -10295,15 +9367,6 @@ "node": ">=10" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -10608,26 +9671,6 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -10911,16 +9954,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -10945,29 +9978,6 @@ "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==" }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -11859,18 +10869,6 @@ "utf8-byte-length": "^1.0.1" } }, - "node_modules/ts-api-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", - "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", - "dev": true, - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, "node_modules/ts-jest": { "version": "29.2.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", @@ -11960,18 +10958,6 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "dev": true }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -12017,28 +11003,6 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.20.0.tgz", - "integrity": "sha512-Kxz2QRFsgbWj6Xcftlw3Dd154b3cEPFqQC+qMZrMypSijPd4UanKKvoKDrJ4o8AIfZFKAF+7sMaEIR8mTElozA==", - "dev": true, - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.20.0", - "@typescript-eslint/parser": "8.20.0", - "@typescript-eslint/utils": "8.20.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", @@ -12125,15 +11089,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/url-join": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", @@ -12376,6 +11331,11 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==" + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -12460,15 +11420,6 @@ "node": ">= 8" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/workerpool": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", diff --git a/package.json b/package.json index 2a3b7065..9b89797d 100644 --- a/package.json +++ b/package.json @@ -511,6 +511,7 @@ "@types/markdown-it": "12.2.3", "@vscode/webview-ui-toolkit": "1.4.0", "eventemitter2": "6.4.9", + "events": "^3.3.0", "jquery": "3.7.1", "katex": "0.16.11", "markdown-it": "13.0.2", @@ -531,7 +532,8 @@ "react-tooltip": "5.28.0", "throttle-debounce": "5.0.2", "vscode-languageclient": "8.1.0", - "vscode-languageserver-types": "3.17.5" + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" }, "dependenciesComments": { "@codemirror/view": "Version 6.28.0 breaks ctrl+v for us, possibly due to switch to EditContext API" diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index cd0ac4aa..2b4326f1 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from "stream"; +import { EventEmitter } from "events"; import { Disposable, EndOfLine, Position, Range, TextDocument, Uri, WebviewPanel, WorkspaceEdit, commands, window, workspace } from "vscode"; import { DocChange, FileFormat, Message, MessageType, WrappingDocChange, LineNumber } from "../../shared"; @@ -82,9 +82,14 @@ export class ProseMirrorWebview extends EventEmitter { this.redoHandler.bind(this)); - var path = require('path') + const basename = (str : string) => { + var base = new String(str).substring(str.lastIndexOf('/') + 1); + if(base.lastIndexOf(".") != -1) + base = base.substring(0, base.lastIndexOf(".")); + return base; + } - const fileName = path.basename(doc.uri.fsPath) + const fileName = basename(doc.uri.fsPath) if (isIllegalFileName(fileName)) { const error = `The file "${fileName}" cannot be opened, most likely because it either contains a space " ", or one of the characters: "\-", "\(", "\)". Please rename the file.` diff --git a/src/version-checker/version-checker.ts b/src/version-checker/version-checker.ts index 0689e92a..c85764db 100644 --- a/src/version-checker/version-checker.ts +++ b/src/version-checker/version-checker.ts @@ -1,6 +1,6 @@ import { ExtensionContext, Uri, WorkspaceConfiguration, commands, env, window } from "vscode"; -import { exec, spawn } from "child_process"; -import path = require("path"); +// import { exec, spawn } from "child_process"; +// import path = require("path"); import { Version, VersionRequirement } from "./version"; import { COMPARE_MODE } from "./types"; @@ -97,18 +97,18 @@ export class VersionChecker { public async checkCoqVersionUsingBinary(): Promise { return new Promise((resolve, reject) => { if (this._wpPath === undefined) return resolve({ reason: "Waterproof.path is undefined" }); - const coqcBinary = path.join(path.parse(this._wpPath).dir, "coqc"); + const coqcBinary = this._wpPath.concat('/', "coqc"); const command = `${coqcBinary} --version`; const regex = /version (?\d+\.\d+\.\d+)/g; - exec(command, (err, stdout, stderr) => { - if (err) resolve({ reason: err.message }); - const groups = regex.exec(stdout)?.groups; - if (groups === undefined) reject("Regex matching on version string went wrong"); - // FIXME: ts-ignore - //@ts-ignore - resolve(Version.fromString(groups["version"])); - }); + // exec(command, (err, stdout, stderr) => { + // if (err) resolve({ reason: err.message }); + // const groups = regex.exec(stdout)?.groups; + // if (groups === undefined) reject("Regex matching on version string went wrong"); + // // FIXME: ts-ignore + // //@ts-ignore + // resolve(Version.fromString(groups["version"])); + // }); }); } @@ -120,19 +120,20 @@ export class VersionChecker { return new Promise((resolve, reject) => { if (this._wpPath === undefined) return resolve({ reason: "Waterproof.path is undefined" }); const ext = process.platform === "win32" ? ".exe" : ""; - const coqtopPath = path.join(path.parse(this._wpPath).dir, `coqtop${ext}`); + // TODO : Fix this + const coqtopPath = this._wpPath.concat('/', `coqtop${ext}`); const printVersionFile = Uri.joinPath(this._context.extensionUri, "misc-includes", "printversion.v").fsPath; const command = `${coqtopPath} -l ${printVersionFile} -set "Coqtop Exit On Error" -batch`; - exec(command, (err, stdout, stderr) => { - if (err) return resolve({ reason: err.message }); + // exec(command, (err, stdout, stderr) => { + // if (err) return resolve({ reason: err.message }); - const [wpVersion, reqCoqVersion] = stdout.trim().split("+"); - const versionCoqWaterproof = Version.fromString(wpVersion); - const versionRequiredCoq = Version.fromString(reqCoqVersion); + // const [wpVersion, reqCoqVersion] = stdout.trim().split("+"); + // const versionCoqWaterproof = Version.fromString(wpVersion); + // const versionRequiredCoq = Version.fromString(reqCoqVersion); - resolve({ wpVersion: versionCoqWaterproof, requiredCoqVersion: versionRequiredCoq }); - }); + // resolve({ wpVersion: versionCoqWaterproof, requiredCoqVersion: versionRequiredCoq }); + // }); }); } @@ -145,11 +146,11 @@ export class VersionChecker { if (this._wpPath === undefined) return resolve({ reason: "Waterproof.path is undefined" }); const command = `${this._wpPath} --version`; - exec(command, (err, stdout, stderr) => { - if (err) return resolve({ reason: err.message }); - const version = Version.fromString(stdout.trim()); - resolve(version); - }); + // exec(command, (err, stdout, stderr) => { + // if (err) return resolve({ reason: err.message }); + // const version = Version.fromString(stdout.trim()); + // resolve(version); + // }); }); } diff --git a/src/webviews/coqWebview.ts b/src/webviews/coqWebview.ts index 16690309..65e360fc 100644 --- a/src/webviews/coqWebview.ts +++ b/src/webviews/coqWebview.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from "stream"; +import { EventEmitter } from "events"; import { Uri, ViewColumn, From 7ea476ac165e027ac6a413bfa17e30124101a925 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Fri, 7 Feb 2025 18:36:24 +0100 Subject: [PATCH 05/93] work in progress --- .vscode/launch.json | 2 +- src/extension.ts | 4 ++-- src/lsp-client/client.ts | 2 +- src/mainBrowser.ts | 1 + src/version-checker/version-checker.ts | 26 +++++++++++++---------- src/webviewManager.ts | 29 +++++++++----------------- 6 files changed, 30 insertions(+), 34 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7c915d10..753a036a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,7 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "--extensionPath=/home/pim/Downloads/coq-lsp_worker and front-end", + "--extensionPath=/home/pim/projects/impermeable/web-experiment/coq-lsp_worker and front-end", "--extensionDevelopmentPath=${workspaceFolder}", "--extensionDevelopmentKind=web", ], diff --git a/src/extension.ts b/src/extension.ts index 54ca2b3e..ef5f2a00 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -394,7 +394,7 @@ export class Waterproof implements Disposable { * Create the lsp client and update relevant status components */ async initializeClient(): Promise { - + console.log("Running initializeClient"); // Run the version checker. const requiredCoqLSPVersion = this.context.extension.packageJSON.requiredCoqLspVersion; const requiredCoqWaterproofVersion = this.context.extension.packageJSON.requiredCoqWaterproofVersion; @@ -423,7 +423,7 @@ export class Waterproof implements Disposable { initializationOptions: serverOptions, markdown: { isTrusted: true, supportHtml: true }, }; - + console.log("Calling clientFactory"); this.client = this.clientFactory(this.context, clientOptions, WaterproofConfigHelper.configuration); return this.client.startWithHandlers(this.webviewManager).then( () => { diff --git a/src/lsp-client/client.ts b/src/lsp-client/client.ts index ef19834a..944da92a 100644 --- a/src/lsp-client/client.ts +++ b/src/lsp-client/client.ts @@ -56,7 +56,7 @@ export function CoqLspClient(Base: T) { constructor(...args: any[]) { super(...args); this.sentenceManager = new SentenceManager(); - + console.log("CoqLspClient constructor"); // forward progress notifications to editor this.fileProgressComponents.push({ dispose() { /* noop */ }, diff --git a/src/mainBrowser.ts b/src/mainBrowser.ts index 608fc9af..a07fcb51 100644 --- a/src/mainBrowser.ts +++ b/src/mainBrowser.ts @@ -25,6 +25,7 @@ const clientFactory: CoqLspClientFactory = (context: ExtensionContext, clientOpt export function activate(context: ExtensionContext): void { + console.log("Browser activate function"); let extension: Waterproof = new Waterproof(context, clientFactory); context.subscriptions.push(extension); // start the lsp client diff --git a/src/version-checker/version-checker.ts b/src/version-checker/version-checker.ts index c85764db..0eb210f6 100644 --- a/src/version-checker/version-checker.ts +++ b/src/version-checker/version-checker.ts @@ -42,18 +42,22 @@ export class VersionChecker { * @returns `Promise` where the boolean indicates whether we can start the extension. */ public async prelaunchChecks(): Promise { - const version = await this.checkLSPBinary(); - if (!isVersionError(version)) { - if (version.needsUpdate(this._reqVersionCoqLSP)) { - this.informUpdateAvailable("coq-lsp", this._reqVersionCoqLSP, version); - } - } else { - - this.informWaterproofPathInvalid(); - - return Promise.resolve(false); - } + console.log("Running prelaunch checks"); + // TODO: Fix cheating return Promise.resolve(true); + // const version = await this.checkLSPBinary(); + // console.log("Version of coq-lsp: ", version); + // if (!isVersionError(version)) { + // if (version.needsUpdate(this._reqVersionCoqLSP)) { + // this.informUpdateAvailable("coq-lsp", this._reqVersionCoqLSP, version); + // } + // } else { + + // this.informWaterproofPathInvalid(); + + // return Promise.resolve(false); + // } + // return Promise.resolve(true); } /** diff --git a/src/webviewManager.ts b/src/webviewManager.ts index f2a387fa..0b11e71a 100644 --- a/src/webviewManager.ts +++ b/src/webviewManager.ts @@ -1,6 +1,5 @@ - -import { EventEmitter, TextDocument, Uri, window } from "vscode"; - +import { TextDocument, Uri, window } from "vscode"; +import { EventEmitter } from "events"; import { Message, MessageType } from "../shared"; import { IExecutor, ILineNumberComponent } from "./components"; import { LineStatusBar } from "./components/lineNumber"; @@ -69,7 +68,7 @@ class ActiveWebviews { /** * The webview manager that manages communication between views */ -export class WebviewManager extends EventTarget { +export class WebviewManager extends EventEmitter { // Tool webviews (UI such as panels), stores the view based on name private readonly _toolWebviews: Map = new Map; @@ -117,8 +116,7 @@ export class WebviewManager extends EventTarget { }); webview.on(WebviewEvents.change, (state) => { if (state == WebviewState.focus && webview.supportInsert) this._active.insert(name); - const event = new CustomEvent(WebviewManagerEvents.updateButton, {'detail' : {name, open: webview.isOpened}}); - this.dispatchEvent(event); + this.emit(WebviewManagerEvents.updateButton, { name, open: webview.isOpened}); }); } @@ -142,14 +140,12 @@ export class WebviewManager extends EventTarget { webview.on(WebviewEvents.change, (state) => { if (state == WebviewState.focus) { this._active.insert(webview.document.uri.toString()); - const event = new CustomEvent(WebviewManagerEvents.focus, {'detail' : webview.document}); - this.dispatchEvent(event); + this.emit(WebviewManagerEvents.focus, webview.document); } }); this._active.insert(webview.document.uri.toString()); - const event = new CustomEvent(WebviewManagerEvents.focus, {'detail' : webview.document}); - this.dispatchEvent(event); + this.emit(WebviewManagerEvents.focus, webview.document); } /** @@ -232,8 +228,7 @@ export class WebviewManager extends EventTarget { callback?.(message.body); break; case MessageType.editorReady: - const event = new CustomEvent(WebviewManagerEvents.editorReady, {'detail' : document}); - this.dispatchEvent(event); + this.emit(WebviewManagerEvents.editorReady, document); break; case MessageType.cursorChange: var pos = document.positionAt(message.body); @@ -242,13 +237,11 @@ export class WebviewManager extends EventTarget { const webview = this._pmWebviews.get(document.uri.toString()); if (!webview) break; if (webview.documentIsUpToDate) { - const event = new CustomEvent(WebviewManagerEvents.cursorChange, {'detail' : {document, pos}}); - this.dispatchEvent(event); + this.emit(WebviewManagerEvents.cursorChange, document, pos); } else { // Document is updating wait for completion const callback = () => { - const event = new CustomEvent(WebviewManagerEvents.cursorChange, {'detail' : {document, pos}}); - this.dispatchEvent(event); + this.emit(WebviewManagerEvents.cursorChange, document, pos); }; if(webview.listeners(WebviewEvents.finishUpdate).length == 0) webview.once(WebviewEvents.finishUpdate, callback); } @@ -282,9 +275,7 @@ export class WebviewManager extends EventTarget { this.postMessage(id, { type: MessageType.insert, body: msg.body }); break; case MessageType.command: - const event = new CustomEvent(WebviewManagerEvents.command, - {'detail' : {'toolWebview': this._toolWebviews.get(id), 'body': msg.body}}); - this.dispatchEvent(event); + this.emit(WebviewManagerEvents.command, this._toolWebviews.get(id), msg.body); break; default: console.error("The message type " + msg.type + " is not currently supported by webview manager"); From e4f34a86263b0855ae19ddfe59b9da1132ad2f40 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Fri, 21 Feb 2025 16:49:24 +0100 Subject: [PATCH 06/93] Wip --- src/mainBrowser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mainBrowser.ts b/src/mainBrowser.ts index a07fcb51..3947ad2f 100644 --- a/src/mainBrowser.ts +++ b/src/mainBrowser.ts @@ -15,11 +15,12 @@ import { Waterproof } from "./extension"; const clientFactory: CoqLspClientFactory = (context: ExtensionContext, clientOptions: LanguageClientOptions, wsConfig: WorkspaceConfiguration) => { const serverMain = Uri.joinPath(context.extensionUri, 'out/src/mainBrowser.js'); const worker = new Worker(serverMain.toString(true)); + const lspWorker = new Worker(Uri.joinPath(context.extensionUri, 'out/coq_lsp_worker.bc.js').toString(true)); return new (CoqLspClient(LanguageClient))( "waterproof", "Waterproof Document Checker", clientOptions, - worker + lspWorker ); }; From 4249edcdf63cdc1db1f173d14437843a646f916e Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Mon, 17 Mar 2025 12:19:54 +0100 Subject: [PATCH 07/93] Semi-proper fixes for web --- .gitignore | 1 + src/extension.ts | 17 +-- src/helpers/file.ts | 21 ++++ src/helpers/index.ts | 3 +- src/mainBrowser.ts | 2 +- src/pm-editor/pmWebview.ts | 12 +-- src/version-checker/version-checker.ts | 144 +++++++++++++++---------- 7 files changed, 123 insertions(+), 77 deletions(-) create mode 100644 src/helpers/file.ts diff --git a/.gitignore b/.gitignore index 2bb309af..cee7c65d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules/ editor_output .DS_store .vscode-test +.vscode-test-web test_out/ .ui-test/ __tests_output__/ diff --git a/src/extension.ts b/src/extension.ts index fb3707e9..1402f6d3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,7 +7,9 @@ import { workspace, window, ConfigurationTarget, - Uri} from "vscode"; + Uri, + env, + UIKind} from "vscode"; import { LanguageClientOptions, RevealOutputChannelOn } from "vscode-languageclient"; import { IExecutor, IGoalsComponent, IStatusComponent } from "./components"; @@ -77,7 +79,7 @@ export class Waterproof implements Disposable { * * @param context the extension context object */ - constructor(context: ExtensionContext, clientFactory: CoqLspClientFactory) { + constructor(context: ExtensionContext, clientFactory: CoqLspClientFactory, private readonly _isWeb = false) { checkConflictingExtensions(); excludeCoqFileTypes(); @@ -399,12 +401,13 @@ export class Waterproof implements Disposable { const requiredCoqLSPVersion = this.context.extension.packageJSON.requiredCoqLspVersion; const requiredCoqWaterproofVersion = this.context.extension.packageJSON.requiredCoqWaterproofVersion; const versionChecker = new VersionChecker(WaterproofConfigHelper.configuration, this.context, requiredCoqLSPVersion, requiredCoqWaterproofVersion); - // - const foundServer = await versionChecker.prelaunchChecks(); - + + const foundServer = this._isWeb ? true : await versionChecker.prelaunchChecks(); + if (foundServer) { - - versionChecker.run(); + if (!this._isWeb) { + versionChecker.run(); + } if (this.client?.isRunning()) { return Promise.reject(new Error("Cannot initialize client; one is already running.")) diff --git a/src/helpers/file.ts b/src/helpers/file.ts new file mode 100644 index 00000000..d630c36c --- /dev/null +++ b/src/helpers/file.ts @@ -0,0 +1,21 @@ +import { Uri } from "vscode"; + + +export class WaterproofFileUtil { + public static getDirectory(filePath: string): string { + const sep = process.platform === "win32" ? "\\" : "/"; + return new String(filePath).substring(0, filePath.lastIndexOf(sep)); + } + + public static getBasename(filePath: Uri): string { + let base = new String(filePath.path).substring(filePath.path.lastIndexOf('/') + 1); + if(base.lastIndexOf(".") != -1) + base = base.substring(0, base.lastIndexOf(".")); + return base; + } + + public static join(...paths: string[]): string { + const sep = process.platform === "win32" ? "\\" : "/"; + return paths.join(sep); + } +} \ No newline at end of file diff --git a/src/helpers/index.ts b/src/helpers/index.ts index b930b640..e76f30b0 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,2 +1,3 @@ export { WaterproofConfigHelper } from "./config-helper"; -export { WaterproofLogger } from "./logger"; \ No newline at end of file +export { WaterproofLogger } from "./logger"; +export { WaterproofFileUtil } from "./file"; \ No newline at end of file diff --git a/src/mainBrowser.ts b/src/mainBrowser.ts index 3947ad2f..a8fd835f 100644 --- a/src/mainBrowser.ts +++ b/src/mainBrowser.ts @@ -27,7 +27,7 @@ const clientFactory: CoqLspClientFactory = (context: ExtensionContext, clientOpt export function activate(context: ExtensionContext): void { console.log("Browser activate function"); - let extension: Waterproof = new Waterproof(context, clientFactory); + let extension: Waterproof = new Waterproof(context, clientFactory, true); context.subscriptions.push(extension); // start the lsp client extension.initializeClient(); diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index 90ebe365..370e5132 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -8,7 +8,7 @@ import { SequentialEditor } from "./edit"; import {getFormatFromExtension, isIllegalFileName } from "./fileUtils"; const SAVE_AS = "Save As"; -import { WaterproofConfigHelper, WaterproofLogger } from "../helpers"; +import { WaterproofConfigHelper, WaterproofFileUtil, WaterproofLogger } from "../helpers"; import { getNonInputRegions, showRestoreMessage } from "./file-utils"; import { CoqEditorProvider } from "./coqEditor"; import { HistoryChangeType } from "../../shared/Messages"; @@ -83,15 +83,7 @@ export class ProseMirrorWebview extends EventEmitter { this.undoHandler.bind(this), this.redoHandler.bind(this)); - // TODOURGENT: Fix - const basename = (str : string) => { - var base = new String(str).substring(str.lastIndexOf('/') + 1); - if(base.lastIndexOf(".") != -1) - base = base.substring(0, base.lastIndexOf(".")); - return base; - } - - const fileName = basename(doc.uri.fsPath) + const fileName = WaterproofFileUtil.getBasename(doc.uri) if (isIllegalFileName(fileName)) { const error = `The file "${fileName}" cannot be opened, most likely because it either contains a space " ", or one of the characters: "-", "(", ")". Please rename the file.` diff --git a/src/version-checker/version-checker.ts b/src/version-checker/version-checker.ts index 664615c4..f2aac061 100644 --- a/src/version-checker/version-checker.ts +++ b/src/version-checker/version-checker.ts @@ -1,8 +1,9 @@ -import { ExtensionContext, Uri, WorkspaceConfiguration, commands, env, window } from "vscode"; +import { ExtensionContext, UIKind, Uri, WorkspaceConfiguration, commands, env, window } from "vscode"; // import { exec, spawn } from "child_process"; // import path = require("path"); import { Version, VersionRequirement } from "./version"; import { COMPARE_MODE } from "./types"; +import { WaterproofFileUtil, WaterproofLogger as wpl } from "../helpers"; export type VersionError = { reason: string; @@ -42,22 +43,19 @@ export class VersionChecker { * @returns `Promise` where the boolean indicates whether we can start the extension. */ public async prelaunchChecks(): Promise { - console.log("Running prelaunch checks"); - // TODOURGENT: Fix cheating - return Promise.resolve(true); - // const version = await this.checkLSPBinary(); - // console.log("Version of coq-lsp: ", version); - // if (!isVersionError(version)) { - // if (version.needsUpdate(this._reqVersionCoqLSP)) { - // this.informUpdateAvailable("coq-lsp", this._reqVersionCoqLSP, version); - // } - // } else { + const version = await this.checkLSPBinary(); + console.log("Version of coq-lsp: ", version); + if (!isVersionError(version)) { + if (version.needsUpdate(this._reqVersionCoqLSP)) { + this.informUpdateAvailable("coq-lsp", this._reqVersionCoqLSP, version); + } + } else { - // this.informWaterproofPathInvalid(); + this.informWaterproofPathInvalid(); - // return Promise.resolve(false); - // } - // return Promise.resolve(true); + return Promise.resolve(false); + } + return Promise.resolve(true); } /** @@ -99,21 +97,20 @@ export class VersionChecker { * @returns */ public async checkCoqVersionUsingBinary(): Promise { - return new Promise((resolve, reject) => { - if (this._wpPath === undefined) return resolve({ reason: "Waterproof.path is undefined" }); - const coqcBinary = this._wpPath.concat('/', "coqc"); - const command = `${coqcBinary} --version`; - const regex = /version (?\d+\.\d+\.\d+)/g; - // TODOURGENT - // exec(command, (err, stdout, stderr) => { - // if (err) resolve({ reason: err.message }); - // const groups = regex.exec(stdout)?.groups; - // if (groups === undefined) reject("Regex matching on version string went wrong"); - // // FIXME: ts-ignore - // //@ts-ignore - // resolve(Version.fromString(groups["version"])); - // }); - }); + if (this._wpPath === undefined) return { reason: "Waterproof.path is undefined" }; + + const coqcBinary = WaterproofFileUtil.join(WaterproofFileUtil.getDirectory(this._wpPath), "coqc"); + const command = `${coqcBinary} --version`; + const regex = /version (?\d+\.\d+\.\d+)/g; + + try { + const stdout = await this.exec(command); + const groups = regex.exec(stdout)?.groups; + if (!groups) throw new Error("Failed to parse version string."); + return Version.fromString(groups["version"]); + } catch (err: any) { + return { reason: err.message }; + } } /** @@ -121,25 +118,23 @@ export class VersionChecker { * @returns */ public async checkWaterproofLib(): Promise<{ wpVersion: Version, requiredCoqVersion: Version } | VersionError> { - return new Promise((resolve, _reject) => { - if (this._wpPath === undefined) return resolve({ reason: "Waterproof.path is undefined" }); - const ext = process.platform === "win32" ? ".exe" : ""; - // TODO : Fix this - const coqtopPath = this._wpPath.concat('/', `coqtop${ext}`); - - const printVersionFile = Uri.joinPath(this._context.extensionUri, "misc-includes", "printversion.v").fsPath; - const command = `${coqtopPath} -l ${printVersionFile} -set "Coqtop Exit On Error" -batch`; - // TODOURGENT: Fix - // exec(command, (err, stdout, stderr) => { - // if (err) return resolve({ reason: err.message }); - - // const [wpVersion, reqCoqVersion] = stdout.trim().split("+"); - // const versionCoqWaterproof = Version.fromString(wpVersion); - // const versionRequiredCoq = Version.fromString(reqCoqVersion); - - // resolve({ wpVersion: versionCoqWaterproof, requiredCoqVersion: versionRequiredCoq }); - // }); - }); + if (this._wpPath === undefined) return { reason: "Waterproof.path is undefined" }; + const ext = process.platform === "win32" ? ".exe" : ""; + + const coqtopPath = WaterproofFileUtil.join(WaterproofFileUtil.getDirectory(this._wpPath), `coqtop${ext}`); + wpl.log(`coqtopPath: ${coqtopPath}`) + const printVersionFile = Uri.joinPath(this._context.extensionUri, "misc-includes", "printversion.v").fsPath; + const command = `${coqtopPath} -l ${printVersionFile} -set "Coqtop Exit On Error" -batch`; + + try { + const stdout = await this.exec(command); + const [wpVersion, reqCoqVersion] = stdout.trim().split("+"); + const versionCoqWaterproof = Version.fromString(wpVersion); + const versionRequiredCoq = Version.fromString(reqCoqVersion); + return { wpVersion: versionCoqWaterproof, requiredCoqVersion: versionRequiredCoq }; + } catch (err: any) { + return { reason: err.message }; + } } /** @@ -147,15 +142,48 @@ export class VersionChecker { * @returns A promise containing either the Version of coq-lsp we found or a VersionError containing an error message. */ private async checkLSPBinary(): Promise { - return new Promise((resolve, _reject) => { - if (this._wpPath === undefined) return resolve({ reason: "Waterproof.path is undefined" }); - const command = `${this._wpPath} --version`; - // TODOURGENT: Fix - // exec(command, (err, stdout, stderr) => { - // if (err) return resolve({ reason: err.message }); - // const version = Version.fromString(stdout.trim()); - // resolve(version); - // }); + if (this._wpPath === undefined) return { reason: "Waterproof.path is undefined" }; + const command = `${this._wpPath} --version`; + + try { + const stdout = await this.exec(command); + const version = Version.fromString(stdout.trim()); + return version; + } catch (err: any) { + return { reason: err.message }; + } + } + + /** Wrapper around shellIntegration */ + private async exec(command: string): Promise { + wpl.log(`Running command: ${command}`) + return new Promise((resolve, reject) => { + const myTerm = window.createTerminal(`Waterproof commands -- ${command}`) + let fired = false; + + window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration}) => { + if (terminal === myTerm && !fired) { + const execution = shellIntegration.executeCommand(command); + const outputStream = execution.read(); + fired = true; + wpl.log(`Type of outputStream: ${typeof outputStream}`) + console.log(`Output stream:`, outputStream) + window.onDidEndTerminalShellExecution(async event => { + if (event.execution === execution) { + + let output = ""; + for await (const data of outputStream) { + output += data + } + wpl.log(`Output of ran command ${output.substring(8)}`) + myTerm.hide(); + myTerm.dispose(); + // Remove terminal-artifacts from the output by taking the first 8 characters + resolve(output.substring(8)); + } + }) + } + }) }); } From a8ccf58367c54105fa351dde62e1098551d7ea3f Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Mon, 17 Mar 2025 12:22:22 +0100 Subject: [PATCH 08/93] Undo debug stuff --- .vscode/launch.json | 1 - .vscode/settings.json | 22 +--------------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 753a036a..d670242e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,6 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "--extensionPath=/home/pim/projects/impermeable/web-experiment/coq-lsp_worker and front-end", "--extensionDevelopmentPath=${workspaceFolder}", "--extensionDevelopmentKind=web", ], diff --git a/.vscode/settings.json b/.vscode/settings.json index 27fc2b66..9101a780 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,23 +1,3 @@ { - "js/ts.implicitProjectConfig.target": "ESNext", - "files.exclude": { - "**/*.vo": true, - "**/*.vok": true, - "**/*.vos": true, - "**/*.aux": true, - "**/*.glob": true, - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/Thumbs.db": true - }, - "terminal.integrated.profiles.linux": { - "bash": { - "path": "bash", - "icon": "terminal-bash", - "args": ["-i"] - } - }, + "js/ts.implicitProjectConfig.target": "ESNext" } \ No newline at end of file From dd2b13ca5d2b0f8fb838344b4d7267c9955367ce Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Mon, 17 Mar 2025 12:29:15 +0100 Subject: [PATCH 09/93] Minor cleanup --- .vscode/launch.json | 2 +- src/extension.ts | 12 ++++++------ src/helpers/logger.ts | 4 ++++ src/version-checker/version-checker.ts | 11 +++++------ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d670242e..046e52a6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,7 @@ "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionDevelopmentKind=web", + "--extensionDevelopmentKind=web" ], "outFiles": ["${workspaceFolder}/out/**/*.js"], "preLaunchTask": "${defaultBuildTask}" diff --git a/src/extension.ts b/src/extension.ts index 1402f6d3..cd826fa2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -32,7 +32,7 @@ import { TacticsPanel } from "./webviews/standardviews/tactics"; import { VersionChecker } from "./version-checker"; import { Utils } from "vscode-uri"; -import { WaterproofConfigHelper, WaterproofLogger } from "./helpers"; +import { WaterproofConfigHelper, WaterproofLogger as wpl } from "./helpers"; @@ -191,7 +191,7 @@ export class Waterproof implements Disposable { try { workspace.getConfiguration().update("waterproof.path", defaultValue, ConfigurationTarget.Global).then(() => { setTimeout(() => { - WaterproofLogger.log("Waterproof Args setting changed to: " + WaterproofConfigHelper.path.toString()); + wpl.log("Waterproof Args setting changed to: " + WaterproofConfigHelper.path.toString()); window.showInformationMessage(`Waterproof Path setting succesfully updated!`); }, 100); }); @@ -214,7 +214,7 @@ export class Waterproof implements Disposable { try { workspace.getConfiguration().update("waterproof.args", defaultArgs, ConfigurationTarget.Global).then(() => { setTimeout(() => { - WaterproofLogger.log("Waterproof Args setting changed to: " + WaterproofConfigHelper.args.toString()); + wpl.log("Waterproof Args setting changed to: " + WaterproofConfigHelper.args.toString()); window.showInformationMessage(`Waterproof args setting succesfully updated!`); }, 100); @@ -303,7 +303,7 @@ export class Waterproof implements Disposable { try { workspace.getConfiguration().update("waterproof.args", defaultArgs, ConfigurationTarget.Global).then(() => { setTimeout(() => { - WaterproofLogger.log("Waterproof Args setting changed to: " + WaterproofConfigHelper.args.toString()); + wpl.log("Waterproof Args setting changed to: " + WaterproofConfigHelper.args.toString()); }, 100); }); } catch (e) { @@ -396,7 +396,7 @@ export class Waterproof implements Disposable { * Create the lsp client and update relevant status components */ async initializeClient(): Promise { - console.log("Running initializeClient"); + wpl.log("Running initializeClient"); // Run the version checker. const requiredCoqLSPVersion = this.context.extension.packageJSON.requiredCoqLspVersion; const requiredCoqWaterproofVersion = this.context.extension.packageJSON.requiredCoqWaterproofVersion; @@ -426,7 +426,7 @@ export class Waterproof implements Disposable { initializationOptions: serverOptions, markdown: { isTrusted: true, supportHtml: true }, }; - console.log("Calling clientFactory"); + wpl.log("Calling clientFactory"); this.client = this.clientFactory(this.context, clientOptions, WaterproofConfigHelper.configuration); return this.client.startWithHandlers(this.webviewManager).then( () => { diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts index a1ea67d3..79a66c5a 100644 --- a/src/helpers/logger.ts +++ b/src/helpers/logger.ts @@ -13,6 +13,10 @@ export class WaterproofLogger { WaterproofLogger.outputChannel.appendLine(message); } + static debug(message: string) { + this.log(message) + } + static show() { if (!WaterproofLogger.outputChannel) { WaterproofLogger.outputChannel = window.createOutputChannel(OUTPUT_CHANNEL_NAME); diff --git a/src/version-checker/version-checker.ts b/src/version-checker/version-checker.ts index f2aac061..0280fc08 100644 --- a/src/version-checker/version-checker.ts +++ b/src/version-checker/version-checker.ts @@ -44,7 +44,7 @@ export class VersionChecker { */ public async prelaunchChecks(): Promise { const version = await this.checkLSPBinary(); - console.log("Version of coq-lsp: ", version); + wpl.log(`Version of coq-lsp: ${version}`); if (!isVersionError(version)) { if (version.needsUpdate(this._reqVersionCoqLSP)) { this.informUpdateAvailable("coq-lsp", this._reqVersionCoqLSP, version); @@ -122,7 +122,7 @@ export class VersionChecker { const ext = process.platform === "win32" ? ".exe" : ""; const coqtopPath = WaterproofFileUtil.join(WaterproofFileUtil.getDirectory(this._wpPath), `coqtop${ext}`); - wpl.log(`coqtopPath: ${coqtopPath}`) + wpl.debug(`coqtopPath: ${coqtopPath}`) const printVersionFile = Uri.joinPath(this._context.extensionUri, "misc-includes", "printversion.v").fsPath; const command = `${coqtopPath} -l ${printVersionFile} -set "Coqtop Exit On Error" -batch`; @@ -166,16 +166,15 @@ export class VersionChecker { const execution = shellIntegration.executeCommand(command); const outputStream = execution.read(); fired = true; - wpl.log(`Type of outputStream: ${typeof outputStream}`) - console.log(`Output stream:`, outputStream) + wpl.debug(`Type of outputStream: ${typeof outputStream}`) + wpl.debug(`Output stream: ${outputStream}`) window.onDidEndTerminalShellExecution(async event => { if (event.execution === execution) { - let output = ""; for await (const data of outputStream) { output += data } - wpl.log(`Output of ran command ${output.substring(8)}`) + wpl.debug(`Output of ran command ${output.substring(8)}`) myTerm.hide(); myTerm.dispose(); // Remove terminal-artifacts from the output by taking the first 8 characters From b640f78b3112247c3ac516bba79a42fcb19c1469 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Mon, 17 Mar 2025 12:33:16 +0100 Subject: [PATCH 10/93] Cleanup --- src/version-checker/version-checker.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/version-checker/version-checker.ts b/src/version-checker/version-checker.ts index 0280fc08..25e53b17 100644 --- a/src/version-checker/version-checker.ts +++ b/src/version-checker/version-checker.ts @@ -108,8 +108,10 @@ export class VersionChecker { const groups = regex.exec(stdout)?.groups; if (!groups) throw new Error("Failed to parse version string."); return Version.fromString(groups["version"]); - } catch (err: any) { - return { reason: err.message }; + } catch (err: unknown) { + return err instanceof Error + ? { reason: err.message } + : { reason: "Unknown error" }; } } @@ -132,8 +134,10 @@ export class VersionChecker { const versionCoqWaterproof = Version.fromString(wpVersion); const versionRequiredCoq = Version.fromString(reqCoqVersion); return { wpVersion: versionCoqWaterproof, requiredCoqVersion: versionRequiredCoq }; - } catch (err: any) { - return { reason: err.message }; + } catch (err: unknown) { + return err instanceof Error + ? { reason: err.message } + : { reason: "Unknown error" }; } } @@ -149,15 +153,17 @@ export class VersionChecker { const stdout = await this.exec(command); const version = Version.fromString(stdout.trim()); return version; - } catch (err: any) { - return { reason: err.message }; + } catch (err: unknown) { + return err instanceof Error + ? { reason: err.message } + : { reason: "Unknown error" }; } } /** Wrapper around shellIntegration */ private async exec(command: string): Promise { wpl.log(`Running command: ${command}`) - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { const myTerm = window.createTerminal(`Waterproof commands -- ${command}`) let fired = false; From 534dcff3806bb955c1759bdbc32e956b4f881f5d Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Mon, 17 Mar 2025 13:46:57 +0100 Subject: [PATCH 11/93] Lint fixes --- src/extension.ts | 8 +++----- src/mainBrowser.ts | 7 ++++--- src/pm-editor/pmWebview.ts | 2 -- src/version-checker/version-checker.ts | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index cd826fa2..0d4c2099 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,9 +7,7 @@ import { workspace, window, ConfigurationTarget, - Uri, - env, - UIKind} from "vscode"; + Uri} from "vscode"; import { LanguageClientOptions, RevealOutputChannelOn } from "vscode-languageclient"; import { IExecutor, IGoalsComponent, IStatusComponent } from "./components"; @@ -274,8 +272,8 @@ export class Waterproof implements Disposable { * Attempts to install all required libraries * @returns A promise containing either the Version of coq-lsp we found or a VersionError containing an error message. */ - private async autoInstall(command: string): Promise { - return new Promise((resolve, reject) => { + private async autoInstall(command: string): Promise { + return new Promise((resolve, _reject) => { const myTerm = window.createTerminal(`AutoInstall Waterproof`) window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration}) => { if (terminal === myTerm) { diff --git a/src/mainBrowser.ts b/src/mainBrowser.ts index a8fd835f..5f20c78d 100644 --- a/src/mainBrowser.ts +++ b/src/mainBrowser.ts @@ -12,9 +12,10 @@ import { Waterproof } from "./extension"; * @param wsConfig the workspace configuration of Waterproof * @returns an LSP client with the added functionality of `CoqFeatures` */ -const clientFactory: CoqLspClientFactory = (context: ExtensionContext, clientOptions: LanguageClientOptions, wsConfig: WorkspaceConfiguration) => { +const clientFactory: CoqLspClientFactory = (context: ExtensionContext, clientOptions: LanguageClientOptions, _wsConfig: WorkspaceConfiguration) => { const serverMain = Uri.joinPath(context.extensionUri, 'out/src/mainBrowser.js'); - const worker = new Worker(serverMain.toString(true)); + // Start our own webworker + new Worker(serverMain.toString(true)); const lspWorker = new Worker(Uri.joinPath(context.extensionUri, 'out/coq_lsp_worker.bc.js').toString(true)); return new (CoqLspClient(LanguageClient))( "waterproof", @@ -27,7 +28,7 @@ const clientFactory: CoqLspClientFactory = (context: ExtensionContext, clientOpt export function activate(context: ExtensionContext): void { console.log("Browser activate function"); - let extension: Waterproof = new Waterproof(context, clientFactory, true); + const extension: Waterproof = new Waterproof(context, clientFactory, true); context.subscriptions.push(extension); // start the lsp client extension.initializeClient(); diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index 370e5132..ab8b7cf5 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -13,8 +13,6 @@ import { getNonInputRegions, showRestoreMessage } from "./file-utils"; import { CoqEditorProvider } from "./coqEditor"; import { HistoryChangeType } from "../../shared/Messages"; -import * as path from 'path'; - export class ProseMirrorWebview extends EventEmitter { /** The webview panel of this ProseMirror instance */ private _panel: WebviewPanel; diff --git a/src/version-checker/version-checker.ts b/src/version-checker/version-checker.ts index 25e53b17..183364ac 100644 --- a/src/version-checker/version-checker.ts +++ b/src/version-checker/version-checker.ts @@ -1,4 +1,4 @@ -import { ExtensionContext, UIKind, Uri, WorkspaceConfiguration, commands, env, window } from "vscode"; +import { ExtensionContext, Uri, WorkspaceConfiguration, commands, env, window } from "vscode"; // import { exec, spawn } from "child_process"; // import path = require("path"); import { Version, VersionRequirement } from "./version"; From a20c1b49671808d6d137488cdc9e0478f9c22d58 Mon Sep 17 00:00:00 2001 From: raulTUe Date: Tue, 8 Apr 2025 00:30:36 +0200 Subject: [PATCH 12/93] add warning for trimming whitespace setting, and modified skeleton code for onsave edits --- editor/src/index.ts | 3 ++ editor/src/kroqed-editor/editor.ts | 48 ++++++++++++++++++++++++++++++ shared/Messages.ts | 2 ++ src/extension.ts | 3 +- src/pm-editor/pmWebview.ts | 16 ++++++++++ src/util.ts | 24 +++++++++++++-- 6 files changed, 93 insertions(+), 3 deletions(-) diff --git a/editor/src/index.ts b/editor/src/index.ts index c0f266dd..38fa93dd 100644 --- a/editor/src/index.ts +++ b/editor/src/index.ts @@ -49,6 +49,9 @@ window.onload = () => { theEditor.insertSymbol(symbolUnicode, symbolLatex); } break; } + case MessageType.updateDocument: + theEditor.updateDocument(msg.body.value, msg.body.version); + break; case MessageType.setAutocomplete: // Handle autocompletion { const state = theEditor.state; diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/kroqed-editor/editor.ts index ccc904b4..7c792421 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/kroqed-editor/editor.ts @@ -155,6 +155,54 @@ export class Editor { this.post({ type: MessageType.editorReady }); } + updateDocument(content: string, version: number) { + // // Ensure we have an editor view to update. + // if (!this._view) { + // //Editor view not initialized. Cannot update document + // return; + // } + + // // If the new content is the same as the current document, do nothing. + // const currentText = this._view.state.doc.textContent; + // if (content === currentText) { + // return; + // } + + // // Preprocess the content. checkPrePost may adjust the content and send messages. + // let newContent = checkPrePost(content, (msg: Message) => { + // this.post(msg); + // }); + + // // If the preprocessing changed the content, update the version. + // if (newContent !== content) { + // version++; + // this._translator = new FileTranslator(this._filef); + + // // Translate the updated content into a ProseMirror document. + // const parsedContent = this._translator.toProsemirror(newContent); + + // // Update the document mapping with the new content and version. + // this._mapping = new TextDocMapping(this._filef, parsedContent, version); + + // const proseDoc = createProseMirrorDocument(newContent, this._filef); + + // // Create a new editor state with the new document. + // const newState = EditorState.create({ + // schema: this._schema, + // doc: proseDoc, + // plugins: this.createPluginsArray() + // }); + + // // Update the editor view's state in place. + // this._view.updateState(newState); + + // // Send an update for line numbers if they are enabled. + // this.sendLineNumbers(); + // } + + } + + get state(): EditorState | undefined { return this._view?.state; } diff --git a/shared/Messages.ts b/shared/Messages.ts index 609447cf..8a436791 100644 --- a/shared/Messages.ts +++ b/shared/Messages.ts @@ -35,6 +35,7 @@ export type Message = | MessageBase | MessageBase | MessageBase + | MessageBase | MessageBase | MessageBase | MessageBase @@ -60,6 +61,7 @@ export const enum MessageType { update, init, ready, + updateDocument, editorReady, docChange, cursorChange, diff --git a/src/extension.ts b/src/extension.ts index 7edb747e..fecebf64 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,7 +15,7 @@ import { CoqnitiveStatusBar } from "./components/enableButton"; import { CoqLspClient, CoqLspClientConfig, CoqLspClientFactory, CoqLspServerConfig } from "./lsp-client/clientTypes"; import { executeCommand } from "./lsp-client/commandExecutor"; import { CoqEditorProvider } from "./pm-editor"; -import { checkConflictingExtensions, excludeCoqFileTypes } from "./util"; +import { checkConflictingExtensions, checkTrimmingWhitespace, excludeCoqFileTypes } from "./util"; import { WebviewManager, WebviewManagerEvents } from "./webviewManager"; import { DebugPanel } from "./webviews/goalviews/debug"; import { GoalsPanel } from "./webviews/goalviews/goalsPanel"; @@ -82,6 +82,7 @@ export class Waterproof implements Disposable { constructor(context: ExtensionContext, clientFactory: CoqLspClientFactory) { checkConflictingExtensions(); excludeCoqFileTypes(); + checkTrimmingWhitespace(); this.context = context; this.clientFactory = clientFactory; diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index b1d4d0bd..5440dd1d 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -177,6 +177,12 @@ export class ProseMirrorWebview extends EventEmitter { } })); + this._disposables.push(workspace.onDidSaveTextDocument(e => { + if (e.uri.toString() === this._document.uri.toString()) { + this.updateOnSave(); // Ensure the editor updates after saving + } + })); + this._disposables.push(workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration("waterproof.teacherMode")) { this.updateTeacherMode(); @@ -266,6 +272,16 @@ export class ProseMirrorWebview extends EventEmitter { for (const m of this._cachedMessages.values()) this.postMessage(m); } + private updateOnSave() { + this.postMessage({ + type: MessageType.updateDocument, + body: { + value: this._document.getText(), + version: this._document.version, + } + }); + } + private updateLineNumberStatusInEditor() { this.updateLineNumbers(); this.postMessage({ diff --git a/src/util.ts b/src/util.ts index 154fec5e..ae7fd7d1 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,4 @@ -import { extensions, window, workspace } from "vscode"; +import { extensions, window, commands, workspace } from "vscode"; /** * Returns a random alphanumerical string of length 32. @@ -32,6 +32,26 @@ export function checkConflictingExtensions() { } } +/** + * Checks whether the user has enabled the setting `trimTrailingWhitespace`, and warns the + * user in that case. + */ +export function checkTrimmingWhitespace() { + const config = workspace.getConfiguration('files'); + const isTrimTrailingWhitespaceEnabled = config.get('trimTrailingWhitespace'); + if (isTrimTrailingWhitespaceEnabled) { + window.showWarningMessage( + "The setting `Trim Trailing Whitespace` is enabled. This may cause unexpected behaviour, and is strongly suggested to turned off.", + "Open Settings", + "Dismiss" + ).then(selection => { + if (selection === "Open Settings") { + commands.executeCommand('workbench.action.openSettings', 'Trim Trailing Whitespace'); + } + }); + } +} + /** * Checks whether the user wants to ignore Coq object files and adjusts the workspace * configuration accordingly. @@ -40,7 +60,7 @@ export function excludeCoqFileTypes() { const activationConfig = workspace.getConfiguration(); const updateIgnores = activationConfig.get("waterproof.updateIgnores") ?? true; if (updateIgnores) { - // TODO: Look at + // TODO: Look at // eslint-disable-next-line @typescript-eslint/no-explicit-any const fexc : any = activationConfig.get("files.exclude"); activationConfig.update("files.exclude", { From 8ea44aebe3897bb36c0373c8cd24d394d9a2e81a Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 8 Apr 2025 09:53:14 +0200 Subject: [PATCH 13/93] Small fixes --- package-lock.json | 4 ++-- package.json | 4 ++-- src/lsp-client/client.ts | 4 ++-- src/version-checker/version-checker.ts | 2 -- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index b86e1911..e06a727c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@types/markdown-it": "12.2.3", "@vscode/webview-ui-toolkit": "1.4.0", "eventemitter2": "6.4.9", - "events": "^3.3.0", + "events": "3.3.0", "jquery": "3.7.1", "katex": "0.16.11", "markdown-it": "13.0.2", @@ -43,7 +43,7 @@ "throttle-debounce": "5.0.2", "vscode-languageclient": "8.1.0", "vscode-languageserver-types": "3.17.5", - "vscode-uri": "^3.1.0" + "vscode-uri": "3.1.0" }, "devDependencies": { "@eslint/js": "^9.18.0", diff --git a/package.json b/package.json index ef338e61..8be6cdce 100644 --- a/package.json +++ b/package.json @@ -514,7 +514,7 @@ "@types/markdown-it": "12.2.3", "@vscode/webview-ui-toolkit": "1.4.0", "eventemitter2": "6.4.9", - "events": "^3.3.0", + "events": "3.3.0", "jquery": "3.7.1", "katex": "0.16.11", "markdown-it": "13.0.2", @@ -536,7 +536,7 @@ "throttle-debounce": "5.0.2", "vscode-languageclient": "8.1.0", "vscode-languageserver-types": "3.17.5", - "vscode-uri": "^3.1.0" + "vscode-uri": "3.1.0" }, "dependenciesComments": { "@codemirror/view": "Version 6.28.0 breaks ctrl+v for us, possibly due to switch to EditContext API" diff --git a/src/lsp-client/client.ts b/src/lsp-client/client.ts index 6e34d5c8..01bdd49d 100644 --- a/src/lsp-client/client.ts +++ b/src/lsp-client/client.ts @@ -17,7 +17,7 @@ import { ICoqLspClient, WpDiagnostic } from "./clientTypes"; import { determineProofStatus, getInputAreas } from "./qedStatus"; import { convertToSimple, fileProgressNotificationType, goalRequestType } from "./requestTypes"; import { SentenceManager } from "./sentenceManager"; -import { WaterproofConfigHelper } from "../helpers"; +import { WaterproofConfigHelper, WaterproofLogger as wpl } from "../helpers"; interface TimeoutDisposable extends Disposable { dispose(timeout?: number): Promise; @@ -60,7 +60,7 @@ export function CoqLspClient(Base: T) { constructor(...args: any[]) { super(...args); this.sentenceManager = new SentenceManager(); - console.log("CoqLspClient constructor"); + wpl.debug("CoqLspClient constructor"); // forward progress notifications to editor this.fileProgressComponents.push({ dispose() { /* noop */ }, diff --git a/src/version-checker/version-checker.ts b/src/version-checker/version-checker.ts index 183364ac..50e049ef 100644 --- a/src/version-checker/version-checker.ts +++ b/src/version-checker/version-checker.ts @@ -1,6 +1,4 @@ import { ExtensionContext, Uri, WorkspaceConfiguration, commands, env, window } from "vscode"; -// import { exec, spawn } from "child_process"; -// import path = require("path"); import { Version, VersionRequirement } from "./version"; import { COMPARE_MODE } from "./types"; import { WaterproofFileUtil, WaterproofLogger as wpl } from "../helpers"; From b7d240abdf016d4de35bd4d5710aed40edc03928 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 8 Apr 2025 09:58:28 +0200 Subject: [PATCH 14/93] Documentation --- developer-resources/developer_instructions.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/developer-resources/developer_instructions.md b/developer-resources/developer_instructions.md index dddcbee6..2ba77ecb 100644 --- a/developer-resources/developer_instructions.md +++ b/developer-resources/developer_instructions.md @@ -31,4 +31,10 @@ Insert the following setting into your `.vscode/settings.json` to stop it from d { "js/ts.implicitProjectConfig.target": "ESNext" } -``` \ No newline at end of file +``` + +### Running a debug version of the webextension + +1. Obtain `coq-lsp_worker and front-end.zip` from the coq-lsp CI artifacts. (Latest build at the time of writing: https://github.com/ejgallego/coq-lsp/actions/runs/13566988935) (Build for 8.17 has serlib errors at this point in time). +2. Unzip the file. Move `coq_lsp_worker.bc.js` to `out. +3. Run the `Run Web Extension in VS Code` task. \ No newline at end of file From 8c333543a82a134f909adbba5281435bb88f76d9 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 8 Apr 2025 10:07:05 +0200 Subject: [PATCH 15/93] Correct default name --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 0d4c2099..f91794c3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -310,7 +310,7 @@ export class Waterproof implements Disposable { } private async waterproofTutorialCommand(): Promise { - const defaultUri = Utils.joinPath(workspace.workspaceFolders![0].uri, "new_waterproof_document.mv"); + const defaultUri = Utils.joinPath(workspace.workspaceFolders![0].uri, "waterproof_tutorial.mv"); window.showSaveDialog({filters: {'Waterproof': ["mv", "v"]}, title: "Waterproof Tutorial", defaultUri}).then((uri) => { if (!uri) { window.showErrorMessage("Something went wrong in saving the Waterproof tutorial file"); From ea6ef3273909030c643d27da56bbb352bb8cd0b9 Mon Sep 17 00:00:00 2001 From: "@20210857" Date: Mon, 21 Apr 2025 17:36:43 +0000 Subject: [PATCH 16/93] Fixed button transparency when reopening side panel --- src/webviews/sidePanel.ts | 58 +++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index f131ca4d..3b15dcb0 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -11,10 +11,12 @@ export function addSidePanel(context: vscode.ExtensionContext, manager: WebviewM } export class SidePanelProvider implements vscode.WebviewViewProvider { - public static readonly viewType = 'options.webview'; + public static readonly viewType = 'sidePanel'; private _view?: vscode.WebviewView; private _transparencyMap: Map = new Map(); + private readonly _greyedOutButtons: Set = new Set(); + constructor( private readonly _extensionUri: vscode.Uri, @@ -27,10 +29,20 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { this.updateButtonTransparency(e.name, e.open); }); } - + public greyedOutButtons(buttonId: string, open: boolean): string[] { + if (this._greyedOutButtons.has(buttonId) && !open) { + this._greyedOutButtons.delete(buttonId); + } else if (open){ + this._greyedOutButtons.add(buttonId); + } + + return Array.from(this._greyedOutButtons); + } public updateButtonTransparency(buttonId: string, open: boolean) { // Set the transparency state of the button in the transparency map this._transparencyMap.set(buttonId, open); + //update the list of buttons that are currently greyed out + const greyedOut = this.greyedOutButtons(buttonId,open); // Update the button styles to reflect the transparency state this.updateButtonStyles(buttonId, open); } @@ -49,11 +61,11 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { public resolveWebviewView( webviewView: vscode.WebviewView, - _context: vscode.WebviewViewResolveContext, + context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken, ) { this._view = webviewView; - + // Set options for the webview webviewView.webview.options = { enableScripts: true, @@ -75,10 +87,21 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { this._manager.open(message.command); } }); + // Handle the visibility of the webview when user reopens side panel + webviewView.onDidChangeVisibility(() => { + if (webviewView.visible) { + + webviewView.webview.postMessage({ + type: 'restoreTransparency', + greyedOutButtonsList: this.greyedOutButtons("goals",true) //the goals panel is always opened + }); + } + }); + } // Now we create the actual web page - private _getHtmlForWebview(_webview: vscode.Webview) { + private _getHtmlForWebview(webview: vscode.Webview) { const nonce = getNonce(); // html code for the webview @@ -185,9 +208,10 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { //check for messages window.addEventListener('message', event => { const message = event.data; + switch (message.type) { // If the message is for changing the transparency - case 'trans': + case 'trans': { const button = document.getElementById(message.buttonId); if (button) { const transparent = message.open; @@ -199,6 +223,28 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { button.classList.remove('transparent'); } } + break; + } + //If the message is for restoring transparency after side panel is reopened + case 'restoreTransparency': { + const buttonIds = [ + 'goals', 'logbook', 'debug', 'execute', + 'help', 'search', 'expandDefinition', 'symbols', 'tactics' + ]; + buttonIds.forEach(id => { + const btn = document.getElementById(id); + if (!btn) return; + // Check if the button ID is in the greyedOutButtonsList + if (message.greyedOutButtonsList?.includes(id)) { + // If it is, make transparent + btn.classList.add('transparent'); + } else { + // If it is not, remove the transparent class + btn.classList.remove('transparent'); + } + }); + break; + } } }); From 14faf824815050f911dbbc147142523f19755e07 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 22 Apr 2025 10:52:50 +0200 Subject: [PATCH 17/93] Update error message --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 40905b4a..6adecdba 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -353,7 +353,7 @@ export class Waterproof implements Disposable { commands.executeCommand("vscode.openWith", uri, "coqEditor.coqEditor"); }); }, (err) => { - window.showErrorMessage("Could not a new Waterproof file."); + window.showErrorMessage("Could not create a new Waterproof file."); console.error(`Could not read Waterproof tutorial file: ${err}`); return; }) From 27a803c3dce2ee556c3a4ca09c76c43ae882ce63 Mon Sep 17 00:00:00 2001 From: "@20210857" Date: Tue, 22 Apr 2025 09:15:02 +0000 Subject: [PATCH 18/93] fixing merge conflicts --- src/webviews/sidePanel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index 3b15dcb0..0654e4bf 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -61,7 +61,7 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { public resolveWebviewView( webviewView: vscode.WebviewView, - context: vscode.WebviewViewResolveContext, + _context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken, ) { this._view = webviewView; @@ -101,7 +101,7 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { } // Now we create the actual web page - private _getHtmlForWebview(webview: vscode.Webview) { + private _getHtmlForWebview(_webview: vscode.Webview) { const nonce = getNonce(); // html code for the webview From fcbca80beb27ac74ff32c34695771c3feaca15b4 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 23 Apr 2025 09:51:58 +0200 Subject: [PATCH 19/93] Add show terminal --- src/extension.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension.ts b/src/extension.ts index 6a2a91dd..4ab39617 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -297,6 +297,7 @@ export class Waterproof implements Disposable { private async autoInstall(command: string): Promise { return new Promise((resolve, _reject) => { const myTerm = window.createTerminal(`AutoInstall Waterproof`) + myTerm.show() window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration}) => { if (terminal === myTerm) { const execution = shellIntegration.executeCommand(command); From a90a36919368a7a38a9aa9d2e9d99df5e137ed6c Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 23 Apr 2025 09:58:16 +0200 Subject: [PATCH 20/93] Fix race conditions --- src/extension.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 4ab39617..6d62b4ed 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -244,14 +244,14 @@ export class Waterproof implements Disposable { } }); - this.registerCommand("autoInstall", () => { + this.registerCommand("autoInstall", async () => { commands.executeCommand(`waterproof.defaultPath`); commands.executeCommand(`waterproof.setDefaultArgsWin`); const windowsInstallationScript = `echo Begin Waterproof Installation && echo Downloading installer ... && curl -o Waterproof_Installer.exe -L https://github.com/impermeable/waterproof-dependencies-installer/releases/download/v2.2.0%2B8.17/Waterproof-dependencies-installer-v2.2.0+8.17.exe && echo Installer Finished Downloading - Please wait for the Installer to execute, this can take up to a few minutes && Waterproof_Installer.exe && echo Required Files Installed && del Waterproof_Installer.exe && echo COMPLETE - The Waterproof checker will restart automatically a few seconds after this terminal is closed` const uninstallerLocation = `C:\\cygwin_wp\\home\\runneradmin\\.opam\\wp\\Uninstall.exe` - this.stopClient(); + await this.stopClient(); let cmnd: string | undefined; switch (process.platform) { @@ -279,7 +279,7 @@ export class Waterproof implements Disposable { if (cmnd === undefined) { window.showInformationMessage("Waterproof has no automatic installation process for this platform, please refer to the walktrough page."); } else { - this.autoInstall(cmnd) + await this.autoInstall(cmnd) } }); @@ -296,10 +296,12 @@ export class Waterproof implements Disposable { */ private async autoInstall(command: string): Promise { return new Promise((resolve, _reject) => { + let fired = false; const myTerm = window.createTerminal(`AutoInstall Waterproof`) myTerm.show() window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration}) => { - if (terminal === myTerm) { + if (terminal === myTerm && !fired) { + fired = true const execution = shellIntegration.executeCommand(command); window.onDidEndTerminalShellExecution(event => { if (event.execution === execution) { From 65982d04dc695a14026be4bd1a0659e7353e747c Mon Sep 17 00:00:00 2001 From: raulTUe Date: Tue, 29 Apr 2025 20:22:10 +0200 Subject: [PATCH 21/93] modified code for updateDocument. however, it remains commented out for later repurpose --- editor/src/kroqed-editor/editor.ts | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/kroqed-editor/editor.ts index b2152986..3168e4f4 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/kroqed-editor/editor.ts @@ -151,50 +151,37 @@ export class WaterproofEditor { } updateDocument(content: string, version: number) { - // // Ensure we have an editor view to update. // if (!this._view) { - // //Editor view not initialized. Cannot update document // return; // } - // // If the new content is the same as the current document, do nothing. // const currentText = this._view.state.doc.textContent; // if (content === currentText) { // return; // } - // // Preprocess the content. checkPrePost may adjust the content and send messages. - // let newContent = checkPrePost(content, (msg: Message) => { - // this.post(msg); - // }); + // const {resultingDocument, documentChange} = checkPrePost(content); - // // If the preprocessing changed the content, update the version. - // if (newContent !== content) { + // if (resultingDocument !== content) { // version++; // this._translator = new FileTranslator(this._filef); - // // Translate the updated content into a ProseMirror document. - // const parsedContent = this._translator.toProsemirror(newContent); + // const parsedContent = this._translator.toProsemirror(resultingDocument); - // // Update the document mapping with the new content and version. // this._mapping = new TextDocMapping(this._filef, parsedContent, version); - // const proseDoc = createProseMirrorDocument(newContent, this._filef); + // const proseDoc = createProseMirrorDocument(resultingDocument, this._filef); - // // Create a new editor state with the new document. // const newState = EditorState.create({ // schema: this._schema, // doc: proseDoc, // plugins: this.createPluginsArray() // }); - // // Update the editor view's state in place. // this._view.updateState(newState); - // // Send an update for line numbers if they are enabled. // this.sendLineNumbers(); // } - } From 87dccb184b3130680301d3de1358ca99f16bb684 Mon Sep 17 00:00:00 2001 From: raulTUe Date: Wed, 30 Apr 2025 11:26:43 +0200 Subject: [PATCH 22/93] cleaning code up --- editor/src/index.ts | 2 +- editor/src/kroqed-editor/editor.ts | 32 +----------------------------- src/pm-editor/pmWebview.ts | 2 +- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/editor/src/index.ts b/editor/src/index.ts index 307dea81..ace0becc 100644 --- a/editor/src/index.ts +++ b/editor/src/index.ts @@ -75,7 +75,7 @@ window.onload = () => { } break; } case MessageType.updateDocument: - theEditor.updateDocument(msg.body.value, msg.body.version); + editor.updateDocument(msg.body.value, msg.body.version); break; case MessageType.setAutocomplete: // Handle autocompletion diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/kroqed-editor/editor.ts index 3168e4f4..7236c2fa 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/kroqed-editor/editor.ts @@ -151,37 +151,7 @@ export class WaterproofEditor { } updateDocument(content: string, version: number) { - // if (!this._view) { - // return; - // } - - // const currentText = this._view.state.doc.textContent; - // if (content === currentText) { - // return; - // } - - // const {resultingDocument, documentChange} = checkPrePost(content); - - // if (resultingDocument !== content) { - // version++; - // this._translator = new FileTranslator(this._filef); - - // const parsedContent = this._translator.toProsemirror(resultingDocument); - - // this._mapping = new TextDocMapping(this._filef, parsedContent, version); - - // const proseDoc = createProseMirrorDocument(resultingDocument, this._filef); - - // const newState = EditorState.create({ - // schema: this._schema, - // doc: proseDoc, - // plugins: this.createPluginsArray() - // }); - - // this._view.updateState(newState); - - // this.sendLineNumbers(); - // } + // TODO: Update the editor's state in place. } diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index 5440dd1d..c72b89a1 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -179,7 +179,7 @@ export class ProseMirrorWebview extends EventEmitter { this._disposables.push(workspace.onDidSaveTextDocument(e => { if (e.uri.toString() === this._document.uri.toString()) { - this.updateOnSave(); // Ensure the editor updates after saving + this.syncWebview(); } })); From e26c6b4af4ec0a5e0faa565c468a583c6bd33d98 Mon Sep 17 00:00:00 2001 From: raulTUe Date: Wed, 30 Apr 2025 11:43:36 +0200 Subject: [PATCH 23/93] final changes --- editor/src/kroqed-editor/editor.ts | 38 +++++++++++++++++++++++++++++- src/pm-editor/pmWebview.ts | 2 +- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/kroqed-editor/editor.ts index 7236c2fa..8be1d105 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/kroqed-editor/editor.ts @@ -151,7 +151,43 @@ export class WaterproofEditor { } updateDocument(content: string, version: number) { - // TODO: Update the editor's state in place. + if (!this._view) { + return; + } + + const currentText = this._view.state.doc.textContent; + if (content === currentText) { + return; + } + + if (this._mapping && this._mapping.version === version) return; + + this._view.dispatch(this._view.state.tr.setMeta(MENU_PLUGIN_KEY, "remove")); + document.querySelector(".menubar")?.remove(); + document.querySelector(".progress-bar")?.remove(); + document.querySelector(".spinner-container")?.remove(); + this._view.dom.remove(); + + if (this._filef === FileFormat.MarkdownV) { + document.body.classList.add("mv"); + } + + this._translator = new FileTranslator(this._filef); + + const { resultingDocument, documentChange } = checkPrePost(content); + if (documentChange !== undefined) { + this._editorConfig.api.documentChange(documentChange); + } + if (resultingDocument !== content) version = version + 1; + + const parsedContent = this._translator.toProsemirror(resultingDocument); + const proseDoc = createProseMirrorDocument(resultingDocument, this._filef); + + this._mapping = new TextDocMapping(this._filef, parsedContent, version); + this.createProseMirrorEditor(proseDoc); + + this.sendLineNumbers(); + this._editorConfig.api.editorReady(); } diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index c72b89a1..2b514d31 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -179,7 +179,7 @@ export class ProseMirrorWebview extends EventEmitter { this._disposables.push(workspace.onDidSaveTextDocument(e => { if (e.uri.toString() === this._document.uri.toString()) { - this.syncWebview(); + this.updateOnSave(); } })); From db44d47fc18b9eb1cd0713cd168457bc9374441d Mon Sep 17 00:00:00 2001 From: "@20210857" Date: Wed, 7 May 2025 09:59:13 +0000 Subject: [PATCH 24/93] temporary changes --- src/extension.ts | 7 +++++-- src/webviewManager.ts | 25 +++++++++++++++++++++---- src/webviews/sidePanel.ts | 16 +++++++++------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 359d6d22..148c5aaf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -118,12 +118,15 @@ export class Waterproof implements Disposable { // update active document // only unset cursor when focussing different document (otherwise cursor position is often lost and user has to double click) if (this.client.activeDocument?.uri.toString() !== document.uri.toString()) { + console.log("Updating active document"); this.client.activeDocument = document; this.client.activeCursorPosition = undefined; + this.webviewManager.open("goals"); for (const g of this.goalsComponents) g.updateGoals(undefined); + } - this.webviewManager.open("goals") - // this.webviewManager.reveal("goals") + //this.webviewManager.open("goals"); + else {this.webviewManager.reveal("goals");} }); this.webviewManager.on(WebviewManagerEvents.cursorChange, (document: TextDocument, position: Position) => { // update active document and cursor diff --git a/src/webviewManager.ts b/src/webviewManager.ts index c8373b9c..5349da9c 100644 --- a/src/webviewManager.ts +++ b/src/webviewManager.ts @@ -149,10 +149,27 @@ export class WebviewManager extends EventEmitter { * @param id the id of the tool webview */ public open(id: string) { - if (this._toolWebviews.has(id)) new Error("Tool webview does not have this panel: " + id); - this._toolWebviews.get(id)?.readyPanel(); - this._toolWebviews.get(id)?.activatePanel(); + if (!this._toolWebviews.has(id)) { + throw new Error("Tool webview does not have this panel: " + id); + } + + const panel = this._toolWebviews.get(id); + + // Check if the panel is already open + if (panel?.isOpened) { + console.log(`Panel with id "${id}" is already open.`); + return; // Do nothing if the panel is already open + } + + // Open the panel if it is not already open + else{ + panel?.readyPanel(); + panel?.activatePanel();} } + //if (this._toolWebviews.has(id)) new Error("Tool webview does not have this panel: " + id); + //this._toolWebviews.get(id)?.readyPanel(); + //this._toolWebviews.get(id)?.activatePanel(); + /** * Reveals a panel to the user @@ -160,8 +177,8 @@ export class WebviewManager extends EventEmitter { */ public reveal(id: string) { if (this._toolWebviews.has(id)) new Error("Tool webview does not have this panel: " + id); - console.log(this._toolWebviews) this._toolWebviews.get(id)?.revealPanel() + console.log('revealing panel',id) } /** diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index 0654e4bf..4248066a 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -29,20 +29,19 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { this.updateButtonTransparency(e.name, e.open); }); } - public greyedOutButtons(buttonId: string, open: boolean): string[] { + public updateGreyedOutButton(buttonId: string, open: boolean) { if (this._greyedOutButtons.has(buttonId) && !open) { this._greyedOutButtons.delete(buttonId); } else if (open){ this._greyedOutButtons.add(buttonId); } - - return Array.from(this._greyedOutButtons); } + public updateButtonTransparency(buttonId: string, open: boolean) { // Set the transparency state of the button in the transparency map this._transparencyMap.set(buttonId, open); //update the list of buttons that are currently greyed out - const greyedOut = this.greyedOutButtons(buttonId,open); + this.updateGreyedOutButton(buttonId,open); // Update the button styles to reflect the transparency state this.updateButtonStyles(buttonId, open); } @@ -82,7 +81,8 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { command: 'executeScript', script: message.script, }); - } else { + } + else { // Handle other messages by opening the command in the manager this._manager.open(message.command); } @@ -93,8 +93,9 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { webviewView.webview.postMessage({ type: 'restoreTransparency', - greyedOutButtonsList: this.greyedOutButtons("goals",true) //the goals panel is always opened + greyedOutButtonsList: Array.from(this._greyedOutButtons) }); + console.log(this._greyedOutButtons) } }); @@ -238,7 +239,8 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { if (message.greyedOutButtonsList?.includes(id)) { // If it is, make transparent btn.classList.add('transparent'); - } else { + } + else { // If it is not, remove the transparent class btn.classList.remove('transparent'); } From 2415b7e485f29aacb961a39c025c03c8518de664 Mon Sep 17 00:00:00 2001 From: "@20210857" Date: Mon, 19 May 2025 14:03:58 +0000 Subject: [PATCH 25/93] fixing multiple goals panel issue --- src/extension.ts | 3 +-- src/webviewManager.ts | 8 +------- src/webviews/coqWebview.ts | 3 ++- src/webviews/goalviews/goalsPanel.ts | 1 - src/webviews/sidePanel.ts | 1 - 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 148c5aaf..dfabeb63 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -118,7 +118,6 @@ export class Waterproof implements Disposable { // update active document // only unset cursor when focussing different document (otherwise cursor position is often lost and user has to double click) if (this.client.activeDocument?.uri.toString() !== document.uri.toString()) { - console.log("Updating active document"); this.client.activeDocument = document; this.client.activeCursorPosition = undefined; this.webviewManager.open("goals"); @@ -126,7 +125,7 @@ export class Waterproof implements Disposable { } //this.webviewManager.open("goals"); - else {this.webviewManager.reveal("goals");} + // else {this.webviewManager.reveal("goals");} }); this.webviewManager.on(WebviewManagerEvents.cursorChange, (document: TextDocument, position: Position) => { // update active document and cursor diff --git a/src/webviewManager.ts b/src/webviewManager.ts index 5349da9c..2e04b13c 100644 --- a/src/webviewManager.ts +++ b/src/webviewManager.ts @@ -121,7 +121,6 @@ export class WebviewManager extends EventEmitter { * @param webview object associated with document */ public addProseMirrorWebview(webview: ProseMirrorWebview) { - console.log("Adding ProseMirror webview", webview.document); if (this.has(webview.document)) { throw new Error(" Webview already registered! THIS SHOULD NOT HAPPEN! "); } @@ -157,8 +156,7 @@ export class WebviewManager extends EventEmitter { // Check if the panel is already open if (panel?.isOpened) { - console.log(`Panel with id "${id}" is already open.`); - return; // Do nothing if the panel is already open + return; } // Open the panel if it is not already open @@ -166,9 +164,6 @@ export class WebviewManager extends EventEmitter { panel?.readyPanel(); panel?.activatePanel();} } - //if (this._toolWebviews.has(id)) new Error("Tool webview does not have this panel: " + id); - //this._toolWebviews.get(id)?.readyPanel(); - //this._toolWebviews.get(id)?.activatePanel(); /** @@ -178,7 +173,6 @@ export class WebviewManager extends EventEmitter { public reveal(id: string) { if (this._toolWebviews.has(id)) new Error("Tool webview does not have this panel: " + id); this._toolWebviews.get(id)?.revealPanel() - console.log('revealing panel',id) } /** diff --git a/src/webviews/coqWebview.ts b/src/webviews/coqWebview.ts index e4515b8a..5b79a20c 100644 --- a/src/webviews/coqWebview.ts +++ b/src/webviews/coqWebview.ts @@ -101,7 +101,7 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { this.name, this.name.charAt(0).toUpperCase() + this.name.slice(1), { preserveFocus: true, viewColumn: ViewColumn.Two }, - webviewOpts + webviewOpts, ); } @@ -160,6 +160,7 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { const prev = this._state; this._state = next; this.emit(WebviewEvents.change, prev); + } /** diff --git a/src/webviews/goalviews/goalsPanel.ts b/src/webviews/goalviews/goalsPanel.ts index 0f2624a1..fea0fc9a 100644 --- a/src/webviews/goalviews/goalsPanel.ts +++ b/src/webviews/goalviews/goalsPanel.ts @@ -22,7 +22,6 @@ export class GoalsPanel extends GoalsBase { //override updateGoals to save the previous goals and activating the panel before posting the goals message override updateGoals(goals: GoalAnswer | undefined) { this.previousGoal = goals; - this.activatePanel(); super.updateGoals(goals); } diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index 4248066a..9d082583 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -95,7 +95,6 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { type: 'restoreTransparency', greyedOutButtonsList: Array.from(this._greyedOutButtons) }); - console.log(this._greyedOutButtons) } }); From f6d349e87b32d68bcf83dfa04d835654745e56ef Mon Sep 17 00:00:00 2001 From: "@20210857" Date: Mon, 19 May 2025 14:32:25 +0000 Subject: [PATCH 26/93] fixing multiple goals panel issue --- src/extension.ts | 3 +-- src/webviewManager.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index dfabeb63..740e354c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -124,8 +124,7 @@ export class Waterproof implements Disposable { for (const g of this.goalsComponents) g.updateGoals(undefined); } - //this.webviewManager.open("goals"); - // else {this.webviewManager.reveal("goals");} + }); this.webviewManager.on(WebviewManagerEvents.cursorChange, (document: TextDocument, position: Position) => { // update active document and cursor diff --git a/src/webviewManager.ts b/src/webviewManager.ts index 2e04b13c..896fbdef 100644 --- a/src/webviewManager.ts +++ b/src/webviewManager.ts @@ -121,6 +121,7 @@ export class WebviewManager extends EventEmitter { * @param webview object associated with document */ public addProseMirrorWebview(webview: ProseMirrorWebview) { + console.log("Adding ProseMirror webview", webview.document); if (this.has(webview.document)) { throw new Error(" Webview already registered! THIS SHOULD NOT HAPPEN! "); } From d8426f4722c2ab5b0af5abe16a45da4af85feb1b Mon Sep 17 00:00:00 2001 From: raulTUe Date: Mon, 16 Jun 2025 12:33:12 +0200 Subject: [PATCH 27/93] added documentation and first implementation of updating cursor --- developer-resources/messages.md | 13 +++++++++++++ editor/src/index.ts | 4 ++-- editor/src/kroqed-editor/editor.ts | 7 ++++++- shared/Messages.ts | 4 ++-- src/pm-editor/pmWebview.ts | 6 +++--- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/developer-resources/messages.md b/developer-resources/messages.md index 4e612e11..7ba24e70 100644 --- a/developer-resources/messages.md +++ b/developer-resources/messages.md @@ -111,6 +111,19 @@ Send by the extension to start initialization of the webview editor, includes th } ``` +### `refreshDocument` + +#### Description +Send by the extension to refresh the webview editor, includes the updated content of the document in the body, as well as the documnet's version. + +#### Body +```ts +{ + value: string, // Updated content of the document + version: number // Current version of the document +} +``` + ### `insert` #### Description Send by the extension to inform the editor that a symbol should be inserted. diff --git a/editor/src/index.ts b/editor/src/index.ts index ace0becc..e826b559 100644 --- a/editor/src/index.ts +++ b/editor/src/index.ts @@ -74,8 +74,8 @@ window.onload = () => { editor.insertSymbol(symbolUnicode, symbolLatex); } break; } - case MessageType.updateDocument: - editor.updateDocument(msg.body.value, msg.body.version); + case MessageType.refreshDocument: + editor.refreshDocument(msg.body.value, msg.body.version); break; case MessageType.setAutocomplete: // Handle autocompletion diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/kroqed-editor/editor.ts index 8be1d105..afb9ebb5 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/kroqed-editor/editor.ts @@ -150,7 +150,7 @@ export class WaterproofEditor { this._editorConfig.api.editorReady(); } - updateDocument(content: string, version: number) { + refreshDocument(content: string, version: number) { if (!this._view) { return; } @@ -162,6 +162,9 @@ export class WaterproofEditor { if (this._mapping && this._mapping.version === version) return; + //TODO: save cursor position and restore it after the refresh + const cursorPos = this._view.state.selection; + this._view.dispatch(this._view.state.tr.setMeta(MENU_PLUGIN_KEY, "remove")); document.querySelector(".menubar")?.remove(); document.querySelector(".progress-bar")?.remove(); @@ -186,6 +189,8 @@ export class WaterproofEditor { this._mapping = new TextDocMapping(this._filef, parsedContent, version); this.createProseMirrorEditor(proseDoc); + this.updateCursor(cursorPos); + this.sendLineNumbers(); this._editorConfig.api.editorReady(); } diff --git a/shared/Messages.ts b/shared/Messages.ts index 0e4da8e1..66c2af99 100644 --- a/shared/Messages.ts +++ b/shared/Messages.ts @@ -34,7 +34,7 @@ export type Message = | MessageBase | MessageBase | MessageBase - | MessageBase + | MessageBase | MessageBase | MessageBase | MessageBase @@ -62,7 +62,7 @@ export const enum MessageType { editorReady, errorGoals, init, - updateDocument, + refreshDocument, insert, lineNumbers, progress, diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index 2b514d31..0dec771e 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -179,7 +179,7 @@ export class ProseMirrorWebview extends EventEmitter { this._disposables.push(workspace.onDidSaveTextDocument(e => { if (e.uri.toString() === this._document.uri.toString()) { - this.updateOnSave(); + this.refreshOnSave(); } })); @@ -272,9 +272,9 @@ export class ProseMirrorWebview extends EventEmitter { for (const m of this._cachedMessages.values()) this.postMessage(m); } - private updateOnSave() { + private refreshOnSave() { this.postMessage({ - type: MessageType.updateDocument, + type: MessageType.refreshDocument, body: { value: this._document.getText(), version: this._document.version, From b2482c1c96ff5795f1a6043253a4b1427bbe018d Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 24 Jun 2025 17:41:54 +0200 Subject: [PATCH 28/93] Completion fixes --- shared/completions/tactics.json | 224 ++++++++++++++++---------------- 1 file changed, 112 insertions(+), 112 deletions(-) diff --git a/shared/completions/tactics.json b/shared/completions/tactics.json index d00862b2..b2c9c58b 100644 --- a/shared/completions/tactics.json +++ b/shared/completions/tactics.json @@ -5,103 +5,103 @@ "detail": "tactic", "template": "Help.", "description": "Tries to give you a hint on what to do next.", - "example": "Lemma example_help :\n 0 = 0.\nProof.\nHelp.\nWe conclude that (0 = 0).\nQed.", + "example": "Lemma example_help :\n 0 = 0.\nProof.\nHelp.\nWe conclude that 0 = 0.\nQed.", "boost": 2 }, { - "label": "Take (*name*) : ((*type*)).", + "label": "Take (*name*) : (*type*).", "type": "type", "detail": "tactic", - "template": "Take ${x} : (${ℝ}).", + "template": "Take ${x} : ${ℝ}.", "description": "Take an arbitrary element from (*type*) and call it (*name*).", - "example": "Lemma example_take :\n for all x : ℝ,\n x = x.\nProof.\nTake x : (ℝ).\nWe conclude that (x = x).\nQed.", + "example": "Lemma example_take :\n for all x : ℝ,\n x = x.\nProof.\nTake x : ℝ.\nWe conclude that x = x.\nQed.", "boost": 1 }, { - "label": "Take (*name*) ∈ ((*set*)).", + "label": "Take (*name*) ∈ (*set*).", "type": "type", "detail": "tactic", - "template": "Take ${x} ∈ (${ℝ}).", + "template": "Take ${x} ∈ ${ℝ}.", "description": "Take an arbitrary element from (*set*) and call it (*name*).", - "example": "Lemma example_take :\n ∀ x ∈ ℝ,\n x = x.\nProof.\nTake x ∈ (ℝ).\nWe conclude that (x = x).\nQed.", + "example": "Lemma example_take :\n ∀ x ∈ ℝ,\n x = x.\nProof.\nTake x ∈ ℝ.\nWe conclude that x = x.\nQed.", "boost": 2 }, { - "label": "Take (*name*) > ((*number*)).", + "label": "Take (*name*) > (*number*).", "type": "type", "detail": "tactic", - "template": "Take ${0:x} > (${1:0}).", + "template": "Take ${0:x} > ${1:0}.", "description": "Take an arbitrary element larger than (*number*) and call it (*name*).", - "example": "Lemma example_take :\n ∀ x > 3,\n x = x.\nProof.\nTake x > (3).\nWe conclude that (x = x).\nQed.", + "example": "Lemma example_take :\n ∀ x > 3,\n x = x.\nProof.\nTake x > 3.\nWe conclude that x = x.\nQed.", "boost": 2 }, { - "label": "Take (*name*) ≥ ((*number*)).", + "label": "Take (*name*) ≥ (*number*).", "type": "type", "detail": "tactic", - "template": "Take ${0:x} ≥ (${1:0}).", + "template": "Take ${0:x} ≥ ${1:0}.", "description": "Take an arbitrary element larger than or equal to (*number*) and call it (*name*).", - "example": "Lemma example_take :\n ∀ x ≥ 5,\n x = x.\nProof.\nTake x ≥ (5).\nWe conclude that (x = x).\nQed.", + "example": "Lemma example_take :\n ∀ x ≥ 5,\n x = x.\nProof.\nTake x ≥ 5.\nWe conclude that x = x.\nQed.", "boost": 2 }, { - "label": "We need to show that ((*(alternative) formulation of current goal*)).", + "label": "We need to show that (*(alternative) formulation of current goal*).", "type": "type", "detail": "tactic", - "template": "We need to show that (${0 = 0}).", + "template": "We need to show that ${0 = 0}.", "description": "Generally makes a proof more readable. Has the additional functionality that you can write a slightly different but equivalent formulation of the goal: you can for instance change names of certain variables.", - "example": "Lemma example_we_need_to_show_that :\n 0 = 0.\nProof.\nWe need to show that (0 = 0).\nWe conclude that (0 = 0).\nQed.", + "example": "Lemma example_we_need_to_show_that :\n 0 = 0.\nProof.\nWe need to show that 0 = 0.\nWe conclude that 0 = 0.\nQed.", "boost": 2 }, { - "label": "We conclude that ((*current goal*)).", + "label": "We conclude that (*current goal*).", "type": "type", "detail": "tactic", - "template": "We conclude that (${0 = 0}).", + "template": "We conclude that ${0 = 0}.", "description": "Tries to automatically prove the current goal.", - "example": "Lemma example_we_conclude_that :\n 0 = 0.\nProof.\nWe conclude that (0 = 0).\nQed." + "example": "Lemma example_we_conclude_that :\n 0 = 0.\nProof.\nWe conclude that 0 = 0.\nQed." }, { - "label": "We conclude that ((*(alternative) formulation of current goal*)).", + "label": "We conclude that (*(alternative) formulation of current goal*).", "type": "type", "detail": "tactic", - "template": "We conclude that (${0 = 0}).", + "template": "We conclude that ${0 = 0}.", "description": "Tries to automatically prove the current goal.", - "example": "Lemma example_we_conclude_that :\n 0 = 0.\nProof.\nWe conclude that (0 = 0).\nQed.", + "example": "Lemma example_we_conclude_that :\n 0 = 0.\nProof.\nWe conclude that 0 = 0.\nQed.", "boost": 1 }, { - "label": "Choose (*name_var*) := ((*expression*)).", + "label": "Choose (*name_var*) := (*expression*).", "type": "type", "detail": "tactic", - "template": "Choose ${0:x} := (${1:0}).", + "template": "Choose ${0:x} := ${1:0}.", "description": "You can use this tactic when you need to show that there exists an x such that a certain property holds. You do this by proposing (*expression*) as a choice for x, giving it the name (*name_var*).", "boost": 2, - "example": "Lemma example_choose :\n ∃ y ∈ ℝ,\n y < 3.\nProof.\nChoose y := (2).\n* Indeed, (y ∈ ℝ).\n* We conclude that (y < 3).\nQed." + "example": "Lemma example_choose :\n ∃ y ∈ ℝ,\n y < 3.\nProof.\nChoose y := 2.\n* Indeed, y ∈ ℝ.\n* We conclude that y < 3.\nQed." }, { - "label": "Assume that ((*statement*)).", + "label": "Assume that (*statement*).", "type": "type", "detail": "tactic", - "template": "Assume that (${0 = 0}).", + "template": "Assume that ${0 = 0}.", "description": "If you need to prove (*statement*) ⇒ B, this allows you to assume that (*statement*) holds.", "boost": 2, - "example": "Lemma example_assume :\n ∀ a ∈ ℝ, a < 0 ⇒ - a > 0.\nProof.\nTake a ∈ (ℝ).\nAssume that (a < 0).\nWe conclude that (- a > 0).\nQed." + "example": "Lemma example_assume :\n ∀ a ∈ ℝ, a < 0 ⇒ - a > 0.\nProof.\nTake a ∈ ℝ.\nAssume that a < 0.\nWe conclude that - a > 0.\nQed." }, { - "label": "Assume that ((*statement*)) ((*label*)).", + "label": "Assume that (*statement*) as ((*label*)).", "type": "type", "detail": "tactic", - "template": "Assume that (${0 = 0}) (${i}).", + "template": "Assume that ${0 = 0} as (${i}).", "description": "If you need to prove (*statement*) ⇒ B, this allows you to assume that (*statement*) holds, giving it the label (*label*). You can leave out ((*label*)) if you don't wish to name your assumption.", "boost": 1, - "example": "Lemma example_assume :\n ∀ a ∈ ℝ, a < 0 ⇒ - a > 0.\nProof.\nTake a ∈ (ℝ).\nAssume that (a < 0) (a_less_than_0).\nWe conclude that (- a > 0).\nQed." + "example": "Lemma example_assume :\n ∀ a ∈ ℝ, a < 0 ⇒ - a > 0.\nProof.\nTake a ∈ ℝ.\nAssume that a < 0 as (a_less_than_0).\nWe conclude that - a > 0.\nQed." }, { - "label": "(& 3 < 5 = 2 + 3 ≤ 7) (chain of (in)equalities, with opening parenthesis)", + "label": "& 3 < 5 = 2 + 3 ≤ 7 (chain of (in)equalities, with opening parenthesis)", "type": "type", "detail": "tactic", - "template": "(& ${3 < 5 = 2 + 3 ≤ 7}", + "template": "& ${3 < 5 = 2 + 3 ≤ 7}", "description": "Example of a chain of (in)equalities in which every inequality should.", "example": "Lemma example_inequalities :\n ∀ ε > 0, Rmin(ε,1) < 2.\nProof.\nTake ε > 0.\nWe conclude that (& Rmin(ε,1) ≤ 1 < 2).\nQed." }, @@ -111,7 +111,7 @@ "detail": "tactic", "template": "& ${3 < 5 = 2 + 3 ≤ 7}", "description": "Example of a chain of (in)equalities in which every inequality should.", - "example": "Lemma example_inequalities :\n ∀ ε > 0, Rmin(ε,1) < 2.\nProof.\nTake ε : (ℝ).\nAssume that (ε > 0).\nWe conclude that (& Rmin(ε,1) ≤ 1 < 2).\nQed." + "example": "Lemma example_inequalities :\n ∀ ε > 0, Rmin(ε,1) < 2.\nProof.\nTake ε : ℝ.\nAssume that ε > 0.\nWe conclude that & Rmin(ε,1) ≤ 1 < 2.\nQed." }, { "label": "Obtain such a (*name_var*)", @@ -120,7 +120,7 @@ "template": "Obtain such a ${k}.", "description": "In case a hypothesis that you just proved starts with 'there exists' s.t. some property holds, then you can use this tactic to select such a variable. The variable will be named (*name_var*).", "boost": 2, - "example": "Lemma example_obtain :\n ∀ x ∈ ℝ,\n (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒\n 10 < x.\nProof.\nTake x ∈ (ℝ).\nAssume that (∃ y ∈ ℝ, 10 < y ∧ y < x) (i).\nObtain such a y.\nQed." + "example": "Lemma example_obtain :\n ∀ x ∈ ℝ,\n (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒\n 10 < x.\nProof.\nTake x ∈ ℝ.\nAssume that ∃ y ∈ ℝ, 10 < y ∧ y < x as (i).\nObtain such a y.\nQed." }, { "label": "Obtain (*name_var*) according to ((*name_hyp*)).", @@ -129,80 +129,80 @@ "template": "Obtain ${k} according to (${i}).", "description": "In case the hypothesis with name (*name_hyp*) starts with 'there exists' s.t. some property holds, then you can use this tactic to select such a variable. The variable will be named (*name_var*).", "boost": 2, - "example": "Lemma example_obtain :\n ∀ x ∈ ℝ,\n (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒\n 10 < x.\nProof.\nTake x ∈ (ℝ).\nAssume that (∃ y ∈ ℝ, 10 < y ∧ y < x) (i).\nObtain y according to (i).\nQed." + "example": "Lemma example_obtain :\n ∀ x ∈ ℝ,\n (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒\n 10 < x.\nProof.\nTake x ∈ ℝ.\nAssume that ∃ y ∈ ℝ, 10 < y ∧ y < x as (i).\nObtain y according to (i).\nQed." }, { - "label": "It suffices to show that ((*statement*)).", + "label": "It suffices to show that (*statement*).", "type": "type", "detail": "tactic", - "template": "It suffices to show that (${0 = 0}).", + "template": "It suffices to show that ${0 = 0}.", "description": "Waterproof tries to verify automatically whether it is indeed enough to show (*statement*) to prove the current goal. If so, (*statement*) becomes the new goal.", "boost": 2, - "example": "Lemma example_it_suffices_to_show_that :\n ∀ ε > 0,\n 3 + Rmax(ε,2) ≥ 3.\nProof.\nTake ε > 0.\nIt suffices to show that (Rmax(ε,2) ≥ 0).\nWe conclude that (& Rmax(ε,2) ≥ 2 ≥ 0).\nQed.", + "example": "Lemma example_it_suffices_to_show_that :\n ∀ ε > 0,\n 3 + Rmax(ε,2) ≥ 3.\nProof.\nTake ε > 0.\nIt suffices to show that Rmax(ε,2) ≥ 0.\nWe conclude that & Rmax(ε,2) ≥ 2 ≥ 0.\nQed.", "advanced": false }, { - "label": "By ((*lemma or assumption*)) it suffices to show that ((*statement*)).", + "label": "By ((*lemma or assumption*)) it suffices to show that (*statement*).", "type": "type", "detail": "tactic", "description": "Waterproof tries to verify automatically whether it is indeed enough to show (*statement*) to prove the current goal, using (*lemma or assumption*). If so, (*statement*) becomes the new goal.", - "template": "By (${i}) it suffices to show that (${0 = 0}).", + "template": "By (${i}) it suffices to show that ${0 = 0}.", "boost": 2, - "example": "Lemma example_it_suffices_to_show_that :\n ∀ ε ∈ ℝ,\n ε > 0 ⇒\n 3 + Rmax(ε,2) ≥ 3.\nProof.\nTake ε ∈ (ℝ).\nAssume that (ε > 0) (i).\nBy (i) it suffices to show that (Rmax(ε,2) ≥ 0).\nWe conclude that (& Rmax(ε,2) ≥ 2 ≥ 0).\nQed.", + "example": "Lemma example_it_suffices_to_show_that :\n ∀ ε ∈ ℝ,\n ε > 0 ⇒\n 3 + Rmax(ε,2) ≥ 3.\nProof.\nTake ε ∈ ℝ.\nAssume that ε > 0 as (i).\nBy (i) it suffices to show that Rmax(ε,2) ≥ 0.\nWe conclude that & Rmax(ε,2) ≥ 2 ≥ 0.\nQed.", "advanced": false }, { - "label": "It holds that ((*statement*)) ((*label*)).", + "label": "It holds that *statement* as ((*label*)).", "type": "type", "detail": "tactic", - "template": "It holds that (${0 = 0}) (${i}).", + "template": "It holds that ${0 = 0} as (${i}).", "description": "Tries to automatically prove (*statement*). If that works, (*statement*) is added as a hypothesis with name (*optional_label*).", - "example": "Lemma example_it_holds_that :\n ∀ ε > 0,\n 4 - Rmax(ε,1) ≤ 3.\n \nProof.\nTake ε > 0.\nIt holds that (Rmax(ε,1) ≥ 1) (i).\nWe conclude that (4 - Rmax(ε,1) ≤ 3).\nQed.", + "example": "Lemma example_it_holds_that :\n ∀ ε > 0,\n 4 - Rmax(ε,1) ≤ 3.\n \nProof.\nTake ε > 0.\nIt holds that Rmax(ε,1) ≥ 1 as (i).\nWe conclude that 4 - Rmax(ε,1) ≤ 3.\nQed.", "boost": 1 }, { - "label": "It holds that ((*statement*)).", + "label": "It holds that (*statement*).", "type": "type", "detail": "tactic", - "template": "It holds that (${0 = 0}).", + "template": "It holds tht (${0 = 0}).", "description": "Tries to automatically prove (*statement*). If that works, (*statement*) is added as a hypothesis.", - "example": "Lemma example_it_holds_that :\n ∀ ε > 0,\n 4 - Rmax(ε,1) ≤ 3.\n \nProof.\nTake ε > 0.\nIt holds that (Rmax(ε,1) ≥ 1).\nWe conclude that (4 - Rmax(ε,1) ≤ 3).\nQed.", + "example": "Lemma example_it_holds_that :\n ∀ ε > 0,\n 4 - Rmax(ε,1) ≤ 3.\n \nProof.\nTake ε > 0.\nIt holds that Rmax(ε,1) ≥ 1.\nWe conclude that 4 - Rmax(ε,1) ≤ 3.\nQed.", "boost": 2 }, { - "label": "By ((*lemma or assumption*)) it holds that ((*statement*)) ((*label*)).", + "label": "By ((*lemma or assumption*)) it holds that (*statement*) as ((*label*)).", "type": "type", "detail": "tactic", - "template": "By (${i}) it holds that (${0 = 0}) (${ii}).", + "template": "By (${i}) it holds that ${0 = 0} (${ii}).", "description": "Tries to prove (*statement*) using (*lemma*) or (*assumption*). If that works, (*statement*) is added as a hypothesis with name (*optional_label*). You can leave out ((*optional_label*)) if you don't wish to name the statement.", - "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that (7 < f(-1)).\nBy (f_increasing) it holds that (f(-1) ≤ f(6)) (i).\nWe conclude that (2 < f(6)).\nQed.", + "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that 7 < f(-1).\nBy f_increasing it holds that f(-1) ≤ f(6) (i).\nWe conclude that 2 < f(6).\nQed.", "boost": 1 }, { - "label": "By ((*lemma or assumption*)) it holds that ((*statement*)).", + "label": "By ((*lemma or assumption*)) it holds that (*statement*).", "type": "type", "detail": "tactic", - "template": "By (${i}) it holds that (${0 = 0}).", + "template": "By (${i}) it holds that ${0 = 0}.", "description": "Tries to prove (*statement*) using (*lemma*) or (*assumption*). If that works, (*statement*) is added as a hypothesis with name (*optional_label*). You can leave out ((*optional_label*)) if you don't wish to name the statement.", - "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that (7 < f(-1)).\nBy (f_increasing) it holds that (f(-1) ≤ f(6)) (i).\nWe conclude that (2 < f(6)).\nQed.", + "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that 7 < f(-1).\nBy f_increasing it holds that f(-1) ≤ f(6) as (i).\nWe conclude that 2 < f(6).\nQed.", "boost": 2 }, { - "label": "We claim that ((*statement*)).", + "label": "We claim that (*statement*).", "type": "type", "detail": "tactic", - "template": "We claim that (${0 = 0}).", + "template": "We claim that ${0 = 0}.", "description": "Lets you first show (*statement*) before continuing with the rest of the proof. After you showed (*statement*), it will be available as a hypothesis with name (*optional_name*).", - "example": "We claim that (2 = 2) (two_is_two).", + "example": "We claim that 2 = 2 as (two_is_two).", "boost": 2 }, { - "label": "We claim that ((*statement*)) ((*label*)).", + "label": "We claim that (*statement*) ((*label*)).", "type": "type", "detail": "tactic", - "template": "We claim that (${0 = 0}) (${i}).", + "template": "We claim that ${0 = 0} as (${i}).", "description": "Lets you first show (*statement*) before continuing with the rest of the proof. After you showed (*statement*), it will be available as a hypothesis with name (*label*).", - "example": "We claim that (2 = 2) (two_is_two).", + "example": "We claim that 2 = 2 as (two_is_two).", "boost": 1 }, { @@ -211,7 +211,7 @@ "detail": "tactic", "template": "We argue by contradiction.", "description": "Assumes the opposite of what you need to show. Afterwards, you need to make substeps that show that both A and ¬ A (i.e. not A) for some statement A. Finally, you can finish your proof with 'Contradiction.'", - "example": "Lemma example_contradicition :\n ∀ x ∈ ℝ,\n (∀ ε > 0, x > 1 - ε) ⇒\n x ≥ 1.\nProof.\nTake x ∈ (ℝ).\nAssume that (∀ ε > 0, x > 1 - ε) (i).\nWe need to show that (x ≥ 1).\nWe argue by contradiction.\nAssume that (¬ (x ≥ 1)).\nIt holds that ((1 - x) > 0).\nBy (i) it holds that (x > 1 - (1 - x)).\nContradiction.\nQed.", + "example": "Lemma example_contradicition :\n ∀ x ∈ ℝ,\n (∀ ε > 0, x > 1 - ε) ⇒\n x ≥ 1.\nProof.\nTake x ∈ ℝ.\nAssume that ∀ ε > 0, x > 1 - ε as (i).\nWe need to show that x ≥ 1.\nWe argue by contradiction.\nAssume that ¬ (x ≥ 1).\nIt holds that (1 - x) > 0.\nBy (i) it holds that x > 1 - (1 - x).\nContradiction.\nQed.", "boost": 2 }, { @@ -224,32 +224,32 @@ "boost": 2 }, { - "label": "Because ((*name_combined_hyp*)) both ((*statement_1*)) and ((*statement_2*)).", + "label": "Because ((*name_combined_hyp*)) both (*statement_1*) and (*statement_2*).", "type": "type", "detail": "tactic", - "template": "Because (${i}) both (${0 = 0}) and (${1 = 1}).", + "template": "Because (${i}) both ${0 = 0} and ${1 = 1}.", "description": "If you currently have a hypothesis with name (*name_combined_hyp*) which is in fact of the form H1 ∧ H2, then this tactic splits it up in two separate hypotheses.", - "example": "Lemma and_example : for all A B : Prop, A ∧ B ⇒ A.\nTake A : Prop. Take B : Prop.\nAssume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii).", + "example": "Lemma and_example : for all A B : Prop, A ∧ B ⇒ A.\nTake A : Prop. Take B : Prop.\nAssume that A ∧ B as (i). Because (i) both A as (ii) and B as (iii).", "advanced": true, "boost": 1 }, { - "label": "Because ((*name_combined_hyp*)) both ((*statement_1*)) ((*label_1*)) and ((*statement_2*)) ((*label_2*)).", + "label": "Because ((*name_combined_hyp*)) both (*statement_1*) as ((*label_1*)) and (*statement_2*) as ((*label_2*)).", "type": "type", "detail": "tactic", - "template": "Because (${i}) both (${0 = 0}) (${ii}) and (${1 = 1}) (${iii}).", + "template": "Because (${i}) both ${0 = 0} as (${ii}) and ${1 = 1} as (${iii}).", "description": "If you currently have a hypothesis with name (*name_combined_hyp*) which is in fact of the form H1 ∧ H2, then this tactic splits it up in two separate hypotheses.", - "example": "Lemma and_example : for all A B : Prop, A ∧ B ⇒ A.\nTake A : Prop. Take B : Prop.\nAssume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii).", + "example": "Lemma and_example : for all A B : Prop, A ∧ B ⇒ A.\nTake A : Prop. Take B : Prop.\nAssume that A ∧ B as (i). Because (i) both A as (ii) and B as (iii).", "advanced": true, "boost": 1 }, { - "label": "Either ((*case_1*)) or ((*case_2*)).", + "label": "Either (*case_1*) or (*case_2*).", "type": "type", "detail": "tactic", - "template": "Either (${0 = 1}) or (${0 ≠ 1}).", + "template": "Either ${0 = 1} or ${0 ≠ 1}.", "description": "Split in two cases (*case_1*) and (*case_2*).", - "example": "Lemma example_cases : \n ∀ x ∈ ℝ, ∀ y ∈ ℝ,\n Rmax(x,y) = x ∨ Rmax(x,y) = y.\nProof. \nTake x ∈ ℝ. Take y ∈ ℝ.\nEither (x < y) or (x ≥ y).\n- Case (x < y).\n It suffices to show that (Rmax(x,y) = y).\n We conclude that (Rmax(x,y) = y).\n- Case (x ≥ y).\n It suffices to show that (Rmax(x,y) = x).\n We conclude that (Rmax(x,y) = x).\nQed.", + "example": "Lemma example_cases : \n ∀ x ∈ ℝ, ∀ y ∈ ℝ,\n Rmax(x,y) = x ∨ Rmax(x,y) = y.\nProof. \nTake x ∈ ℝ. Take y ∈ ℝ.\nEither x < y or x ≥ y.\n- Case x < y.\n It suffices to show that (Rmax(x,y) = y).\n We conclude that (Rmax(x,y) = y).\n- Case (x ≥ y).\n It suffices to show that (Rmax(x,y) = x).\n We conclude that (Rmax(x,y) = x).\nQed.", "boost": 2 }, { @@ -268,7 +268,7 @@ "detail": "tactic", "template": "Expand the definition of ${infimum} in (${0 = 0}).", "description": "Expands the definition of the keyword (*name_kw*) in the statement (*expression*).", - "example": "Expand the definition of upper bound in (4 is an upper bound for [0, 3)).", + "example": "Expand the definition of upper bound in (4 is an upper bound for [0, 3).", "advanced": false, "boost": 1 }, @@ -278,7 +278,7 @@ "detail": "tactic", "template": "We show both statements.", "description": "Splits the goal in two separate goals, if it is of the form A ∧ B", - "example": "Lemma example_both_statements:\n ∀ x ∈ ℝ, (x^2 ≥ 0) ∧ (| x | ≥ 0).\nProof.\nTake x ∈ (ℝ).\nWe show both statements.\n- We conclude that (x^2 ≥ 0).\n- We conclude that (| x | ≥ 0).\nQed.", + "example": "Lemma example_both_statements:\n ∀ x ∈ ℝ, (x^2 ≥ 0) ∧ (| x | ≥ 0).\nProof.\nTake x ∈ ℝ.\nWe show both statements.\n- We conclude that x^2 ≥ 0.\n- We conclude that (| x | ≥ 0).\nQed.", "boost": 2 }, { @@ -287,7 +287,7 @@ "detail": "tactic", "template": "We show both directions.", "description": "Splits a goal of the form A ⇔ B, into the goals (A ⇒ B) and (B ⇒ A)", - "example": "Lemma example_both_directions:\n ∀ x ∈ ℝ, ∀ y ∈ ℝ,\n x < y ⇔ y > x.\nProof.\nTake x ∈ (ℝ). Take y ∈ (ℝ).\nWe show both directions.\n- We need to show that (x < y ⇒ y > x).\n Assume that (x < y).\n We conclude that (y > x).\n- We need to show that (y > x ⇒ x < y).\n Assume that (y > x).\n We conclude that (x < y).\nQed.", + "example": "Lemma example_both_directions:\n ∀ x ∈ ℝ, ∀ y ∈ ℝ,\n x < y ⇔ y > x.\nProof.\nTake x ∈ ℝ. Take y ∈ ℝ.\nWe show both directions.\n- We need to show that x < y ⇒ y > x.\n Assume that x < y.\n We conclude that y > x.\n- We need to show that y > x ⇒ x < y.\n Assume that y > x.\n We conclude that x < y.\nQed.", "boost": 2 }, { @@ -300,131 +300,131 @@ "boost": 2 }, { - "label": "By ((*lemma or assumption*)) we conclude that ((*(alternative) formulation of current goal*)).", + "label": "By ((*lemma or assumption*)) we conclude that (*(alternative) formulation of current goal*).", "type": "type", "detail": "tactic", - "template": "By (${i}) we conclude that (${0 = 0}).", + "template": "By (${i}) we conclude that ${0 = 0}.", "description": "Tries to directly prove the goal using (*lemma or assumption*) when the goal corresponds to (*statement*).", "boost": 2 }, { - "label": "Define (*name*) := ((*expression*)).", + "label": "Define *name* := ((*expression*)).", "type": "type", "detail": "tactic", - "template": "Define ${0:s} := (${1:0}).", + "template": "Define ${0:s} := ${1:0}.", "description": "Temporarily give the name (*name*) to the expression (*expression*)", "boost": 2 }, { - "label": "Since ((*extra_statement*)) it holds that ((*statement*)).", + "label": "Since (*extra_statement*) it holds that (*statement*).", "type": "type", "detail": "tactic", - "template": "Since (${1 = 1}) it holds that (${0 = 0}).", + "template": "Since ${1 = 1} it holds that ${0 = 0}.", "description": "Tries to first verify (*extra_statement*) after it uses that to verify (*statement*). The statement gets added as a hypothesis.", - "example": "Since (x = y) it holds that (x = z).", + "example": "Since x = y it holds that x = z.", "boost": 2 }, { - "label": "Since ((*extra_statement*)) it holds that ((*statement*)) ((*label*)).", + "label": "Since (*extra_statement*) it holds that (*statement*) as ((*label*)).", "type": "type", "detail": "tactic", - "template": "Since (${1 = 1}) it holds that (${0 = 0}) (${i}).", + "template": "Since ${1 = 1} it holds that ${0 = 0} as (${i}).", "description": "Tries to first verify (*extra_statement*) after it uses that to verify (*statement*). The statement gets added as a hypothesiwe need to show{s, optionally with the name (*optional_label*).", - "example": "Since (x = y) it holds that (x = z).", + "example": "Since x = y it holds that x = z.", "boost": 1 }, { - "label": "Since ((*extra_statement*)) we conclude that ((*(alternative) formulation of current goal*)).", + "label": "Since (*extra_statement*) we conclude that (*(alternative) formulation of current goal*).", "type": "type", "detail": "tactic", - "template": "Since (${1 = 1}) we conclude that (${0 = 0}).", + "template": "Since ${1 = 1} we conclude that ${0 = 0}.", "description": "Tries to automatically prove the current goal, after first trying to prove (*extra_statement*).", - "example": "Since (x = y) we conclude that (x = z).", + "example": "Since x = y we conclude that x = z.", "boost": 2 }, { - "label": "Since ((*extra_statement*)) it suffices to show that ((*statement*)).", + "label": "Since (*extra_statement*) it suffices to show that (*statement*).", "type": "type", "detail": "tactic", - "template": "Since (${1 = 1}) it suffices to show that (${0 = 0}).", + "template": "Since ${1 = 1} it suffices to show that ${0 = 0}.", "description": "Waterproof tries to verify automatically whether it is indeed enough to show (*statement*) to prove the current goal, after first trying to prove (*extra_statement*). If so, (*statement*) becomes the new goal.", - "example": "Lemma example_backwards :\n 3 < f(0) ⇒ 2 < f(5).\nProof.\nAssume that (3 < f(0)).\nIt suffices to show that (f(0) ≤ f(5)).\nBy (f_increasing) we conclude that (f(0) ≤ f(5)).\nQed.", + "example": "Lemma example_backwards :\n 3 < f(0) ⇒ 2 < f(5).\nProof.\nAssume that 3 < f(0).\nIt suffices to show that f(0) ≤ f(5).\nBy f_increasing we conclude that f(0) ≤ f(5).\nQed.", "advanced": false, "boost": 2 }, { - "label": "Use (*name*) := ((*expression*)) in ((*label*)).", + "label": "Use (*name*) := (*expression*) in ((*label*)).", "type": "type", "detail": "tactic", - "template": "Use ${0:x} := (${1:0}) in (${2:i}).", + "template": "Use ${0:x} := ${1:0} in (${2:i}).", "description": "Use a forall statement, i.e. apply it to a particular expression.", - "example": "Lemma example_use_for_all :\n ∀ x ∈ ℝ,\n (∀ ε > 0, x < ε) ⇒\n x + 1/2 < 1.\nProof.\nTake x ∈ ℝ.\nAssume that (∀ ε > 0, x < ε) (i).\nUse ε := (1/2) in (i).\n* Indeed, (1 / 2 > 0).\n* It holds that (x < 1 / 2).\n We conclude that (x + 1/2 < 1).\nQed.", + "example": "Lemma example_use_for_all :\n ∀ x ∈ ℝ,\n (∀ ε > 0, x < ε) ⇒\n x + 1/2 < 1.\nProof.\nTake x ∈ ℝ.\nAssume that ∀ ε > 0, x < ε as (i).\nUse ε := 1/2 in (i).\n* Indeed, 1 / 2 > 0.\n* It holds that x < 1 / 2.\n We conclude that x + 1/2 < 1.\nQed.", "advanced": false, "boost": 2 }, { - "label": "Indeed, ((*statement*)).", + "label": "Indeed, (*statement*).", "type": "type", "detail": "tactic", - "template": "Indeed, (${0 = 0}).", + "template": "Indeed, ${0 = 0}.", "description": "A synonym for \"We conclude that ((*statement*))\".", - "example": "Lemma example_choose :\n ∃ y ∈ ℝ,\n y < 3.\nProof.\nChoose y := (2).\n* Indeed, (y ∈ ℝ).\n* We conclude that (y < 3).\nQed.", + "example": "Lemma example_choose :\n ∃ y ∈ ℝ,\n y < 3.\nProof.\nChoose y := 2.\n* Indeed, y ∈ ℝ.\n* We conclude that y < 3.\nQed.", "advanced": false, "boost": 1 }, { - "label": "We need to verify that ((*statement*)).", + "label": "We need to verify that (*statement*).", "type": "type", "detail": "tactic", - "template": "We need to verify that (${0 = 0}).", + "template": "We need to verify that ${0 = 0}.", "description": "Used to indicate what to check after using the \"Choose\" or \"Use\" tactic.", - "example": "Lemma example_choose :\n ∃ y ∈ ℝ,\n y < 3.\nProof.\nChoose y := (2).\n* We need to verify that (y ∈ ℝ).\nWe conclude that (y ∈ ℝ).\n* We conclude that (y < 3).\nQed.", + "example": "Lemma example_choose :\n ∃ y ∈ ℝ,\n y < 3.\nProof.\nChoose y := 2.\n* We need to verify that y ∈ ℝ.\nWe conclude that y ∈ ℝ.\n* We conclude that y < 3.\nQed.", "advanced": false, "boost": 1 }, { - "label": "By magic it holds that ((*statement*)) ((*label*)).", + "label": "By magic it holds that (*statement*) as ((*label*)).", "type": "type", "detail": "tactic", - "template": "By magic it holds that (${0 = 0}) (${i}).", + "template": "By magic it holds that ${0 = 0} as (${i}).", "description": "Postpones the proof of (*statement*), and (*statement*) is added as a hypothesis with name (*optional_label*). You can leave out ((*optional_label*)) if you don't wish to name the statement.", - "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that (7 < f(-1)).\nBy magic it holds that (f(-1) ≤ f(6)) (i).\nWe conclude that (2 < f(6)).\nQed.", + "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that 7 < f(-1).\nBy magic it holds that f(-1) ≤ f(6) as (i).\nWe conclude that 2 < f(6).\nQed.", "boost": 1 }, { - "label": "By magic it holds that ((*statement*)).", + "label": "By magic it holds that (*statement*).", "type": "type", "detail": "tactic", - "template": "By magic it holds that (${0 = 0}).", + "template": "By magic it holds that ${0 = 0}.", "description": "Postpones the proof of (*statement*), and (*statement*) is added as a hypothesis.", - "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that (7 < f(-1)).\nBy magic it holds that (f(-1) ≤ f(6)) (i).\nWe conclude that (2 < f(6)).\nQed.", + "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that 7 < f(-1).\nBy magic it holds that f(-1) ≤ f(6) (i).\nWe conclude that 2 < f(6).\nQed.", "boost": 2 }, { - "label": "By magic we conclude that ((*(alternative) formulation of current goal*)).", + "label": "By magic we conclude that (*(alternative) formulation of current goal*).", "type": "type", "detail": "tactic", - "template": "By magic we conclude that (${0 = 0}).", + "template": "By magic we conclude that ${0 = 0}.", "description": "Postpones for now the proof of (a possible alternative formulation of) the current goal.", "boost": 2 }, { - "label": "By magic it suffices to show that ((*statement*)).", + "label": "By magic it suffices to show that (*statement*).", "type": "type", "detail": "tactic", "description": "Postpones for now the proof that (*statement*) is enough to prove the current goal. Now, (*statement*) becomes the new goal.", - "template": "By magic it suffices to show that (${0 = 0}).", + "template": "By magic it suffices to show that ${0 = 0}.", "boost": 2, - "example": "Lemma example_backwards :\n 3 < f(0) ⇒ 2 < f(5).\nProof.\nAssume that (3 < f(0)).\nBy magic it suffices to show that (f(0) ≤ f(5)).\nBy (f_increasing) we conclude that (f(0) ≤ f(5)).\nQed.", + "example": "Lemma example_backwards :\n 3 < f(0) ⇒ 2 < f(5).\nProof.\nAssume that 3 < f(0).\nBy magic it suffices to show that f(0) ≤ f(5).\nBy f_increasing we conclude that f(0) ≤ f(5).\nQed.", "advanced": false }, { - "label": "Case ((*statement*)).", + "label": "Case (*statement*).", "type": "type", "detail": "tactic", - "template": "Case (${0 = 0}).", + "template": "Case ${0 = 0}.", "description": "Used to indicate the case after an \"Either\" sentence.", - "example": "Lemma example_cases : \n ∀ x ∈ ℝ, ∀ y ∈ ℝ,\n Rmax(x,y) = x ∨ Rmax(x,y) = y.\nProof. \nTake x ∈ ℝ. Take y ∈ ℝ.\nEither (x < y) or (x ≥ y).\n- Case (x < y).\n It suffices to show that (Rmax(x,y) = y).\n We conclude that (Rmax(x,y) = y).\n- Case (x ≥ y).\n It suffices to show that (Rmax(x,y) = x).\n We conclude that (Rmax(x,y) = x).\nQed.", + "example": "Lemma example_cases : \n ∀ x ∈ ℝ, ∀ y ∈ ℝ,\n Rmax(x,y) = x ∨ Rmax(x,y) = y.\nProof. \nTake x ∈ ℝ. Take y ∈ ℝ.\nEither x < y or x ≥ y.\n- Case x < y.\n It suffices to show that Rmax(x,y) = y.\n We conclude that Rmax(x,y) = y.\n- Case x ≥ y.\n It suffices to show that Rmax(x,y) = x.\n We conclude that Rmax(x,y) = x.\nQed.", "boost": 1 } ] From 7282fb276920088641dc688dce809fda10d6b304 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 24 Jun 2025 17:42:59 +0200 Subject: [PATCH 29/93] Remove references to mac installer --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 7309afbd..9b3bed3e 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,7 @@ If vscode cannot detect the installation, set the coq-lsp path to the output of using ctrl+shift+p and selecting "Waterproof: Change Waterproof path". Alternatively, make sure that the `PATH` available to vscode contains the coq-lsp binary. -## Manual Installation on Mac - -If the above method did not work for Mac, it is possible to instead install the dependencies manually using opam. +## Installation on Mac ### Step 1: Install this [Waterproof vscode extension](https://marketplace.visualstudio.com/items?itemName=waterproof-tue.waterproof) From d4ee4db946621fde6326974abf0a1635db487f09 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 24 Jun 2025 18:12:03 +0200 Subject: [PATCH 30/93] Disable windows and mac unit tests --- .github/workflows/unit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index f2c46842..f042bd12 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest] node-version: [16.x] steps: From cab2dc0d98ede599b50c093a28618cedf0b6e26f Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Thu, 26 Jun 2025 16:26:54 +0200 Subject: [PATCH 31/93] Patched goals panel to show hypotheses --- views/goals/Goals.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/views/goals/Goals.tsx b/views/goals/Goals.tsx index b3c71e39..4d1b1e17 100644 --- a/views/goals/Goals.tsx +++ b/views/goals/Goals.tsx @@ -29,6 +29,9 @@ function Goal({ goal }: GoalP) { return (
+ {goal.hyps.map((hyp, _) => +
:
+ )}
From b3bd1fd9f7a454b1fcd877d956634dcb0c2d26e1 Mon Sep 17 00:00:00 2001 From: "@20210857" Date: Fri, 27 Jun 2025 21:16:57 +0000 Subject: [PATCH 32/93] Sync: commit all local changes --- .direnv/bin/nix-direnv-reload | 19 +++++++++++++++++++ .vscode/settings.json | 14 +++++++++++++- src/webviewManager.ts | 14 ++++++++++++-- src/webviews/coqWebview.ts | 7 +++++-- src/webviews/sidePanel.ts | 4 ++-- 5 files changed, 51 insertions(+), 7 deletions(-) create mode 100755 .direnv/bin/nix-direnv-reload diff --git a/.direnv/bin/nix-direnv-reload b/.direnv/bin/nix-direnv-reload new file mode 100755 index 00000000..c40a2e84 --- /dev/null +++ b/.direnv/bin/nix-direnv-reload @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e +if [[ ! -d "/home/nixos/waterproof/waterproof-vscode" ]]; then + echo "Cannot find source directory; Did you move it?" + echo "(Looking for "/home/nixos/waterproof/waterproof-vscode")" + echo 'Cannot force reload with this script - use "direnv reload" manually and then try again' + exit 1 +fi + +# rebuild the cache forcefully +_nix_direnv_force_reload=1 direnv exec "/home/nixos/waterproof/waterproof-vscode" true + +# Update the mtime for .envrc. +# This will cause direnv to reload again - but without re-building. +touch "/home/nixos/waterproof/waterproof-vscode/.envrc" + +# Also update the timestamp of whatever profile_rc we have. +# This makes sure that we know we are up to date. +touch -r "/home/nixos/waterproof/waterproof-vscode/.envrc" "/home/nixos/waterproof/waterproof-vscode/.direnv"/*.rc diff --git a/.vscode/settings.json b/.vscode/settings.json index 9101a780..002ec4c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,15 @@ { - "js/ts.implicitProjectConfig.target": "ESNext" + "js/ts.implicitProjectConfig.target": "ESNext", + "files.exclude": { + "**/*.vo": true, + "**/*.vok": true, + "**/*.vos": true, + "**/*.aux": true, + "**/*.glob": true, + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + } } \ No newline at end of file diff --git a/src/webviewManager.ts b/src/webviewManager.ts index 896fbdef..0bba204f 100644 --- a/src/webviewManager.ts +++ b/src/webviewManager.ts @@ -154,16 +154,26 @@ export class WebviewManager extends EventEmitter { } const panel = this._toolWebviews.get(id); - + console.log("called open " + id); // Check if the panel is already open if (panel?.isOpened) { + console.log("Panel is already open: " + id); return; } // Open the panel if it is not already open + else if(panel?.isHidden) { + console.log("Panel is hidden, revealing: " + id); + panel?.readyPanel(); + panel?.revealPanel(); + } + + // Open the panel if it is not hidden and not already open else{ + console.log("Panel is not open, creating: " + id); panel?.readyPanel(); - panel?.activatePanel();} + panel?.activatePanel(); + } } diff --git a/src/webviews/coqWebview.ts b/src/webviews/coqWebview.ts index 5b79a20c..e4c4da9e 100644 --- a/src/webviews/coqWebview.ts +++ b/src/webviews/coqWebview.ts @@ -60,8 +60,11 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { } public get isOpened() { - return (this._state == WebviewState.visible || this._state == WebviewState.open); + return (this._state == WebviewState.visible); } + public get isHidden() { + return (this._state == WebviewState.open); + } protected get state() { return this._state; @@ -177,7 +180,7 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { */ public revealPanel() { if (!this._panel?.visible) { - this._panel?.reveal() + this._panel?.reveal(ViewColumn.Two) } } diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index 9d082583..d3f69fa3 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -26,7 +26,7 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { this._manager.on(WebviewManagerEvents.updateButton, (e) => { // Update the transparency of the button based on the event // This is done when a panel is open - this.updateButtonTransparency(e.name, e.open); + //this.updateButtonTransparency(e.name, e.open); }); } public updateGreyedOutButton(buttonId: string, open: boolean) { @@ -93,7 +93,7 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { webviewView.webview.postMessage({ type: 'restoreTransparency', - greyedOutButtonsList: Array.from(this._greyedOutButtons) + greyedOutButtonsList: Array.from(this._greyedOutButtons) }); } }); From be41735ddda66695931cd2553435a901b845892c Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Wed, 2 Jul 2025 08:57:30 +0200 Subject: [PATCH 33/93] Add option to disable launch checks --- package.json | 5 ++ src/extension.ts | 97 ++++++++++++++++++++---------------- src/helpers/config-helper.ts | 5 ++ 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 8de14c8d..cfa40b5f 100644 --- a/package.json +++ b/package.json @@ -321,6 +321,11 @@ "type": "boolean", "default": false, "description": "Show line numbers in the Waterproof editor" + }, + "waterproof.skipLaunchChecks": { + "type": "boolean", + "default": false, + "description": "Disables the checks that happen when launching the extension. During those checks Waterproof checks whether Coq, coq-lsp and coq-waterproof are installed. Use at your own risk!" } } }, diff --git a/src/extension.ts b/src/extension.ts index 359d6d22..415183f9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -419,54 +419,63 @@ export class Waterproof implements Disposable { */ async initializeClient(): Promise { WaterproofLogger.log("Start of initializeClient"); - // Run the version checker. - const requiredCoqLSPVersion = this.context.extension.packageJSON.requiredCoqLspVersion; - const requiredCoqWaterproofVersion = this.context.extension.packageJSON.requiredCoqWaterproofVersion; - const versionChecker = new VersionChecker(WaterproofConfigHelper.configuration, this.context, requiredCoqLSPVersion, requiredCoqWaterproofVersion); - // - const foundServer = await versionChecker.prelaunchChecks(); + + // Whether the user has decided to skip the launch checks + const launchChecksDisabled = WaterproofConfigHelper.skipLaunchChecks; - if (foundServer) { - - versionChecker.run(); - - if (this.client?.isRunning()) { - return Promise.reject(new Error("Cannot initialize client; one is already running.")) + if (launchChecksDisabled) { + WaterproofLogger.log("'skipLaunchChecks' option has been set by the user. Attempting to launch client..."); + } else { + // Run the version checker. + const requiredCoqLSPVersion = this.context.extension.packageJSON.requiredCoqLspVersion; + const requiredCoqWaterproofVersion = this.context.extension.packageJSON.requiredCoqWaterproofVersion; + const versionChecker = new VersionChecker(WaterproofConfigHelper.configuration, this.context, requiredCoqLSPVersion, requiredCoqWaterproofVersion); + + // Check whether we can find coq-lsp + const foundServer = await versionChecker.prelaunchChecks(); + if (foundServer) { + // Only run the version checker after we know that there is a valid coq-lsp server + versionChecker.run(); + } else { + this.statusBar.failed("LSP not found"); } + } - const serverOptions = CoqLspServerConfig.create( - // TODO: Support +coqversion versions. - this.context.extension.packageJSON.requiredCoqLspVersion.slice(2), - WaterproofConfigHelper.configuration - ); - - const clientOptions: LanguageClientOptions = { - documentSelector: [{ language: "coqmarkdown" }, { language: "coq" }], // both .mv and .v files - outputChannelName: "Waterproof LSP Events (Initial)", - revealOutputChannelOn: RevealOutputChannelOn.Info, - initializationOptions: serverOptions, - markdown: { isTrusted: true, supportHtml: true }, - }; - - WaterproofLogger.log("Initializing client..."); - this.client = this.clientFactory(clientOptions, WaterproofConfigHelper.configuration); - return this.client.startWithHandlers(this.webviewManager).then( - () => { - // show user that LSP is working - this.statusBar.update(true); - this.clientRunning = true; - WaterproofLogger.log("Client initialization complete."); - }, - reason => { - const message = reason.toString(); - WaterproofLogger.log(`Error during client initialization: ${message}`); - this.statusBar.failed(message); - throw reason; // keep chain rejected - } - ); - } else { - this.statusBar.failed("LSP not found"); + if (this.client?.isRunning()) { + return Promise.reject(new Error("Cannot initialize client; one is already running.")) } + + const serverOptions = CoqLspServerConfig.create( + // TODO: Support +coqversion versions. + this.context.extension.packageJSON.requiredCoqLspVersion.slice(2), + WaterproofConfigHelper.configuration + ); + + const clientOptions: LanguageClientOptions = { + documentSelector: [{ language: "coqmarkdown" }, { language: "coq" }], // both .mv and .v files + outputChannelName: "Waterproof LSP Events (Initial)", + revealOutputChannelOn: RevealOutputChannelOn.Info, + initializationOptions: serverOptions, + markdown: { isTrusted: true, supportHtml: true }, + }; + + WaterproofLogger.log("Initializing client..."); + this.client = this.clientFactory(clientOptions, WaterproofConfigHelper.configuration); + return this.client.startWithHandlers(this.webviewManager).then( + () => { + // show user that LSP is working + this.statusBar.update(true); + this.clientRunning = true; + WaterproofLogger.log("Client initialization complete."); + }, + reason => { + const message = reason.toString(); + WaterproofLogger.log(`Error during client initialization: ${message}`); + this.statusBar.failed(message); + throw reason; // keep chain rejected + } + ); + } /** diff --git a/src/helpers/config-helper.ts b/src/helpers/config-helper.ts index d4252abc..8d057c28 100644 --- a/src/helpers/config-helper.ts +++ b/src/helpers/config-helper.ts @@ -22,6 +22,11 @@ export class WaterproofConfigHelper { return config().get("showLineNumbersInEditor") as boolean; } + /** `waterproof.skipLaunchChecks` */ + static get skipLaunchChecks() { + return config().get("skipLaunchChecks") as boolean; + } + /** `waterproof.showLineNumbersInEditor` */ static set showLineNumbersInEditor(value: boolean) { // Update the Waterproof showLineNumbersInEditor configuration From b9de6c145867aca2e742fe7068a7831cf01aee9e Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:29:19 +0200 Subject: [PATCH 34/93] Add error boxes to code cells --- .../src/kroqed-editor/codeview/coqTheme.json | 30 ++++++++-------- editor/src/kroqed-editor/codeview/nodeview.ts | 34 ++++++++++++------- editor/src/kroqed-editor/styles/index.ts | 5 ++- .../src/kroqed-editor/styles/waterproof.css | 12 +++++++ 4 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 editor/src/kroqed-editor/styles/waterproof.css diff --git a/editor/src/kroqed-editor/codeview/coqTheme.json b/editor/src/kroqed-editor/codeview/coqTheme.json index dfa974f3..78525a38 100644 --- a/editor/src/kroqed-editor/codeview/coqTheme.json +++ b/editor/src/kroqed-editor/codeview/coqTheme.json @@ -1,7 +1,7 @@ { "&": { - "color": "var(--vscode-input-foreground)", - "backgroundColor": "var(--vscode-listFilterWidget-background)" + "color": "var(--wp-cm-foreground)", + "backgroundColor": "var(--wp-cm-background)" }, ".cm-diagnosticButton": { @@ -21,23 +21,21 @@ ".cm-content": { "caretColor": "auto", - "fontFamily": "var(--vscode-editor-font-family)", - "fontSize": "var(--vscode-editor-font-size)", - "fontWeight": "var(--vscode-editor-font-weight)" + "fontFamily": "var(--wp-editor-font-family)", + "fontSize": "var(--wp-editor-font-size)", + "fontWeight": "var(--wp-editor-font-weight)" }, ".cm-cursor, .cm-dropCursor": {"borderLeftColor": "auto"}, - ".cm-panels": {"backgroundColor": "var(--vscode-list-dropBackground)", "color": "var(--vscode-list-highlightForeground)"}, - ".cm-panels.cm-panels-top": {"borderBottom": "2px solid black"}, - ".cm-panels.cm-panels-bottom": {"borderTop": "2px solid black"}, - - ".cm-searchMatch": { - "backgroundColor": "#72a1ff59", - "outline": "1px solid #457dff" - }, - ".cm-searchMatch.cm-searchMatch-selected": { - "backgroundColor": "#6199ff2f" + ".cm-panels": {"backgroundColor": "transparent", "color": "var(--vscode-list-highlightForeground)"}, + ".cm-panel-lint": {"backgroundColor": "transparent", "color": "var(--vscode-list-highlightForeground)"}, + ".cm-panel-lint > ul": { + "backgroundColor": "transparent", + "color": "var(--wp-cm-lint-panel-foreground)", + "display": "flex", + "flexFlow": "column" }, + ".cm-activeLine": {"backgroundColor": "#6699ff0b"}, "&:not(.cm-focused) .cm-activeLine": { "backgroundColor": "transparent"}, ".cm-selectionMatch": {"backgroundColor": "#aafe661a"}, @@ -85,4 +83,4 @@ "width": "1.5em", "opacity": "1.0" } -} +} \ No newline at end of file diff --git a/editor/src/kroqed-editor/codeview/nodeview.ts b/editor/src/kroqed-editor/codeview/nodeview.ts index 2c530009..e142514c 100644 --- a/editor/src/kroqed-editor/codeview/nodeview.ts +++ b/editor/src/kroqed-editor/codeview/nodeview.ts @@ -11,7 +11,7 @@ import { EditorView } from "prosemirror-view" import { customTheme } from "./color-scheme" import { symbolCompletionSource, coqCompletionSource, tacticCompletionSource, renderIcon } from "../autocomplete"; import { EmbeddedCodeMirrorEditor } from "../embedded-codemirror"; -import { linter, LintSource, Diagnostic, setDiagnosticsEffect } from "@codemirror/lint"; +import { linter, LintSource, Diagnostic, setDiagnosticsEffect, lintGutter } from "@codemirror/lint"; import { Debouncer } from "./debouncer"; import { INPUT_AREA_PLUGIN_KEY } from "../inputArea"; @@ -21,7 +21,6 @@ import { INPUT_AREA_PLUGIN_KEY } from "../inputArea"; * Corresponds with the example as can be found here: * https://prosemirror.net/examples/codemirror/ */ - export class CodeBlockView extends EmbeddedCodeMirrorEditor { dom: HTMLElement; @@ -77,11 +76,18 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { return div; } + const inInputArea = this.partOfInputArea(); + const optional = inInputArea ? [lintGutter()] : []; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const shadowThis = this; this._codemirror = new CodeMirror({ doc: this._node.textContent, extensions: [ // Add the linting extension for showing diagnostics (errors, warnings, etc) - linter(this.lintingFunction), + linter(this.lintingFunction, { + autoPanel: inInputArea, // Only enable auto panel when this view is inside of an input area + }), + ...optional, this._readOnlyCompartment.of(EditorState.readOnly.of(!this._outerView.editable)), this._lineNumberCompartment.of(this._lineNumbersExtension), @@ -125,14 +131,7 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { } if (locked) { - // in student mode. - const pos = getPos(); - if (pos === undefined) return; - // Resolve the position in the prosemirror document and get the node one level underneath the root. - // TODO: Assumption that ``s only ever appear one level beneath the root node. - // TODO: Hardcoded node names. - const name = outerView.state.doc.resolve(pos).node(1).type.name; - if (name !== "input") return; // This node is not part of an input area. + if (shadowThis.partOfInputArea()) return; } view.update([tr]); @@ -155,6 +154,17 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { this.handleNewComplete([]); } + private partOfInputArea(): boolean { + const pos = this._getPos(); + if (pos === undefined) return false; + // Resolve the position in the prosemirror document and get the node one level underneath the root. + // TODO: Assumption that ``s only ever appear one level beneath the root node. + // TODO: Hardcoded node names. + const name = this._outerView.state.doc.resolve(pos).node(1).type.name; + if (name !== "input") return false; + return true; + } + public handleSnippet(template: string, posFrom: number, posTo: number) { this._codemirror?.focus(); snippet(template)({ @@ -403,4 +413,4 @@ const severityToString = (sv: number) => { default: return "error"; } -} +} \ No newline at end of file diff --git a/editor/src/kroqed-editor/styles/index.ts b/editor/src/kroqed-editor/styles/index.ts index 9fac4fbb..4f5242eb 100644 --- a/editor/src/kroqed-editor/styles/index.ts +++ b/editor/src/kroqed-editor/styles/index.ts @@ -37,4 +37,7 @@ import "./freeze.css"; import "./context-menu.css"; // For the symbol autocompletions: -import "./autocomplete.css"; \ No newline at end of file +import "./autocomplete.css"; + +// Imports all variable bindings: +import "./waterproof.css"; diff --git a/editor/src/kroqed-editor/styles/waterproof.css b/editor/src/kroqed-editor/styles/waterproof.css new file mode 100644 index 00000000..8ec2c24e --- /dev/null +++ b/editor/src/kroqed-editor/styles/waterproof.css @@ -0,0 +1,12 @@ +:root { + /* General Waterproof Editor styling */ + --wp-editor-font-family: var(--vscode-editor-font-family); + --wp-editor-font-size: var(--vscode-editor-font-size); + --wp-editor-font-weight: var(--vscode-editor-font-weight); + + /* CodeMirror related styling */ + --wp-cm-foreground: var(--vscode-input-foreground); + --wp-cm-background: var(--vscode-listFilterWidget-background); + + --wp-cm-lint-panel-foreground: var(--vscode-list-highlightForeground); +} \ No newline at end of file From 1ec9e4bfc63d7628f7b24bcf85ad2cb768934227 Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Wed, 2 Jul 2025 12:22:19 +0200 Subject: [PATCH 35/93] Disable showing menu buttons by default --- editor/src/index.ts | 2 ++ editor/src/kroqed-editor/editor.ts | 11 +++++++++-- editor/src/kroqed-editor/menubar/menubar.ts | 21 ++++++++++++++------- package.json | 5 +++++ shared/Messages.ts | 2 ++ src/helpers/config-helper.ts | 5 +++++ src/pm-editor/pmWebview.ts | 14 ++++++++++++++ 7 files changed, 51 insertions(+), 9 deletions(-) diff --git a/editor/src/index.ts b/editor/src/index.ts index 7cc1e996..513f6114 100644 --- a/editor/src/index.ts +++ b/editor/src/index.ts @@ -93,6 +93,8 @@ window.onload = () => { { const show = msg.body; editor.setShowLineNumbers(show); break; } + case MessageType.setShowMenuItems: + { const show = msg.body; editor.setShowMenuItems(show); break; } case MessageType.editorHistoryChange: editor.handleHistoryChange(msg.body); break; diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/kroqed-editor/editor.ts index 9e62b6fb..12879803 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/kroqed-editor/editor.ts @@ -114,7 +114,6 @@ export class WaterproofEditor { // Initialize the file translator given the fileformat. if(this._view) { if (this._mapping && this._mapping.version == version) return; - this._view.dispatch(this._view.state.tr.setMeta(MENU_PLUGIN_KEY, "remove")); // Hack to forcefully remove the 'old' menubar document.querySelector(".menubar")?.remove(); document.querySelector(".progress-bar")?.remove(); @@ -415,6 +414,14 @@ export class WaterproofEditor { this.sendLineNumbers(); } + public setShowMenuItems(show: boolean) { + const view = this._view; + if (view === undefined) return; + const tr = view.state.tr; + tr.setMeta(MENU_PLUGIN_KEY, show); + view.dispatch(tr); + } + private createAndDispatchInsertionTransaction( trans: Transaction, textToInsert: string, from: number, to: number) { @@ -431,7 +438,7 @@ export class WaterproofEditor { if (!this._view) return; const state = this._view.state; const trans = state.tr; - trans.setMeta(INPUT_AREA_PLUGIN_KEY,!isTeacher); + trans.setMeta(INPUT_AREA_PLUGIN_KEY, {teacher: !isTeacher}); this._view.dispatch(trans); } diff --git a/editor/src/kroqed-editor/menubar/menubar.ts b/editor/src/kroqed-editor/menubar/menubar.ts index 5ec77afc..4245fdaa 100644 --- a/editor/src/kroqed-editor/menubar/menubar.ts +++ b/editor/src/kroqed-editor/menubar/menubar.ts @@ -11,6 +11,7 @@ import { FileFormat } from "../../../../shared"; type MenuEntry = { dom: HTMLElement; teacherModeOnly: boolean; + showByDefault: boolean; cmd: Command; }; @@ -22,7 +23,7 @@ type MenuEntry = { * @param teacherModeOnly [`false` by default] Whether this button should only be available in teacher mode. * @returns A new `MenuButton` object. */ -function createMenuItem(displayedText: string, tooltipText: string, cmd: Command, teacherModeOnly: boolean = false): MenuEntry { +function createMenuItem(displayedText: string, tooltipText: string, cmd: Command, teacherModeOnly: boolean = false, showByDefault: boolean = false): MenuEntry { // Create the DOM element. const menuItem = document.createElement("div"); // Set the displayed text and the tooltip. @@ -31,7 +32,7 @@ function createMenuItem(displayedText: string, tooltipText: string, cmd: Command // Add the class for styling this menubar item menuItem.classList.add("menubar-item"); // Return the new item. - return {cmd, dom: menuItem, teacherModeOnly}; + return {cmd, dom: menuItem, teacherModeOnly, showByDefault}; } /** @@ -84,6 +85,9 @@ class MenuView implements PluginView { update(view: EditorView) { // Whether we are currently in teacher mode. const inTeacherMode = MENU_PLUGIN_KEY.getState(view.state)?.teacher; + const showItems = MENU_PLUGIN_KEY.getState(view.state)?.showMenuItems; + + console.log("SHOW ITEMS", showItems); for(const item of this.items) { const active = item.cmd(this.view.state, undefined, this.view); @@ -98,7 +102,7 @@ class MenuView implements PluginView { // We hide the item (set display to none) if it should only be available in teacher mode // and the user is not in teacher mode. - if (item.teacherModeOnly && !inTeacherMode) { + if ((item.teacherModeOnly && !inTeacherMode) || (!item.showByDefault && showItems)) { item.dom.style.display = "none"; } } @@ -179,6 +183,7 @@ function createDefaultMenu(schema: Schema, outerView: EditorView, filef: FileFor */ interface IMenuPluginState { teacher: boolean; + showMenuItems: boolean; } /** @@ -215,17 +220,19 @@ export function menuPlugin(schema: Schema, filef: FileFormat, os: OS) { init(_config, _instance): IMenuPluginState { // Initially teacher mode is set to true. return { - teacher: true + teacher: true, + showMenuItems: false, } }, apply(tr, value, _oldState, _newState) { // Upon recieving a transaction with meta information... - let teacherState = !tr.getMeta(INPUT_AREA_PLUGIN_KEY); + const teacherState = !tr.getMeta(INPUT_AREA_PLUGIN_KEY); + const menuState = tr.getMeta(MENU_PLUGIN_KEY); // we check if the teacherState variable is set and if so, // we update the plugin state to reflect this - if (teacherState === undefined) teacherState = value.teacher; return { - teacher: teacherState, + teacher: teacherState === undefined ? value.teacher : teacherState, + showMenuItems: menuState === undefined ? value.showMenuItems : menuState, }; }, } diff --git a/package.json b/package.json index cfa40b5f..7db7a802 100644 --- a/package.json +++ b/package.json @@ -326,6 +326,11 @@ "type": "boolean", "default": false, "description": "Disables the checks that happen when launching the extension. During those checks Waterproof checks whether Coq, coq-lsp and coq-waterproof are installed. Use at your own risk!" + }, + "waterproof.showMenuItemsInEditor": { + "type": "boolean", + "default": false, + "description": "Enables the menu items in the editor. Using these buttons you can add text, code and latex cells in input areas." } } }, diff --git a/shared/Messages.ts b/shared/Messages.ts index 5fb0bf00..e7e6da24 100644 --- a/shared/Messages.ts +++ b/shared/Messages.ts @@ -44,6 +44,7 @@ export type Message = | MessageBase | MessageBase > | MessageBase + | MessageBase | MessageBase | MessageBase; @@ -71,6 +72,7 @@ export const enum MessageType { setAutocomplete, setData, setShowLineNumbers, + setShowMenuItems, syntax, teacher, } diff --git a/src/helpers/config-helper.ts b/src/helpers/config-helper.ts index 8d057c28..c260fb59 100644 --- a/src/helpers/config-helper.ts +++ b/src/helpers/config-helper.ts @@ -27,6 +27,11 @@ export class WaterproofConfigHelper { return config().get("skipLaunchChecks") as boolean; } + /** `waterproof.showMenuItemsInEditor` */ + static get showMenuItems() { + return config().get("showMenuItemsInEditor") as boolean; + } + /** `waterproof.showLineNumbersInEditor` */ static set showLineNumbersInEditor(value: boolean) { // Update the Waterproof showLineNumbersInEditor configuration diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index b1d4d0bd..7b3cbd36 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -41,6 +41,7 @@ export class ProseMirrorWebview extends EventEmitter { private _provider: CoqEditorProvider; private _showLineNrsInEditor: boolean = WaterproofConfigHelper.showLineNumbersInEditor; + private _showMenuItemsInEditor: boolean = WaterproofConfigHelper.showMenuItems; /** These regions contain the strings that are outside of the tags, but including the tags themselves */ private _nonInputRegions: { @@ -194,6 +195,12 @@ export class ProseMirrorWebview extends EventEmitter { this._showLineNrsInEditor = WaterproofConfigHelper.showLineNumbersInEditor; this.updateLineNumberStatusInEditor(); } + + if (e.affectsConfiguration("waterproof.showMenuItemsInEditor")) { + this._showMenuItemsInEditor = WaterproofConfigHelper.showMenuItems; + WaterproofLogger.log(`Will now show menu items? ${WaterproofConfigHelper.showMenuItems}`); + this.updateMenuItemsInEditor(); + } })); this._disposables.push(this._panel.webview.onDidReceiveMessage((msg) => { @@ -275,6 +282,13 @@ export class ProseMirrorWebview extends EventEmitter { } + private updateMenuItemsInEditor() { + this.postMessage({ + type: MessageType.setShowMenuItems, + body: this._showMenuItemsInEditor + }, true); + } + /** Convert line number offsets to line indices and send message to Editor webview */ private updateLineNumbers() { // Early return if line numbers should not be shown in the editor. From 77f0043255ee6a0f0bdf105f77fcc03277b1f537 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 2 Jul 2025 16:04:23 +0200 Subject: [PATCH 36/93] Skip launch checks for webversion --- src/extension.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 2f1e1c3e..8dda585a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -294,6 +294,7 @@ export class Waterproof implements Disposable { * Attempts to install all required libraries * @returns A promise containing either the Version of coq-lsp we found or a VersionError containing an error message. */ + private async autoInstall(command: string): Promise { return new Promise((resolve, _reject) => { let fired = false; @@ -424,8 +425,9 @@ export class Waterproof implements Disposable { // Whether the user has decided to skip the launch checks const launchChecksDisabled = WaterproofConfigHelper.skipLaunchChecks; - if (launchChecksDisabled) { - wpl.log("'skipLaunchChecks' option has been set by the user. Attempting to launch client..."); + if (launchChecksDisabled || this._isWeb) { + const reason = launchChecksDisabled ? "Launch checks disabled by user." : "Web extension, skipping launch checks."; + wpl.log(`${reason} Attempting to launch client...`); } else { // Run the version checker. const requiredCoqLSPVersion = this.context.extension.packageJSON.requiredCoqLspVersion; From 34625b99ba3cc3ca49dff5ff2b688490823d627b Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 2 Jul 2025 16:58:34 +0200 Subject: [PATCH 37/93] Revert to child_process based workflow --- esbuild.mjs | 2 +- src/extension.ts | 33 ++++++++++------------- src/version-checker/version-checker.ts | 36 ++++++++------------------ 3 files changed, 26 insertions(+), 45 deletions(-) diff --git a/esbuild.mjs b/esbuild.mjs index cd1ef046..12733124 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -74,7 +74,7 @@ var browser = esbuild ...sourcemap_client, format: "cjs", platform: "browser", - external: ["vscode"], + external: ["vscode", "child_process"], outfile: "out/src/mainBrowser.js", minify, watch: watch("./src/mainBrowser.ts"), diff --git a/src/extension.ts b/src/extension.ts index 8dda585a..7c641aaa 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -294,24 +294,19 @@ export class Waterproof implements Disposable { * Attempts to install all required libraries * @returns A promise containing either the Version of coq-lsp we found or a VersionError containing an error message. */ - private async autoInstall(command: string): Promise { return new Promise((resolve, _reject) => { - let fired = false; - const myTerm = window.createTerminal(`AutoInstall Waterproof`) - myTerm.show() - window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration}) => { - if (terminal === myTerm && !fired) { - fired = true - const execution = shellIntegration.executeCommand(command); - window.onDidEndTerminalShellExecution(event => { - if (event.execution === execution) { - this.initializeClient(); - resolve(true); - } - }) + // Doing the require here to avoid issues with the import in the browser version + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { exec } = require("child_process"); + exec(command, (err : unknown, _stdout: unknown, _stderr: unknown) => { + if (err) { + // Simple fixed scripts are run, the user is able to stop these but they are not considered errors + // as the user has freedom to choose the steps and can rerun the command. } - }) + this.initializeClient(); + resolve(true) + }); }); } @@ -462,18 +457,18 @@ export class Waterproof implements Disposable { markdown: { isTrusted: true, supportHtml: true }, }; - WaterproofLogger.log("Initializing client..."); - this.client = this.clientFactory(clientOptions, WaterproofConfigHelper.configuration); + wpl.log("Initializing client..."); + this.client = this.clientFactory(this.context, clientOptions, WaterproofConfigHelper.configuration); return this.client.startWithHandlers(this.webviewManager).then( () => { // show user that LSP is working this.statusBar.update(true); this.clientRunning = true; - WaterproofLogger.log("Client initialization complete."); + wpl.log("Client initialization complete."); }, reason => { const message = reason.toString(); - WaterproofLogger.log(`Error during client initialization: ${message}`); + wpl.log(`Error during client initialization: ${message}`); this.statusBar.failed(message); throw reason; // keep chain rejected } diff --git a/src/version-checker/version-checker.ts b/src/version-checker/version-checker.ts index 50e049ef..3d9459c9 100644 --- a/src/version-checker/version-checker.ts +++ b/src/version-checker/version-checker.ts @@ -161,32 +161,18 @@ export class VersionChecker { /** Wrapper around shellIntegration */ private async exec(command: string): Promise { wpl.log(`Running command: ${command}`) - return new Promise((resolve, _reject) => { - const myTerm = window.createTerminal(`Waterproof commands -- ${command}`) - let fired = false; - - window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration}) => { - if (terminal === myTerm && !fired) { - const execution = shellIntegration.executeCommand(command); - const outputStream = execution.read(); - fired = true; - wpl.debug(`Type of outputStream: ${typeof outputStream}`) - wpl.debug(`Output stream: ${outputStream}`) - window.onDidEndTerminalShellExecution(async event => { - if (event.execution === execution) { - let output = ""; - for await (const data of outputStream) { - output += data - } - wpl.debug(`Output of ran command ${output.substring(8)}`) - myTerm.hide(); - myTerm.dispose(); - // Remove terminal-artifacts from the output by taking the first 8 characters - resolve(output.substring(8)); - } - }) + return new Promise((resolve, reject) => { + // We use require here to avoid issues with the import statement in the browser context. + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { exec } = require("child_process"); + exec(command, (err: { message: string; }, stdout: string, _stderr: unknown) => { + if (err) { + reject({ reason: err.message }); + } else { + resolve(stdout); } - }) + }); + }); } From fe938331338fb273075929c1cdbe57872f56ed81 Mon Sep 17 00:00:00 2001 From: raulTUe Date: Thu, 3 Jul 2025 22:52:30 +0200 Subject: [PATCH 38/93] quick add --- src/pm-editor/pmWebview.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index 0dec771e..3e5294fc 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -280,6 +280,9 @@ export class ProseMirrorWebview extends EventEmitter { version: this._document.version, } }); + this.updateLineNumberStatusInEditor(); + // send any cached messages + for (const m of this._cachedMessages.values()) this.postMessage(m); } private updateLineNumberStatusInEditor() { From ee26eb9e68df81172e550ca8fb933d10df3cb1db Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Sun, 6 Jul 2025 10:14:34 +0200 Subject: [PATCH 39/93] Add tutorial --- misc-includes/waterproof_tutorial.mv | 134 ++++++++++++++------------- 1 file changed, 69 insertions(+), 65 deletions(-) diff --git a/misc-includes/waterproof_tutorial.mv b/misc-includes/waterproof_tutorial.mv index 6c0ec369..4067b6eb 100644 --- a/misc-includes/waterproof_tutorial.mv +++ b/misc-includes/waterproof_tutorial.mv @@ -16,10 +16,14 @@ Require Import Waterproof.Automation. Waterproof Enable Automation RealsAndIntegers. + Open Scope R_scope. Open Scope subset_scope. Set Default Goal Selector "!". +Set Bullet Behavior "Waterproof Relaxed Subproofs". + + Notation "'max(' x , y )" := (Rmax x y) (format "'max(' x , y ')'"). Notation "'min(' x , y )" := (Rmin x y) @@ -33,7 +37,7 @@ Lemma example_reflexivity : 0 = 0. Proof. Help. (* optional, ask for help, remove from final proof *) -We conclude that (0 = 0). +We conclude that 0 = 0. Qed. ``` ### Try it yourself: @@ -59,8 +63,8 @@ Sometimes it is useful to remind the reader or yourself of what you need to show Lemma example_we_need_to_show_that : 2 = 2. Proof. -We need to show that (2 = 2). -We conclude that (2 = 2). +We need to show that 2 = 2. +We conclude that 2 = 2. Qed. ``` ### Try it yourself @@ -86,7 +90,7 @@ Lemma example_take : x = x. Proof. Take x ∈ ℝ. -We conclude that (x = x). +We conclude that x = x. Qed. ``` ### Try it yourself: @@ -111,9 +115,9 @@ Lemma example_choose : ∃ y ∈ ℝ, y < 3. Proof. -Choose y := (2). -* Indeed, (y ∈ ℝ). -* We conclude that (y < 3). +Choose y := 2. +* Indeed, y ∈ ℝ. +* We conclude that y < 3. Qed. ``` ### Try it yourself @@ -140,11 +144,11 @@ Lemma example_combine_quantifiers : ∃ c ∈ ℝ, c > b - a. Proof. -Take a ∈ (ℝ). +Take a ∈ ℝ. Take b > 5. -Choose c := (b - a + 1). -* Indeed, (c ∈ ℝ). -* We conclude that (c > b - a). +Choose c := b - a + 1. +* Indeed, c ∈ ℝ. +* We conclude that c > b - a. Qed. ``` ### Try it yourself @@ -171,9 +175,9 @@ Lemma example_assumptions : ∀ a ∈ ℝ, a < 0 ⇒ - a > 0. Proof. -Take a ∈ (ℝ). -Assume that (a < 0). -We conclude that (- a > 0). +Take a ∈ ℝ. +Assume that a < 0. +We conclude that - a > 0. Qed. ``` ### Another example with explicit labels @@ -182,9 +186,9 @@ Lemma example_assumptions_2 : ∀ a ∈ ℝ, a < 0 ⇒ - a > 0. Proof. -Take a ∈ (ℝ). -Assume that (a < 0) (i). (* The label here is optional *) -By (i) we conclude that (- a > 0). +Take a ∈ ℝ. +Assume that a < 0 as (i). (* The label here is optional *) +By (i) we conclude that - a > 0. Qed. ``` ### Try it yourself @@ -192,7 +196,7 @@ Qed. Lemma exercise_assumptions : ∀ a ≥ 2, ∀ b ∈ ℝ, - a > 0 ⇒ (b > 0 ⇒ a + b > - 1). + a > 0 ⇒ b > 0 ⇒ a + b > - 1. Proof. ``` @@ -213,8 +217,8 @@ Parameter f_increasing : ∀ x ∈ ℝ, ∀ y ∈ ℝ, x ≤ y ⇒ f(x) ≤ f(y) Lemma example_inequalities: 2 < f(0) ⇒ 2 < f(1). Proof. -Assume that (2 < f(0)). -By (f_increasing) we conclude that (& 2 < f(0) ≤ f(1)). +Assume that 2 < f(0). +By f_increasing we conclude that & 2 < f(0) ≤ f(1). Qed. ``` ### Try it yourself @@ -238,9 +242,9 @@ Qed. Lemma example_backwards : 3 < f(0) ⇒ 2 < f(5). Proof. -Assume that (3 < f(0)). -It suffices to show that (f(0) ≤ f(5)). -By (f_increasing) we conclude that (f(0) ≤ f(5)). +Assume that 3 < f(0). +It suffices to show that f(0) ≤ f(5). +By f_increasing we conclude that f(0) ≤ f(5). Qed. ``` ### Try it yourself @@ -263,9 +267,9 @@ Qed. Lemma example_forwards : 7 < f(-1) ⇒ 2 < f(6). Proof. -Assume that (7 < f(-1)). -By (f_increasing) it holds that (f(-1) ≤ f(6)). -We conclude that (2 < f(6)). +Assume that 7 < f(-1). +By f_increasing it holds that f(-1) ≤ f(6). +We conclude that 2 < f(6). Qed. ``` ### Try it yourself @@ -295,11 +299,11 @@ Lemma example_use_for_all : x + 1/2 < 1. Proof. Take x ∈ ℝ. -Assume that (∀ ε > 0, x < ε) (i). -Use ε := (1/2) in (i). -* Indeed, (1 / 2 > 0). -* It holds that (x < 1 / 2). - We conclude that (x + 1/2 < 1). +Assume that ∀ ε > 0, x < ε as (i). +Use ε := 1/2 in (i). +* Indeed, 1 / 2 > 0. +* It holds that x < 1 / 2. + We conclude that x + 1/2 < 1. Qed. ``` ### Try it yourself @@ -328,9 +332,9 @@ Lemma example_use_there_exists : 10 < x. Proof. Take x ∈ ℝ. -Assume that (∃ y > 10, y < x) (i). +Assume that ∃ y > 10, y < x as (i). Obtain such a y. -We conclude that (& 10 < y < x). +We conclude that & 10 < y < x. Qed. ``` ### Another example @@ -341,9 +345,9 @@ Lemma example_use_there_exists_2 : 12 < x. Proof. Take x ∈ ℝ. -Assume that (∃ y > 14, y < x) (i). +Assume that ∃ y > 14, y < x as (i). Obtain y according to (i). -We conclude that (& 12 < y < x). +We conclude that & 12 < y < x. Qed. ``` ### Try it yourself @@ -371,13 +375,13 @@ Lemma example_contradicition : (∀ ε > 0, x > 1 - ε) ⇒ x ≥ 1. Proof. -Take x ∈ (ℝ). -Assume that (∀ ε > 0, x > 1 - ε) (i). -We need to show that (x ≥ 1). +Take x ∈ ℝ. +Assume that ∀ ε > 0, x > 1 - ε as (i). +We need to show that x ≥ 1. We argue by contradiction. -Assume that (¬ (x ≥ 1)). -It holds that ((1 - x) > 0). -By (i) it holds that (x > 1 - (1 - x)). +Assume that ¬ (x ≥ 1). +It holds that (1 - x) > 0. +By (i) it holds that x > 1 - (1 - x). Contradiction. Qed. ``` @@ -409,13 +413,13 @@ Proof. ```coq Take x ∈ (ℝ). Take y ∈ (ℝ). -Either (x < y) or (x ≥ y). -- Case (x < y). - It suffices to show that (max(x, y) = y). - We conclude that (max(x, y) = y). -- Case (x ≥ y). - It suffices to show that (max(x, y) = x). - We conclude that (max(x, y) = x). +Either x < y or x ≥ y. +- Case x < y. + It suffices to show that max(x, y) = y. + We conclude that max(x, y) = y. +- Case x ≥ y. + It suffices to show that max(x, y) = x. + We conclude that max(x, y) = x. Qed. ``` ### Try it yourself @@ -439,12 +443,12 @@ Qed. Lemma example_both_statements : ∀ x ∈ ℝ, x^2 ≥ 0 ∧ | x | ≥ 0. Proof. -Take x ∈ (ℝ). +Take x ∈ ℝ. We show both statements. -* We need to show that (x^2 ≥ 0). - We conclude that (x^2 ≥ 0). -* We need to show that (| x | ≥ 0). - We conclude that (| x | ≥ 0). +* We need to show that x^2 ≥ 0. + We conclude that x^2 ≥ 0. +* We need to show that | x | ≥ 0. + We conclude that | x | ≥ 0. Qed. ``` ### Try it yourself @@ -468,15 +472,15 @@ Lemma example_both_directions : ∀ x ∈ ℝ, ∀ y ∈ ℝ, x < y ⇔ y > x. Proof. -Take x ∈ (ℝ). -Take y ∈ (ℝ). +Take x ∈ ℝ. +Take y ∈ ℝ. We show both directions. -++ We need to show that (x < y ⇒ y > x). - Assume that (x < y). - We conclude that (y > x). -++ We need to show that (y > x ⇒ x < y). - Assume that (y > x). - We conclude that (x < y). +++ We need to show that x < y ⇒ y > x. + Assume that x < y. + We conclude that y > x. +++ We need to show that y > x ⇒ x < y. + Assume that y > x. + We conclude that x < y. Qed. ``` ### Try it yourself @@ -500,10 +504,10 @@ Lemma example_induction : ∀ n : ℕ → ℕ, (∀ k ∈ ℕ, (n(k) < n(k+1))%nat) ⇒ ∀ k ∈ ℕ, (k ≤ n(k))%nat. Proof. -Take n : (ℕ → ℕ). +Take n : ℕ → ℕ. Assume that (∀ k ∈ ℕ, n(k) < n(k+1))%nat. We use induction on k. -+ We first show the base case ((0 ≤ n(0))%nat). ++ We first show the base case (0 ≤ n(0))%nat. We conclude that (0 ≤ n(0))%nat. + We now show the induction step. Take k ∈ ℕ. @@ -543,8 +547,8 @@ Proof. Take x ∈ (ℝ). Expand the definition of square. (* Remove the above line in your own code! *) -We need to show that (x^2 ≥ 0). -We conclude that (x^2 ≥ 0). +We need to show that x^2 ≥ 0. +We conclude that x^2 ≥ 0. Qed. ``` ### Try it yourself From 6c6b86544c654927b726b487ca52b07ff1f6543a Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Sun, 6 Jul 2025 20:54:41 +0200 Subject: [PATCH 40/93] Work-in-progress --- lib/types.ts | 6 ++++ package.json | 10 ++++++ shared/Messages.ts | 6 ++-- src/helpers/config-helper.ts | 5 +++ src/webviews/goalviews/goalsBase.ts | 2 +- src/webviews/goalviews/logbook.ts | 4 +-- views/debug/Debug.tsx | 9 ++--- views/goals/Goals.tsx | 51 ++++++++++++++++++++++------- views/goals/Info.tsx | 12 +++++-- views/logbook/Logbook.tsx | 2 +- 10 files changed, 83 insertions(+), 24 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 5df0670f..9ef579f5 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -143,3 +143,9 @@ export interface DocumentPerfParams { summary: string; timings: SentencePerfParams[]; } + +export enum HypVisibility { + All = "all", + Limited = "limited", + None = "none" +} diff --git a/package.json b/package.json index 8de14c8d..b5b0ddfa 100644 --- a/package.json +++ b/package.json @@ -393,6 +393,16 @@ "Use jsCoq's Pp rich layout printer", "Coq Layout Engine (experimental)" ] + }, + "waterproof.hyp_visibilty" : { + "type": "string", + "default": "none", + "enum": [ + "all", + "limited", + "none" + ], + "description": "Visibility of hypotheses in the goals panel.\n- `all`: show all hypotheses\n- `limited`: show only hypotheses not starting with _\n- `none`: do not show any hypotheses\nFor didiactic purposes, we recommend `none` for mathematicians learning proofs and `limited` or `all` for students using the Rocq language." } } }, diff --git a/shared/Messages.ts b/shared/Messages.ts index 5fb0bf00..eafb00c3 100644 --- a/shared/Messages.ts +++ b/shared/Messages.ts @@ -4,7 +4,7 @@ import { LineNumber } from "./LineNumber"; import { DocChange, WrappingDocChange } from "./DocChange"; import { QedStatus } from "./QedStatus"; import { Completion } from "@codemirror/autocomplete"; -import { GoalAnswer, PpString } from "../lib/types"; +import { GoalAnswer, HypVisibility, PpString } from "../lib/types"; /** Type former for the `Message` type. A message has an optional body B, but must include a type T (from MessageType) * @@ -39,7 +39,8 @@ export type Message = | MessageBase | MessageBase | MessageBase - | MessageBase + | MessageBase[], visibility?: HypVisibility }> + | MessageBase | MessageBase | MessageBase | MessageBase > @@ -67,6 +68,7 @@ export const enum MessageType { qedStatus, ready, renderGoals, + renderGoalsLegacy, response, setAutocomplete, setData, diff --git a/src/helpers/config-helper.ts b/src/helpers/config-helper.ts index d4252abc..ddcee422 100644 --- a/src/helpers/config-helper.ts +++ b/src/helpers/config-helper.ts @@ -75,6 +75,11 @@ export class WaterproofConfigHelper { return config().get("pp_type") as number; } + + /** `waterproof.hyp_visibility` */ + static get hyp_visibility() { + return config().get<"all" | "limited" | "none">; + } /** `waterproof.trace.server` */ static get trace_server() { return config().get<"off" | "messages" | "verbose">("trace.server") as "off" | "messages" | "verbose"; diff --git a/src/webviews/goalviews/goalsBase.ts b/src/webviews/goalviews/goalsBase.ts index 575c21ab..b01b3bb7 100644 --- a/src/webviews/goalviews/goalsBase.ts +++ b/src/webviews/goalviews/goalsBase.ts @@ -17,7 +17,7 @@ export abstract class GoalsBase extends CoqWebview implements IGoalsComponent { //sends message for renderGoals updateGoals(goals: GoalAnswer | undefined) { - this.postMessage({ type: MessageType.renderGoals, body: goals }); + this.postMessage({ type: MessageType.renderGoals, body: {goals : goals ? [goals] : []} }); } //sends message for errorGoals diff --git a/src/webviews/goalviews/logbook.ts b/src/webviews/goalviews/logbook.ts index 2e24f252..362a79c3 100644 --- a/src/webviews/goalviews/logbook.ts +++ b/src/webviews/goalviews/logbook.ts @@ -17,7 +17,7 @@ export class Logbook extends GoalsBase { this.on(WebviewEvents.change, () => { if (this.state === WebviewState.visible) { // when the panel is visible or focused the messages are sent - this.postMessage({ type: MessageType.renderGoals, body: this.messages }); + this.postMessage({ type: MessageType.renderGoalsLegacy, body: this.messages }); } }); } @@ -27,7 +27,7 @@ export class Logbook extends GoalsBase { if (!goals) return; this.messages.push(goals); this.activatePanel(); - this.postMessage({ type: MessageType.renderGoals, body: this.messages }); + this.postMessage({ type: MessageType.renderGoalsLegacy, body: this.messages }); } } diff --git a/views/debug/Debug.tsx b/views/debug/Debug.tsx index cd0e14d5..3d677dbf 100644 --- a/views/debug/Debug.tsx +++ b/views/debug/Debug.tsx @@ -1,6 +1,6 @@ import React, { Suspense, lazy, useEffect, useState } from "react"; -import { GoalAnswer, PpString } from "../../lib/types"; +import { GoalAnswer, HypVisibility, PpString } from "../../lib/types"; import { ErrorBrowser } from "../goals/ErrorBrowser"; import { Goals } from "../goals/Goals"; import { Hypothesis } from "./Hypothesis"; @@ -22,9 +22,9 @@ export function Debug() { //handles the message //event : CoqMessageEvent as defined above function infoViewDispatch(msg: Message) { - if (msg.type === MessageType.renderGoals) { + if (msg.type === MessageType.renderGoalsLegacy) { // most important case that actually get the information - setGoals(msg.body); + setGoals(msg.body as GoalAnswer); } } @@ -45,8 +45,9 @@ export function Debug() { return (
+ - +
{!goals.error ? null : ( diff --git a/views/goals/Goals.tsx b/views/goals/Goals.tsx index 4d1b1e17..4f8fd87d 100644 --- a/views/goals/Goals.tsx +++ b/views/goals/Goals.tsx @@ -1,7 +1,7 @@ import $ from "jquery"; import { PropsWithChildren, useLayoutEffect, useRef } from "react"; import { FormatPrettyPrint } from "../../lib/format-pprint/js/main"; -import { Goal, GoalConfig, PpString } from "../../lib/types"; +import { convertToString, Goal, GoalConfig, Hyp, HypVisibility, PpString } from "../../lib/types"; import { Box } from "./Box"; import { CoqPp } from "./CoqPp"; @@ -11,11 +11,13 @@ import { } from "vscode-languageserver-types"; import "../styles/goals.css"; + + //type that contains Goal and an ID for that goal -type GoalP = { goal: Goal; idx: number; }; +type GoalP = { goal: Goal; idx: number; visibility : HypVisibility}; //component to display a single goal as a Pp string -function Goal({ goal }: GoalP) { +function Goal({ goal, visibility }: GoalP) { // https://beta.reactjs.org/learn/manipulating-the-dom-with-refs const ref: React.LegacyRef | null = useRef(null); const tyRef: React.LegacyRef | null = useRef(null); @@ -26,10 +28,26 @@ function Goal({ goal }: GoalP) { } }); + const hyps : () => Array> = () => { + const hyps : Array> = []; + if (visibility === HypVisibility.All) { + hyps.push(...goal.hyps); + } else if (visibility === HypVisibility.Limited) { + hyps.push( + ...goal.hyps + .map (hyp => + // Filter out all names starting with an underscore + ({ ...hyp, names: hyp.names.filter(name => convertToString(name).charAt(0) !== "_") })) + .filter(hyp => hyp.names.length > 0) + ); + } + return hyps; + } + return (
- {goal.hyps.map((hyp, _) => + {hyps().map((hyp, _) =>
:
)} @@ -47,6 +65,7 @@ type GoalsListP = PropsWithChildren<{ bullet_msg?: PpString; pos: Position textDoc: VersionedTextDocumentIdentifier + visibility: HypVisibility }>; //function that displays a list of goals if it exists @@ -56,7 +75,8 @@ function GoalsList({ show_on_empty, bullet_msg, pos, - textDoc + textDoc, + visibility }: GoalsListP) { const count = goals.length; @@ -77,7 +97,7 @@ function GoalsList({ } else if (count == 1) { return ( - + ); //Numerous goals, only the first goal is displayed, the other goals are hidden. @@ -85,7 +105,7 @@ function GoalsList({ return (
- + @@ -132,11 +152,12 @@ type StackSummaryP = PropsWithChildren<{ stack: [Goal[], Goal[]][]; pos: Position textDoc: VersionedTextDocumentIdentifier + visibility: HypVisibility }>; //goals can have multiple layers which are stacked. //the following function handles the display of stacked goals -function StackGoals({ idx, stack, pos, textDoc }: StackSummaryP) { +function StackGoals({ idx, stack, pos, textDoc, visibility }: StackSummaryP) { const count = stack.length; if (count <= idx) return null; @@ -153,6 +174,7 @@ function StackGoals({ idx, stack, pos, textDoc }: StackSummaryP) { show_on_empty={false} pos={pos} textDoc={textDoc} + visibility={visibility} />
); @@ -166,17 +188,18 @@ function StackGoals({ idx, stack, pos, textDoc }: StackSummaryP) { show_on_empty={false} pos={pos} textDoc={textDoc} + visibility={visibility} />
) } } //type that has goals, position and textdocument and takes its children -type GoalsParams = PropsWithChildren<{ goals?: GoalConfig, pos: Position, textDoc: VersionedTextDocumentIdentifier }>; +type GoalsParams = PropsWithChildren<{ goals?: GoalConfig, pos: Position, textDoc: VersionedTextDocumentIdentifier, visibility: HypVisibility }>; //the component that is used by other components //uses both the stackgoals for the goals at different levels and the GoalsList for the goals that consist of a list -export function Goals({ goals, pos, textDoc }: GoalsParams) { +export function Goals({ goals, pos, textDoc, visibility }: GoalsParams) { //if there are no goals, the user is shown "No goals at this point." if (!goals) { return @@ -197,10 +220,11 @@ export function Goals({ goals, pos, textDoc }: GoalsParams) { bullet_msg={goals.bullet} pos={pos} textDoc={textDoc} + visibility={visibility} /> {/* stacking goals that are on different levels */}
- +
{/* a list for the goals that are on the shelf */} @@ -210,6 +234,7 @@ export function Goals({ goals, pos, textDoc }: GoalsParams) { show_on_empty={false} // these goals are not shown if they do not exist pos={pos} textDoc={textDoc} + visibility={visibility} /> {/* a list for the goals that are given up */}
@@ -234,6 +260,7 @@ export function Goals({ goals, pos, textDoc }: GoalsParams) { bullet_msg={goals.bullet} pos={pos} textDoc={textDoc} + visibility={visibility} />
@@ -244,6 +271,7 @@ export function Goals({ goals, pos, textDoc }: GoalsParams) { show_on_empty={false} // these goals are not shown if they do not exist pos={pos} textDoc={textDoc} + visibility={visibility} /> {/* a list for the goals that are given up */}
diff --git a/views/goals/Info.tsx b/views/goals/Info.tsx index 130553ea..c878b66b 100644 --- a/views/goals/Info.tsx +++ b/views/goals/Info.tsx @@ -1,10 +1,11 @@ import { Suspense, lazy, useEffect, useState } from "react"; -import { GoalAnswer, PpString } from "../../lib/types"; +import { GoalAnswer, HypVisibility, PpString } from "../../lib/types"; import { ErrorBrowser } from "./ErrorBrowser"; import { Goals } from "./Goals"; import { Messages } from "./Messages"; + import "../styles/info.css"; import { Message, MessageType } from "../../shared"; @@ -17,16 +18,20 @@ const VSCodeDivider = lazy(async () => { export function InfoPanel() { + // visibility of the hypotheses in the goals panel + //saves the goal const [goals, setGoals] = useState>(); //boolean to check if the goals are still loading const [isLoading, setIsLoading] = useState(false); + //visibility of the hypotheses in the goals panel as State + const [visibility, setVisibility] = useState(HypVisibility.None); //handles the message //event : CoqMessageEvent as defined above function infoViewDispatch(msg: Message) { // TODO: make this change in logbook as well if (msg.type === MessageType.renderGoals) { - const goals = msg.body; + const goals = msg.body.goals; // FIXME: The `renderGoals` message type is currently overloaded and used for very different // functions. This can easily be seen when using global search on `MessageType.renderGoals`. @@ -34,6 +39,7 @@ export function InfoPanel() { //@ts-expect-error FIXME: `goals` should be properly typed instead of relying on `unknown`. setGoals(goals); //setting the information setIsLoading(false); + setVisibility(msg.body.visibility ?? HypVisibility.None); //set visibility if it exists, otherwise set to None } } @@ -56,7 +62,7 @@ export function InfoPanel() { return (
- +
{!goals.error ? null : ( diff --git a/views/logbook/Logbook.tsx b/views/logbook/Logbook.tsx index 01e031ad..dde2929d 100644 --- a/views/logbook/Logbook.tsx +++ b/views/logbook/Logbook.tsx @@ -16,7 +16,7 @@ export function Logbook() { //message handler function infoViewDispatch(msg: Message) { - if (msg.type === MessageType.renderGoals) { + if (msg.type === MessageType.renderGoalsLegacy) { //@ts-expect-error FIXME: renderGoals body is currently unknown. setGoalsArray(msg.body); //setting the information setIsLoading(false); //putting loading to false From 6ca615af27bba1b75151cdc2005b4f4f0bbb5134 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Mon, 7 Jul 2025 07:39:10 +0200 Subject: [PATCH 41/93] Work in progress --- shared/Messages.ts | 6 +++--- src/helpers/config-helper.ts | 14 ++++++++++++-- src/webviews/goalviews/goalsBase.ts | 8 ++++++-- src/webviews/goalviews/logbook.ts | 4 ++-- views/debug/Debug.tsx | 6 +++--- views/goals/Info.tsx | 4 ---- views/logbook/Logbook.tsx | 5 ++--- 7 files changed, 28 insertions(+), 19 deletions(-) diff --git a/shared/Messages.ts b/shared/Messages.ts index eafb00c3..5ac8073f 100644 --- a/shared/Messages.ts +++ b/shared/Messages.ts @@ -39,8 +39,8 @@ export type Message = | MessageBase | MessageBase | MessageBase - | MessageBase[], visibility?: HypVisibility }> - | MessageBase + | MessageBase, visibility?: HypVisibility }> + | MessageBase[]}> | MessageBase | MessageBase | MessageBase > @@ -68,7 +68,7 @@ export const enum MessageType { qedStatus, ready, renderGoals, - renderGoalsLegacy, + renderGoalsList, response, setAutocomplete, setData, diff --git a/src/helpers/config-helper.ts b/src/helpers/config-helper.ts index ddcee422..c57e5062 100644 --- a/src/helpers/config-helper.ts +++ b/src/helpers/config-helper.ts @@ -1,4 +1,5 @@ import { workspace } from "vscode"; +import { HypVisibility } from "../../lib/types"; export class WaterproofConfigHelper { @@ -77,8 +78,17 @@ export class WaterproofConfigHelper { /** `waterproof.hyp_visibility` */ - static get hyp_visibility() { - return config().get<"all" | "limited" | "none">; + static get hyp_visibility() : HypVisibility { + const hypVisibility = config().get("hyp_visibility"); + switch(hypVisibility) { + case "all": + return HypVisibility.All; + case "limited": + return HypVisibility.Limited; + case "none": + default: + return HypVisibility.None; + } } /** `waterproof.trace.server` */ static get trace_server() { diff --git a/src/webviews/goalviews/goalsBase.ts b/src/webviews/goalviews/goalsBase.ts index b01b3bb7..d8bda1be 100644 --- a/src/webviews/goalviews/goalsBase.ts +++ b/src/webviews/goalviews/goalsBase.ts @@ -1,9 +1,10 @@ import { Uri } from "vscode"; -import { GoalAnswer, PpString } from "../../../lib/types"; +import { GoalAnswer, HypVisibility, PpString } from "../../../lib/types"; import { MessageType } from "../../../shared"; import { IGoalsComponent } from "../../components"; import { CoqLspClientConfig } from "../../lsp-client/clientTypes"; import { CoqWebview } from "../coqWebview"; +import { WaterproofConfigHelper } from "../../helpers"; //class for panels that need Goals objects from coq-lsp export abstract class GoalsBase extends CoqWebview implements IGoalsComponent { @@ -17,7 +18,10 @@ export abstract class GoalsBase extends CoqWebview implements IGoalsComponent { //sends message for renderGoals updateGoals(goals: GoalAnswer | undefined) { - this.postMessage({ type: MessageType.renderGoals, body: {goals : goals ? [goals] : []} }); + if (goals) { + const visibility = WaterproofConfigHelper.hyp_visibility; + this.postMessage({ type: MessageType.renderGoals, body: {goals, visibility } }); + } } //sends message for errorGoals diff --git a/src/webviews/goalviews/logbook.ts b/src/webviews/goalviews/logbook.ts index 362a79c3..b9488c32 100644 --- a/src/webviews/goalviews/logbook.ts +++ b/src/webviews/goalviews/logbook.ts @@ -17,7 +17,7 @@ export class Logbook extends GoalsBase { this.on(WebviewEvents.change, () => { if (this.state === WebviewState.visible) { // when the panel is visible or focused the messages are sent - this.postMessage({ type: MessageType.renderGoalsLegacy, body: this.messages }); + this.postMessage({ type: MessageType.renderGoalsList, body: {goalsList: this.messages }}); } }); } @@ -27,7 +27,7 @@ export class Logbook extends GoalsBase { if (!goals) return; this.messages.push(goals); this.activatePanel(); - this.postMessage({ type: MessageType.renderGoalsLegacy, body: this.messages }); + this.postMessage({ type: MessageType.renderGoalsList, body: {goalsList: this.messages }}); } } diff --git a/views/debug/Debug.tsx b/views/debug/Debug.tsx index 3d677dbf..3f7e740b 100644 --- a/views/debug/Debug.tsx +++ b/views/debug/Debug.tsx @@ -22,9 +22,9 @@ export function Debug() { //handles the message //event : CoqMessageEvent as defined above function infoViewDispatch(msg: Message) { - if (msg.type === MessageType.renderGoalsLegacy) { + if (msg.type === MessageType.renderGoals) { // most important case that actually get the information - setGoals(msg.body as GoalAnswer); + setGoals(msg.body.goals as GoalAnswer); } } @@ -47,7 +47,7 @@ export function Debug() {
- +
{!goals.error ? null : ( diff --git a/views/goals/Info.tsx b/views/goals/Info.tsx index c878b66b..9e1d0c52 100644 --- a/views/goals/Info.tsx +++ b/views/goals/Info.tsx @@ -33,10 +33,6 @@ export function InfoPanel() { if (msg.type === MessageType.renderGoals) { const goals = msg.body.goals; - // FIXME: The `renderGoals` message type is currently overloaded and used for very different - // functions. This can easily be seen when using global search on `MessageType.renderGoals`. - // This should be changed. - //@ts-expect-error FIXME: `goals` should be properly typed instead of relying on `unknown`. setGoals(goals); //setting the information setIsLoading(false); setVisibility(msg.body.visibility ?? HypVisibility.None); //set visibility if it exists, otherwise set to None diff --git a/views/logbook/Logbook.tsx b/views/logbook/Logbook.tsx index dde2929d..01554335 100644 --- a/views/logbook/Logbook.tsx +++ b/views/logbook/Logbook.tsx @@ -16,9 +16,8 @@ export function Logbook() { //message handler function infoViewDispatch(msg: Message) { - if (msg.type === MessageType.renderGoalsLegacy) { - //@ts-expect-error FIXME: renderGoals body is currently unknown. - setGoalsArray(msg.body); //setting the information + if (msg.type === MessageType.renderGoalsList) { + setGoalsArray(msg.body.goalsList); //setting the information setIsLoading(false); //putting loading to false } } From a7bcae69cae604d533684a0245c2074e5ac808da Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:21:05 +0200 Subject: [PATCH 42/93] Revert back to using inline code for input area The abstraction `inInputArea` breaks the logic of input areas, making input areas not editable. Some more work needs to be done here to get the abstraction working correctly, I suspect the `this` binding is wrong. --- editor/src/kroqed-editor/codeview/nodeview.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/editor/src/kroqed-editor/codeview/nodeview.ts b/editor/src/kroqed-editor/codeview/nodeview.ts index e142514c..119d4893 100644 --- a/editor/src/kroqed-editor/codeview/nodeview.ts +++ b/editor/src/kroqed-editor/codeview/nodeview.ts @@ -76,10 +76,11 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { return div; } + // Makes sure that we only enable the linting gutter for codecells inside of input areas. const inInputArea = this.partOfInputArea(); const optional = inInputArea ? [lintGutter()] : []; - // eslint-disable-next-line @typescript-eslint/no-this-alias - const shadowThis = this; + + this._codemirror = new CodeMirror({ doc: this._node.textContent, extensions: [ @@ -131,7 +132,16 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { } if (locked) { - if (shadowThis.partOfInputArea()) return; + // in student mode. + // TODO: This code is essentially a duplicate of the inInputArea function, but using + // inInputArea here does not work out of the box. Maybe the `this` binding is wrong? + const pos = getPos(); + if (pos === undefined) return; + // Resolve the position in the prosemirror document and get the node one level underneath the root. + // TODO: Assumption that ``s only ever appear one level beneath the root node. + // TODO: Hardcoded node names. + const name = outerView.state.doc.resolve(pos).node(1).type.name; + if (name !== "input") return; // This node is not part of an input area. } view.update([tr]); From f478e2c1768227622437f591242f19f860d24001 Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:31:15 +0200 Subject: [PATCH 43/93] Remove input area from line numbers test Input area is not needed for this test and breaks the test since the input area creates a gutter element. --- cypress/e2e/line-numbers.cy.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/line-numbers.cy.ts b/cypress/e2e/line-numbers.cy.ts index 4e1c26d1..a9ef935d 100644 --- a/cypress/e2e/line-numbers.cy.ts +++ b/cypress/e2e/line-numbers.cy.ts @@ -4,7 +4,7 @@ import { MessageType } from "../../shared"; import { setupTest } from "./util"; const edits = []; -const initialDocument = "# Title\n\n```coq\nDefinition foo := 42.\n```\n\n"; +const initialDocument = "# Title\n```coq\nDefinition foo := 42.\n```\n"; const callbacks = { [MessageType.lineNumbers]: (data: { linenumbers: number[]; }) => { @@ -29,7 +29,9 @@ describe('Line numbers', () => { beforeEach(() => { setupTest(initialDocument, edits, callbacks); }); it("Toggle display of line numbers", () => { + // CodeMirror Editor should exist cy.get('.cm-content').should("exist"); + // The line number gutter should not exist cy.get('.cm-gutter').should("not.exist"); cy.window().then((win) => { win.postMessage({ @@ -39,7 +41,8 @@ describe('Line numbers', () => { }); cy.wait(1); cy.get('.cm-gutter').should("exist"); - cy.get('.cm-gutter').should("contain", "4"); + // Third line contains the coq code + cy.get('.cm-gutter').should("contain", "3"); cy.window().then((win) => { win.postMessage({ type: MessageType.setShowLineNumbers, From 93a921fa2f37e21fbdc632a934c428d71d4441e6 Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:48:21 +0200 Subject: [PATCH 44/93] Add cypress test for error box --- cypress/e2e/with-mock-messages.cy.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/with-mock-messages.cy.ts b/cypress/e2e/with-mock-messages.cy.ts index 85e9e721..48d96e9a 100644 --- a/cypress/e2e/with-mock-messages.cy.ts +++ b/cypress/e2e/with-mock-messages.cy.ts @@ -4,6 +4,8 @@ import { Message, MessageType } from "../../shared/Messages"; const edits = []; +const diagMessage = " = 6\n : nat"; + const messages: Array = [ { "type": MessageType.diagnostics, @@ -37,7 +39,7 @@ const messages: Array = [ "body": { "positionedDiagnostics": [ { - "message": " = 6\n : nat", + "message": diagMessage, "severity": 2, "startOffset": 20, "endOffset": 34 @@ -100,8 +102,14 @@ describe('TestingTest', () => { }); it("Displays Info Message", () => { - cy.get('.cm-lintRange').click().trigger('mouseover'); - cy.get('.cm-diagnostic').as("diag").should('be.visible').should('contain.text', '= 6'); + cy.get('.cm-lintRange.cm-lintRange-info').click().trigger('mouseover'); + cy.get('.cm-diagnostic').as("diag").should('be.visible').should('contain.text', diagMessage); + }); + it("Displays the error box underneath", () => { + cy.get('.cm-lintRange.cm-lintRange-info').click().trigger('mouseover'); + // Make sure we do have an input area + cy.get("waterproofinput").should("exist"); + cy.get("waterproofinput").get('.cm-diagnostic').as("diag").should('be.visible').should('contain.text', diagMessage); }); }) \ No newline at end of file From 767fa44c6bdc61f06468412c758ade24cf0141a5 Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:00:47 +0200 Subject: [PATCH 45/93] Update tactic templates with placeholder at end of line. This allows the user to use tab to jump to the end of the line when all the placeholders have been filled in. I also took the liberty to update the template of the contradiction tactic from `Contradiction` to `Contradiction.`. --- shared/completions/tactics.json | 94 ++++++++++++++++----------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/shared/completions/tactics.json b/shared/completions/tactics.json index d00862b2..0fc5bc50 100644 --- a/shared/completions/tactics.json +++ b/shared/completions/tactics.json @@ -3,7 +3,7 @@ "label": "Help.", "type": "type", "detail": "tactic", - "template": "Help.", + "template": "Help.${}", "description": "Tries to give you a hint on what to do next.", "example": "Lemma example_help :\n 0 = 0.\nProof.\nHelp.\nWe conclude that (0 = 0).\nQed.", "boost": 2 @@ -12,7 +12,7 @@ "label": "Take (*name*) : ((*type*)).", "type": "type", "detail": "tactic", - "template": "Take ${x} : (${ℝ}).", + "template": "Take ${x} : (${ℝ}).${}", "description": "Take an arbitrary element from (*type*) and call it (*name*).", "example": "Lemma example_take :\n for all x : ℝ,\n x = x.\nProof.\nTake x : (ℝ).\nWe conclude that (x = x).\nQed.", "boost": 1 @@ -21,7 +21,7 @@ "label": "Take (*name*) ∈ ((*set*)).", "type": "type", "detail": "tactic", - "template": "Take ${x} ∈ (${ℝ}).", + "template": "Take ${x} ∈ (${ℝ}).${}", "description": "Take an arbitrary element from (*set*) and call it (*name*).", "example": "Lemma example_take :\n ∀ x ∈ ℝ,\n x = x.\nProof.\nTake x ∈ (ℝ).\nWe conclude that (x = x).\nQed.", "boost": 2 @@ -30,7 +30,7 @@ "label": "Take (*name*) > ((*number*)).", "type": "type", "detail": "tactic", - "template": "Take ${0:x} > (${1:0}).", + "template": "Take ${0:x} > (${1:0}).${}", "description": "Take an arbitrary element larger than (*number*) and call it (*name*).", "example": "Lemma example_take :\n ∀ x > 3,\n x = x.\nProof.\nTake x > (3).\nWe conclude that (x = x).\nQed.", "boost": 2 @@ -39,7 +39,7 @@ "label": "Take (*name*) ≥ ((*number*)).", "type": "type", "detail": "tactic", - "template": "Take ${0:x} ≥ (${1:0}).", + "template": "Take ${0:x} ≥ (${1:0}).${}", "description": "Take an arbitrary element larger than or equal to (*number*) and call it (*name*).", "example": "Lemma example_take :\n ∀ x ≥ 5,\n x = x.\nProof.\nTake x ≥ (5).\nWe conclude that (x = x).\nQed.", "boost": 2 @@ -48,7 +48,7 @@ "label": "We need to show that ((*(alternative) formulation of current goal*)).", "type": "type", "detail": "tactic", - "template": "We need to show that (${0 = 0}).", + "template": "We need to show that (${0 = 0}).${}", "description": "Generally makes a proof more readable. Has the additional functionality that you can write a slightly different but equivalent formulation of the goal: you can for instance change names of certain variables.", "example": "Lemma example_we_need_to_show_that :\n 0 = 0.\nProof.\nWe need to show that (0 = 0).\nWe conclude that (0 = 0).\nQed.", "boost": 2 @@ -57,7 +57,7 @@ "label": "We conclude that ((*current goal*)).", "type": "type", "detail": "tactic", - "template": "We conclude that (${0 = 0}).", + "template": "We conclude that (${0 = 0}).${}", "description": "Tries to automatically prove the current goal.", "example": "Lemma example_we_conclude_that :\n 0 = 0.\nProof.\nWe conclude that (0 = 0).\nQed." }, @@ -65,7 +65,7 @@ "label": "We conclude that ((*(alternative) formulation of current goal*)).", "type": "type", "detail": "tactic", - "template": "We conclude that (${0 = 0}).", + "template": "We conclude that (${0 = 0}).${}", "description": "Tries to automatically prove the current goal.", "example": "Lemma example_we_conclude_that :\n 0 = 0.\nProof.\nWe conclude that (0 = 0).\nQed.", "boost": 1 @@ -74,7 +74,7 @@ "label": "Choose (*name_var*) := ((*expression*)).", "type": "type", "detail": "tactic", - "template": "Choose ${0:x} := (${1:0}).", + "template": "Choose ${0:x} := (${1:0}).${}", "description": "You can use this tactic when you need to show that there exists an x such that a certain property holds. You do this by proposing (*expression*) as a choice for x, giving it the name (*name_var*).", "boost": 2, "example": "Lemma example_choose :\n ∃ y ∈ ℝ,\n y < 3.\nProof.\nChoose y := (2).\n* Indeed, (y ∈ ℝ).\n* We conclude that (y < 3).\nQed." @@ -83,7 +83,7 @@ "label": "Assume that ((*statement*)).", "type": "type", "detail": "tactic", - "template": "Assume that (${0 = 0}).", + "template": "Assume that (${0 = 0}).${}", "description": "If you need to prove (*statement*) ⇒ B, this allows you to assume that (*statement*) holds.", "boost": 2, "example": "Lemma example_assume :\n ∀ a ∈ ℝ, a < 0 ⇒ - a > 0.\nProof.\nTake a ∈ (ℝ).\nAssume that (a < 0).\nWe conclude that (- a > 0).\nQed." @@ -92,7 +92,7 @@ "label": "Assume that ((*statement*)) ((*label*)).", "type": "type", "detail": "tactic", - "template": "Assume that (${0 = 0}) (${i}).", + "template": "Assume that (${0 = 0}) (${i}).${}", "description": "If you need to prove (*statement*) ⇒ B, this allows you to assume that (*statement*) holds, giving it the label (*label*). You can leave out ((*label*)) if you don't wish to name your assumption.", "boost": 1, "example": "Lemma example_assume :\n ∀ a ∈ ℝ, a < 0 ⇒ - a > 0.\nProof.\nTake a ∈ (ℝ).\nAssume that (a < 0) (a_less_than_0).\nWe conclude that (- a > 0).\nQed." @@ -101,7 +101,7 @@ "label": "(& 3 < 5 = 2 + 3 ≤ 7) (chain of (in)equalities, with opening parenthesis)", "type": "type", "detail": "tactic", - "template": "(& ${3 < 5 = 2 + 3 ≤ 7}", + "template": "(& ${3 < 5 = 2 + 3 ≤ 7}${}", "description": "Example of a chain of (in)equalities in which every inequality should.", "example": "Lemma example_inequalities :\n ∀ ε > 0, Rmin(ε,1) < 2.\nProof.\nTake ε > 0.\nWe conclude that (& Rmin(ε,1) ≤ 1 < 2).\nQed." }, @@ -109,7 +109,7 @@ "label": "& 3 < 5 = 2 + 3 ≤ 7 (chain of (in)equalities)", "type": "type", "detail": "tactic", - "template": "& ${3 < 5 = 2 + 3 ≤ 7}", + "template": "& ${3 < 5 = 2 + 3 ≤ 7}${}", "description": "Example of a chain of (in)equalities in which every inequality should.", "example": "Lemma example_inequalities :\n ∀ ε > 0, Rmin(ε,1) < 2.\nProof.\nTake ε : (ℝ).\nAssume that (ε > 0).\nWe conclude that (& Rmin(ε,1) ≤ 1 < 2).\nQed." }, @@ -117,7 +117,7 @@ "label": "Obtain such a (*name_var*)", "type": "type", "detail": "tactic", - "template": "Obtain such a ${k}.", + "template": "Obtain such a ${k}.${}", "description": "In case a hypothesis that you just proved starts with 'there exists' s.t. some property holds, then you can use this tactic to select such a variable. The variable will be named (*name_var*).", "boost": 2, "example": "Lemma example_obtain :\n ∀ x ∈ ℝ,\n (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒\n 10 < x.\nProof.\nTake x ∈ (ℝ).\nAssume that (∃ y ∈ ℝ, 10 < y ∧ y < x) (i).\nObtain such a y.\nQed." @@ -126,7 +126,7 @@ "label": "Obtain (*name_var*) according to ((*name_hyp*)).", "type": "type", "detail": "tactic", - "template": "Obtain ${k} according to (${i}).", + "template": "Obtain ${k} according to (${i}).${}", "description": "In case the hypothesis with name (*name_hyp*) starts with 'there exists' s.t. some property holds, then you can use this tactic to select such a variable. The variable will be named (*name_var*).", "boost": 2, "example": "Lemma example_obtain :\n ∀ x ∈ ℝ,\n (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒\n 10 < x.\nProof.\nTake x ∈ (ℝ).\nAssume that (∃ y ∈ ℝ, 10 < y ∧ y < x) (i).\nObtain y according to (i).\nQed." @@ -135,7 +135,7 @@ "label": "It suffices to show that ((*statement*)).", "type": "type", "detail": "tactic", - "template": "It suffices to show that (${0 = 0}).", + "template": "It suffices to show that (${0 = 0}).${}", "description": "Waterproof tries to verify automatically whether it is indeed enough to show (*statement*) to prove the current goal. If so, (*statement*) becomes the new goal.", "boost": 2, "example": "Lemma example_it_suffices_to_show_that :\n ∀ ε > 0,\n 3 + Rmax(ε,2) ≥ 3.\nProof.\nTake ε > 0.\nIt suffices to show that (Rmax(ε,2) ≥ 0).\nWe conclude that (& Rmax(ε,2) ≥ 2 ≥ 0).\nQed.", @@ -146,7 +146,7 @@ "type": "type", "detail": "tactic", "description": "Waterproof tries to verify automatically whether it is indeed enough to show (*statement*) to prove the current goal, using (*lemma or assumption*). If so, (*statement*) becomes the new goal.", - "template": "By (${i}) it suffices to show that (${0 = 0}).", + "template": "By (${i}) it suffices to show that (${0 = 0}).${}", "boost": 2, "example": "Lemma example_it_suffices_to_show_that :\n ∀ ε ∈ ℝ,\n ε > 0 ⇒\n 3 + Rmax(ε,2) ≥ 3.\nProof.\nTake ε ∈ (ℝ).\nAssume that (ε > 0) (i).\nBy (i) it suffices to show that (Rmax(ε,2) ≥ 0).\nWe conclude that (& Rmax(ε,2) ≥ 2 ≥ 0).\nQed.", "advanced": false @@ -155,7 +155,7 @@ "label": "It holds that ((*statement*)) ((*label*)).", "type": "type", "detail": "tactic", - "template": "It holds that (${0 = 0}) (${i}).", + "template": "It holds that (${0 = 0}) (${i}).${}", "description": "Tries to automatically prove (*statement*). If that works, (*statement*) is added as a hypothesis with name (*optional_label*).", "example": "Lemma example_it_holds_that :\n ∀ ε > 0,\n 4 - Rmax(ε,1) ≤ 3.\n \nProof.\nTake ε > 0.\nIt holds that (Rmax(ε,1) ≥ 1) (i).\nWe conclude that (4 - Rmax(ε,1) ≤ 3).\nQed.", "boost": 1 @@ -164,7 +164,7 @@ "label": "It holds that ((*statement*)).", "type": "type", "detail": "tactic", - "template": "It holds that (${0 = 0}).", + "template": "It holds that (${0 = 0}).${}", "description": "Tries to automatically prove (*statement*). If that works, (*statement*) is added as a hypothesis.", "example": "Lemma example_it_holds_that :\n ∀ ε > 0,\n 4 - Rmax(ε,1) ≤ 3.\n \nProof.\nTake ε > 0.\nIt holds that (Rmax(ε,1) ≥ 1).\nWe conclude that (4 - Rmax(ε,1) ≤ 3).\nQed.", "boost": 2 @@ -173,7 +173,7 @@ "label": "By ((*lemma or assumption*)) it holds that ((*statement*)) ((*label*)).", "type": "type", "detail": "tactic", - "template": "By (${i}) it holds that (${0 = 0}) (${ii}).", + "template": "By (${i}) it holds that (${0 = 0}) (${ii}).${}", "description": "Tries to prove (*statement*) using (*lemma*) or (*assumption*). If that works, (*statement*) is added as a hypothesis with name (*optional_label*). You can leave out ((*optional_label*)) if you don't wish to name the statement.", "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that (7 < f(-1)).\nBy (f_increasing) it holds that (f(-1) ≤ f(6)) (i).\nWe conclude that (2 < f(6)).\nQed.", "boost": 1 @@ -182,7 +182,7 @@ "label": "By ((*lemma or assumption*)) it holds that ((*statement*)).", "type": "type", "detail": "tactic", - "template": "By (${i}) it holds that (${0 = 0}).", + "template": "By (${i}) it holds that (${0 = 0}).${}", "description": "Tries to prove (*statement*) using (*lemma*) or (*assumption*). If that works, (*statement*) is added as a hypothesis with name (*optional_label*). You can leave out ((*optional_label*)) if you don't wish to name the statement.", "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that (7 < f(-1)).\nBy (f_increasing) it holds that (f(-1) ≤ f(6)) (i).\nWe conclude that (2 < f(6)).\nQed.", "boost": 2 @@ -191,7 +191,7 @@ "label": "We claim that ((*statement*)).", "type": "type", "detail": "tactic", - "template": "We claim that (${0 = 0}).", + "template": "We claim that (${0 = 0}).${}", "description": "Lets you first show (*statement*) before continuing with the rest of the proof. After you showed (*statement*), it will be available as a hypothesis with name (*optional_name*).", "example": "We claim that (2 = 2) (two_is_two).", "boost": 2 @@ -200,7 +200,7 @@ "label": "We claim that ((*statement*)) ((*label*)).", "type": "type", "detail": "tactic", - "template": "We claim that (${0 = 0}) (${i}).", + "template": "We claim that (${0 = 0}) (${i}).${}", "description": "Lets you first show (*statement*) before continuing with the rest of the proof. After you showed (*statement*), it will be available as a hypothesis with name (*label*).", "example": "We claim that (2 = 2) (two_is_two).", "boost": 1 @@ -209,7 +209,7 @@ "label": "We argue by contradiction.", "type": "type", "detail": "tactic", - "template": "We argue by contradiction.", + "template": "We argue by contradiction.${}", "description": "Assumes the opposite of what you need to show. Afterwards, you need to make substeps that show that both A and ¬ A (i.e. not A) for some statement A. Finally, you can finish your proof with 'Contradiction.'", "example": "Lemma example_contradicition :\n ∀ x ∈ ℝ,\n (∀ ε > 0, x > 1 - ε) ⇒\n x ≥ 1.\nProof.\nTake x ∈ (ℝ).\nAssume that (∀ ε > 0, x > 1 - ε) (i).\nWe need to show that (x ≥ 1).\nWe argue by contradiction.\nAssume that (¬ (x ≥ 1)).\nIt holds that ((1 - x) > 0).\nBy (i) it holds that (x > 1 - (1 - x)).\nContradiction.\nQed.", "boost": 2 @@ -218,7 +218,7 @@ "label": "Contradiction", "type": "type", "detail": "tactic", - "template": "Contradiction", + "template": "Contradiction.${}", "description": "If you have shown both A and not A for some statement A, you can write \"Contradiction\" to finish the proof of the current goal.", "example": "Contradiction.", "boost": 2 @@ -227,7 +227,7 @@ "label": "Because ((*name_combined_hyp*)) both ((*statement_1*)) and ((*statement_2*)).", "type": "type", "detail": "tactic", - "template": "Because (${i}) both (${0 = 0}) and (${1 = 1}).", + "template": "Because (${i}) both (${0 = 0}) and (${1 = 1}).${}", "description": "If you currently have a hypothesis with name (*name_combined_hyp*) which is in fact of the form H1 ∧ H2, then this tactic splits it up in two separate hypotheses.", "example": "Lemma and_example : for all A B : Prop, A ∧ B ⇒ A.\nTake A : Prop. Take B : Prop.\nAssume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii).", "advanced": true, @@ -237,7 +237,7 @@ "label": "Because ((*name_combined_hyp*)) both ((*statement_1*)) ((*label_1*)) and ((*statement_2*)) ((*label_2*)).", "type": "type", "detail": "tactic", - "template": "Because (${i}) both (${0 = 0}) (${ii}) and (${1 = 1}) (${iii}).", + "template": "Because (${i}) both (${0 = 0}) (${ii}) and (${1 = 1}) (${iii}).${}", "description": "If you currently have a hypothesis with name (*name_combined_hyp*) which is in fact of the form H1 ∧ H2, then this tactic splits it up in two separate hypotheses.", "example": "Lemma and_example : for all A B : Prop, A ∧ B ⇒ A.\nTake A : Prop. Take B : Prop.\nAssume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii).", "advanced": true, @@ -247,7 +247,7 @@ "label": "Either ((*case_1*)) or ((*case_2*)).", "type": "type", "detail": "tactic", - "template": "Either (${0 = 1}) or (${0 ≠ 1}).", + "template": "Either (${0 = 1}) or (${0 ≠ 1}).${}", "description": "Split in two cases (*case_1*) and (*case_2*).", "example": "Lemma example_cases : \n ∀ x ∈ ℝ, ∀ y ∈ ℝ,\n Rmax(x,y) = x ∨ Rmax(x,y) = y.\nProof. \nTake x ∈ ℝ. Take y ∈ ℝ.\nEither (x < y) or (x ≥ y).\n- Case (x < y).\n It suffices to show that (Rmax(x,y) = y).\n We conclude that (Rmax(x,y) = y).\n- Case (x ≥ y).\n It suffices to show that (Rmax(x,y) = x).\n We conclude that (Rmax(x,y) = x).\nQed.", "boost": 2 @@ -256,7 +256,7 @@ "label": "Expand the definition of (*name_kw*).", "type": "type", "detail": "tactic", - "template": "Expand the definition of ${infimum}.", + "template": "Expand the definition of ${infimum}.${}", "description": "Expands the definition of the keyword (*name_kw*) in relevant statements in the proof, and gives suggestions on how to use them.", "example": "Expand the definition of upper bound.", "advanced": false, @@ -266,7 +266,7 @@ "label": "Expand the definition of (*name_kw*) in ((*expression*)).", "type": "type", "detail": "tactic", - "template": "Expand the definition of ${infimum} in (${0 = 0}).", + "template": "Expand the definition of ${infimum} in (${0 = 0}).${}", "description": "Expands the definition of the keyword (*name_kw*) in the statement (*expression*).", "example": "Expand the definition of upper bound in (4 is an upper bound for [0, 3)).", "advanced": false, @@ -276,7 +276,7 @@ "label": "We show both statements.", "type": "type", "detail": "tactic", - "template": "We show both statements.", + "template": "We show both statements.${}", "description": "Splits the goal in two separate goals, if it is of the form A ∧ B", "example": "Lemma example_both_statements:\n ∀ x ∈ ℝ, (x^2 ≥ 0) ∧ (| x | ≥ 0).\nProof.\nTake x ∈ (ℝ).\nWe show both statements.\n- We conclude that (x^2 ≥ 0).\n- We conclude that (| x | ≥ 0).\nQed.", "boost": 2 @@ -285,7 +285,7 @@ "label": "We show both directions.", "type": "type", "detail": "tactic", - "template": "We show both directions.", + "template": "We show both directions.${}", "description": "Splits a goal of the form A ⇔ B, into the goals (A ⇒ B) and (B ⇒ A)", "example": "Lemma example_both_directions:\n ∀ x ∈ ℝ, ∀ y ∈ ℝ,\n x < y ⇔ y > x.\nProof.\nTake x ∈ (ℝ). Take y ∈ (ℝ).\nWe show both directions.\n- We need to show that (x < y ⇒ y > x).\n Assume that (x < y).\n We conclude that (y > x).\n- We need to show that (y > x ⇒ x < y).\n Assume that (y > x).\n We conclude that (x < y).\nQed.", "boost": 2 @@ -294,7 +294,7 @@ "label": "We use induction on (*name_var*).", "type": "type", "detail": "tactic", - "template": "We use induction on ${n}.", + "template": "We use induction on ${n}.${}", "description": "Prove a statement by induction on the variable with (*name_var*).", "example": "Lemma example_induction :\n ∀ n : ℕ → ℕ, (∀ k ∈ ℕ, n(k) < n(k + 1))%nat ⇒\n ∀ k ∈ ℕ, (k ≤ n(k))%nat.\nProof.\nTake n : (ℕ → ℕ).\nAssume that (∀ k ∈ ℕ, n(k) < n(k + 1))%nat (i).\nWe use induction on k.\n- We first show the base case, namely (0 ≤ n(0))%nat.\n We conclude that (0 ≤ n(0))%nat.\n- We now show the induction step.\n Take k ∈ ℕ.\n Assume that (k ≤ n(k))%nat.\n By (i) it holds that (n(k) < n(k + 1))%nat.\n We conclude that (& k + 1 ≤ n(k) + 1 ≤ n(k + 1))%nat.\nQed.", "boost": 2 @@ -303,7 +303,7 @@ "label": "By ((*lemma or assumption*)) we conclude that ((*(alternative) formulation of current goal*)).", "type": "type", "detail": "tactic", - "template": "By (${i}) we conclude that (${0 = 0}).", + "template": "By (${i}) we conclude that (${0 = 0}).${}", "description": "Tries to directly prove the goal using (*lemma or assumption*) when the goal corresponds to (*statement*).", "boost": 2 }, @@ -311,7 +311,7 @@ "label": "Define (*name*) := ((*expression*)).", "type": "type", "detail": "tactic", - "template": "Define ${0:s} := (${1:0}).", + "template": "Define ${0:s} := (${1:0}).${}", "description": "Temporarily give the name (*name*) to the expression (*expression*)", "boost": 2 }, @@ -319,7 +319,7 @@ "label": "Since ((*extra_statement*)) it holds that ((*statement*)).", "type": "type", "detail": "tactic", - "template": "Since (${1 = 1}) it holds that (${0 = 0}).", + "template": "Since (${1 = 1}) it holds that (${0 = 0}).${}", "description": "Tries to first verify (*extra_statement*) after it uses that to verify (*statement*). The statement gets added as a hypothesis.", "example": "Since (x = y) it holds that (x = z).", "boost": 2 @@ -328,7 +328,7 @@ "label": "Since ((*extra_statement*)) it holds that ((*statement*)) ((*label*)).", "type": "type", "detail": "tactic", - "template": "Since (${1 = 1}) it holds that (${0 = 0}) (${i}).", + "template": "Since (${1 = 1}) it holds that (${0 = 0}) (${i}).${}", "description": "Tries to first verify (*extra_statement*) after it uses that to verify (*statement*). The statement gets added as a hypothesiwe need to show{s, optionally with the name (*optional_label*).", "example": "Since (x = y) it holds that (x = z).", "boost": 1 @@ -337,7 +337,7 @@ "label": "Since ((*extra_statement*)) we conclude that ((*(alternative) formulation of current goal*)).", "type": "type", "detail": "tactic", - "template": "Since (${1 = 1}) we conclude that (${0 = 0}).", + "template": "Since (${1 = 1}) we conclude that (${0 = 0}).${}", "description": "Tries to automatically prove the current goal, after first trying to prove (*extra_statement*).", "example": "Since (x = y) we conclude that (x = z).", "boost": 2 @@ -346,7 +346,7 @@ "label": "Since ((*extra_statement*)) it suffices to show that ((*statement*)).", "type": "type", "detail": "tactic", - "template": "Since (${1 = 1}) it suffices to show that (${0 = 0}).", + "template": "Since (${1 = 1}) it suffices to show that (${0 = 0}).${}", "description": "Waterproof tries to verify automatically whether it is indeed enough to show (*statement*) to prove the current goal, after first trying to prove (*extra_statement*). If so, (*statement*) becomes the new goal.", "example": "Lemma example_backwards :\n 3 < f(0) ⇒ 2 < f(5).\nProof.\nAssume that (3 < f(0)).\nIt suffices to show that (f(0) ≤ f(5)).\nBy (f_increasing) we conclude that (f(0) ≤ f(5)).\nQed.", "advanced": false, @@ -356,7 +356,7 @@ "label": "Use (*name*) := ((*expression*)) in ((*label*)).", "type": "type", "detail": "tactic", - "template": "Use ${0:x} := (${1:0}) in (${2:i}).", + "template": "Use ${0:x} := (${1:0}) in (${2:i}).${}", "description": "Use a forall statement, i.e. apply it to a particular expression.", "example": "Lemma example_use_for_all :\n ∀ x ∈ ℝ,\n (∀ ε > 0, x < ε) ⇒\n x + 1/2 < 1.\nProof.\nTake x ∈ ℝ.\nAssume that (∀ ε > 0, x < ε) (i).\nUse ε := (1/2) in (i).\n* Indeed, (1 / 2 > 0).\n* It holds that (x < 1 / 2).\n We conclude that (x + 1/2 < 1).\nQed.", "advanced": false, @@ -366,7 +366,7 @@ "label": "Indeed, ((*statement*)).", "type": "type", "detail": "tactic", - "template": "Indeed, (${0 = 0}).", + "template": "Indeed, (${0 = 0}).${}", "description": "A synonym for \"We conclude that ((*statement*))\".", "example": "Lemma example_choose :\n ∃ y ∈ ℝ,\n y < 3.\nProof.\nChoose y := (2).\n* Indeed, (y ∈ ℝ).\n* We conclude that (y < 3).\nQed.", "advanced": false, @@ -376,7 +376,7 @@ "label": "We need to verify that ((*statement*)).", "type": "type", "detail": "tactic", - "template": "We need to verify that (${0 = 0}).", + "template": "We need to verify that (${0 = 0}).${}", "description": "Used to indicate what to check after using the \"Choose\" or \"Use\" tactic.", "example": "Lemma example_choose :\n ∃ y ∈ ℝ,\n y < 3.\nProof.\nChoose y := (2).\n* We need to verify that (y ∈ ℝ).\nWe conclude that (y ∈ ℝ).\n* We conclude that (y < 3).\nQed.", "advanced": false, @@ -386,7 +386,7 @@ "label": "By magic it holds that ((*statement*)) ((*label*)).", "type": "type", "detail": "tactic", - "template": "By magic it holds that (${0 = 0}) (${i}).", + "template": "By magic it holds that (${0 = 0}) (${i}).${}", "description": "Postpones the proof of (*statement*), and (*statement*) is added as a hypothesis with name (*optional_label*). You can leave out ((*optional_label*)) if you don't wish to name the statement.", "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that (7 < f(-1)).\nBy magic it holds that (f(-1) ≤ f(6)) (i).\nWe conclude that (2 < f(6)).\nQed.", "boost": 1 @@ -395,7 +395,7 @@ "label": "By magic it holds that ((*statement*)).", "type": "type", "detail": "tactic", - "template": "By magic it holds that (${0 = 0}).", + "template": "By magic it holds that (${0 = 0}).${}", "description": "Postpones the proof of (*statement*), and (*statement*) is added as a hypothesis.", "example": "Lemma example_forwards :\n 7 < f(-1) ⇒ 2 < f(6).\nProof.\nAssume that (7 < f(-1)).\nBy magic it holds that (f(-1) ≤ f(6)) (i).\nWe conclude that (2 < f(6)).\nQed.", "boost": 2 @@ -404,7 +404,7 @@ "label": "By magic we conclude that ((*(alternative) formulation of current goal*)).", "type": "type", "detail": "tactic", - "template": "By magic we conclude that (${0 = 0}).", + "template": "By magic we conclude that (${0 = 0}).${}", "description": "Postpones for now the proof of (a possible alternative formulation of) the current goal.", "boost": 2 }, @@ -413,7 +413,7 @@ "type": "type", "detail": "tactic", "description": "Postpones for now the proof that (*statement*) is enough to prove the current goal. Now, (*statement*) becomes the new goal.", - "template": "By magic it suffices to show that (${0 = 0}).", + "template": "By magic it suffices to show that (${0 = 0}).${}", "boost": 2, "example": "Lemma example_backwards :\n 3 < f(0) ⇒ 2 < f(5).\nProof.\nAssume that (3 < f(0)).\nBy magic it suffices to show that (f(0) ≤ f(5)).\nBy (f_increasing) we conclude that (f(0) ≤ f(5)).\nQed.", "advanced": false @@ -422,7 +422,7 @@ "label": "Case ((*statement*)).", "type": "type", "detail": "tactic", - "template": "Case (${0 = 0}).", + "template": "Case (${0 = 0}).${}", "description": "Used to indicate the case after an \"Either\" sentence.", "example": "Lemma example_cases : \n ∀ x ∈ ℝ, ∀ y ∈ ℝ,\n Rmax(x,y) = x ∨ Rmax(x,y) = y.\nProof. \nTake x ∈ ℝ. Take y ∈ ℝ.\nEither (x < y) or (x ≥ y).\n- Case (x < y).\n It suffices to show that (Rmax(x,y) = y).\n We conclude that (Rmax(x,y) = y).\n- Case (x ≥ y).\n It suffices to show that (Rmax(x,y) = x).\n We conclude that (Rmax(x,y) = x).\nQed.", "boost": 1 From 30e2baef19e7d5e7226c5e570dbe7ffd1f45f70f Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 9 Jul 2025 08:24:09 +0200 Subject: [PATCH 46/93] Fix typo, switch to Hyp element from debug --- package.json | 2 +- src/helpers/config-helper.ts | 2 ++ src/webviews/goalviews/goalsBase.ts | 3 +-- views/debug/Hypothesis.tsx | 4 ++-- views/goals/Goals.tsx | 4 +++- views/goals/tsconfig.json | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index b5b0ddfa..3962888c 100644 --- a/package.json +++ b/package.json @@ -394,7 +394,7 @@ "Coq Layout Engine (experimental)" ] }, - "waterproof.hyp_visibilty" : { + "waterproof.hyp_visibility" : { "type": "string", "default": "none", "enum": [ diff --git a/src/helpers/config-helper.ts b/src/helpers/config-helper.ts index c57e5062..4876afe4 100644 --- a/src/helpers/config-helper.ts +++ b/src/helpers/config-helper.ts @@ -1,5 +1,6 @@ import { workspace } from "vscode"; import { HypVisibility } from "../../lib/types"; +import { WaterproofLogger as wpl } from "./logger"; export class WaterproofConfigHelper { @@ -80,6 +81,7 @@ export class WaterproofConfigHelper { /** `waterproof.hyp_visibility` */ static get hyp_visibility() : HypVisibility { const hypVisibility = config().get("hyp_visibility"); + wpl.log(`Hypothesis visibility set to: ${hypVisibility}`); switch(hypVisibility) { case "all": return HypVisibility.All; diff --git a/src/webviews/goalviews/goalsBase.ts b/src/webviews/goalviews/goalsBase.ts index d8bda1be..b752b007 100644 --- a/src/webviews/goalviews/goalsBase.ts +++ b/src/webviews/goalviews/goalsBase.ts @@ -1,11 +1,10 @@ import { Uri } from "vscode"; -import { GoalAnswer, HypVisibility, PpString } from "../../../lib/types"; +import { GoalAnswer, PpString } from "../../../lib/types"; import { MessageType } from "../../../shared"; import { IGoalsComponent } from "../../components"; import { CoqLspClientConfig } from "../../lsp-client/clientTypes"; import { CoqWebview } from "../coqWebview"; import { WaterproofConfigHelper } from "../../helpers"; - //class for panels that need Goals objects from coq-lsp export abstract class GoalsBase extends CoqWebview implements IGoalsComponent { diff --git a/views/debug/Hypothesis.tsx b/views/debug/Hypothesis.tsx index a2cdadcd..8b201397 100644 --- a/views/debug/Hypothesis.tsx +++ b/views/debug/Hypothesis.tsx @@ -16,7 +16,7 @@ import "../styles/goals.css"; type CoqId = PpString; //displays the hypothesis as a pp string -function Hyp({ hyp: { names, def, ty } }: { hyp: Hyp }) { +export function HypEl({ hyp: { names, def, ty } }: { hyp: Hyp }) { //className to give the right css to the hypothesis, definition or not const className = "coq-hypothesis" + (def ? " coq-has-def" : ""); //a label for the hypothesis @@ -51,7 +51,7 @@ function Hyps({ hyps }: HypsP) { <> {hyps.map((v) => { const key = objectHash(v); - return ; + return ; })} ); diff --git a/views/goals/Goals.tsx b/views/goals/Goals.tsx index 4f8fd87d..4a4b0c19 100644 --- a/views/goals/Goals.tsx +++ b/views/goals/Goals.tsx @@ -4,6 +4,7 @@ import { FormatPrettyPrint } from "../../lib/format-pprint/js/main"; import { convertToString, Goal, GoalConfig, Hyp, HypVisibility, PpString } from "../../lib/types"; import { Box } from "./Box"; import { CoqPp } from "./CoqPp"; +import { HypEl } from "../debug/Hypothesis" import { Position, @@ -48,7 +49,8 @@ function Goal({ goal, visibility }: GoalP) {
{hyps().map((hyp, _) => -
:
+ + //
:
)}
diff --git a/views/goals/tsconfig.json b/views/goals/tsconfig.json index c40db6c6..de7f68c5 100644 --- a/views/goals/tsconfig.json +++ b/views/goals/tsconfig.json @@ -7,5 +7,5 @@ "esModuleInterop": true, "jsx": "react-jsx" }, - "include": ["**/*.ts", "**/*.tsx", "../../lib/**/*", "../logbook/Logbook.tsx", "../debug/ErrorBrowser.tsx", "../../shared/**/*", "../lib/CoqMessage.ts"] + "include": ["**/*.ts", "**/*.tsx", "../../lib/**/*", "../logbook/Logbook.tsx", "../debug/ErrorBrowser.tsx", "../debug/Hypothesis.tsx", "../../shared/**/*", "../lib/CoqMessage.ts"] } From 2b17021a8fd6b1e03b4f58ef41869eb948c99019 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 9 Jul 2025 09:49:29 +0200 Subject: [PATCH 47/93] Fix debug panel --- src/extension.ts | 7 +++++- src/webviews/goalviews/logbook.ts | 2 ++ src/webviews/sidePanel.ts | 3 ++- views/debug/Debug.tsx | 39 +++++++++++++++++++------------ views/debug/tsconfig.json | 11 +++++++++ views/goals/Messages.tsx | 2 ++ views/logbook/Logbook.tsx | 7 +++--- 7 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 views/debug/tsconfig.json diff --git a/src/extension.ts b/src/extension.ts index 359d6d22..33a62cf4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -164,8 +164,10 @@ export class Waterproof implements Disposable { this.webviewManager.addToolWebview("tactics", new TacticsPanel(this.context.extensionUri)); const logbook = new Logbook(this.context.extensionUri, CoqLspClientConfig.create(WaterproofConfigHelper.configuration)); this.webviewManager.addToolWebview("logbook", logbook); + this.goalsComponents.push(logbook); const debug = new DebugPanel(this.context.extensionUri, CoqLspClientConfig.create(WaterproofConfigHelper.configuration)); this.webviewManager.addToolWebview("debug", debug); + this.goalsComponents.push(debug); this.sidePanelProvider = addSidePanel(context, this.webviewManager); @@ -512,7 +514,10 @@ export class Waterproof implements Disposable { const params = this.client.createGoalsRequestParameters(document, position); this.client.requestGoals(params).then( response => { - for (const g of this.goalsComponents) g.updateGoals(response) + for (const g of this.goalsComponents) { + WaterproofLogger.log(`Updating goals for component: ${g}`); + g.updateGoals(response) + } }, reason => { for (const g of this.goalsComponents) g.failedGoals(reason); diff --git a/src/webviews/goalviews/logbook.ts b/src/webviews/goalviews/logbook.ts index b9488c32..b41c5f82 100644 --- a/src/webviews/goalviews/logbook.ts +++ b/src/webviews/goalviews/logbook.ts @@ -4,6 +4,7 @@ import { MessageType } from "../../../shared"; import { CoqLspClientConfig } from "../../lsp-client/clientTypes"; import { WebviewEvents, WebviewState } from "../coqWebview"; import { GoalsBase } from "./goalsBase"; +import { WaterproofLogger } from "../../helpers"; //the logbook panel extends the GoalsBase class export class Logbook extends GoalsBase { @@ -24,6 +25,7 @@ export class Logbook extends GoalsBase { //override updateGoals to save the previous message and activating the panel before posting the message override updateGoals(goals: GoalAnswer | undefined) { + WaterproofLogger.log(`Logbook: updating goals with ${goals}.`); if (!goals) return; this.messages.push(goals); this.activatePanel(); diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index f131ca4d..cad0b66a 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { getNonce } from '../util'; import { WebviewManager, WebviewManagerEvents } from '../webviewManager'; - +import { WaterproofLogger as wpl} from '../helpers'; // this function add the side panel to the vs code side panel export function addSidePanel(context: vscode.ExtensionContext, manager: WebviewManager) { const provider = new SidePanelProvider(context.extensionUri, manager); @@ -24,6 +24,7 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { this._manager.on(WebviewManagerEvents.updateButton, (e) => { // Update the transparency of the button based on the event // This is done when a panel is open + wpl.log(`SidePanelProvider: Updating button transparency for ${e.name} to ${e.open}`); this.updateButtonTransparency(e.name, e.open); }); } diff --git a/views/debug/Debug.tsx b/views/debug/Debug.tsx index 3f7e740b..fcbf2137 100644 --- a/views/debug/Debug.tsx +++ b/views/debug/Debug.tsx @@ -1,9 +1,10 @@ -import React, { Suspense, lazy, useEffect, useState } from "react"; +import { Suspense, lazy, useEffect, useState } from "react"; import { GoalAnswer, HypVisibility, PpString } from "../../lib/types"; import { ErrorBrowser } from "../goals/ErrorBrowser"; import { Goals } from "../goals/Goals"; -import { Hypothesis } from "./Hypothesis"; +import { Messages } from "../goals/Messages"; + import "../styles/info.css"; import { Message, MessageType } from "../../shared"; @@ -17,39 +18,46 @@ const VSCodeDivider = lazy(async () => { export function Debug() { + // visibility of the hypotheses in the goals panel + + //saves the goal const [goals, setGoals] = useState>(); + //boolean to check if the goals are still loading + const [isLoading, setIsLoading] = useState(false); //handles the message //event : CoqMessageEvent as defined above - function infoViewDispatch(msg: Message) { + function infoViewDispatch(msg: Message) { // TODO: make this change in logbook as well if (msg.type === MessageType.renderGoals) { - // most important case that actually get the information - setGoals(msg.body.goals as GoalAnswer); + const goals = msg.body.goals; + + setGoals(goals); //setting the information + setIsLoading(false); } } - // Set the callback, adds and removes the event listener + // Set the callback useEffect(() => { const callback = (ev: MessageEvent) => {infoViewDispatch(ev.data);}; window.addEventListener("message", callback); return () => window.removeEventListener("message", callback); }, []); + //show that the messages are loading + if (isLoading) return
Loading...
; + if (!goals) { - return
Place your cursor in the document to show the goals and hypothesis at that position.
+ return
Place your cursor in the document to show the goals at that position.
} - //the goals and hypothesis are displayed primarily - //if an error occurs at the specified line this error will also be displayed - //the following components are used: Hypothesis, Goals, ErrorBrowser + //The goal and message are displayed along with the error at the position (if it exists) + //Components used: Goals, Messages, ErrorBrowser return (
- - - + +
- {!goals.error ? null : (
@@ -59,7 +67,8 @@ export function Debug() {
)}
- ) + ); } export default Debug; + diff --git a/views/debug/tsconfig.json b/views/debug/tsconfig.json new file mode 100644 index 00000000..12dc30d3 --- /dev/null +++ b/views/debug/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig-base.json", + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "jsx": "react-jsx" + }, + "include": ["**/*.ts", "**/*.tsx", "../../lib/**/*", "../goals/*.tsx", "../../shared/**/*", "../lib/CoqMessage.ts"] +} diff --git a/views/goals/Messages.tsx b/views/goals/Messages.tsx index ffb7b9ca..e64f37dd 100644 --- a/views/goals/Messages.tsx +++ b/views/goals/Messages.tsx @@ -6,6 +6,7 @@ import {Box} from "./Box"; import "../styles/messages.css"; import { PropsWithChildren } from "react"; + //type that makes a GoalAnswer also takes its childrens components with export type MessagesInfo = PropsWithChildren< { @@ -16,6 +17,7 @@ export type MessagesInfo = PropsWithChildren< //component that takes in the MessagesInfo and displays the list of messages export function Messages({answer} : MessagesInfo) { const count = answer.messages.length; + console.log("Messages: count is ", count); //check if there are any messages that need to be shown if (count != 0) { //the Box component is used to display the messages along with the location diff --git a/views/logbook/Logbook.tsx b/views/logbook/Logbook.tsx index 01554335..a7277b3e 100644 --- a/views/logbook/Logbook.tsx +++ b/views/logbook/Logbook.tsx @@ -1,4 +1,3 @@ -import objectHash from "object-hash"; import React, { useEffect, useState } from 'react'; import { GoalAnswer, PpString } from "../../lib/types"; @@ -34,13 +33,13 @@ export function Logbook() { if (!goalsArray) return null; + console.log("Logbook: goalsArray length is ", goalsArray.length); //when messages are loaded, we map over them and display them with the Messages component return (
- {goalsArray.map((value, _) => { - const key = objectHash(value); - return ; + {goalsArray.map((value, i) => { + return ; })}
From cd7f1da4a39fdf821a00539653e7cc66b0e683e1 Mon Sep 17 00:00:00 2001 From: raulTUe Date: Wed, 9 Jul 2025 13:29:12 +0200 Subject: [PATCH 48/93] update editorview state --- editor/src/kroqed-editor/editor.ts | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/kroqed-editor/editor.ts index afb9ebb5..2ef892ea 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/kroqed-editor/editor.ts @@ -154,27 +154,8 @@ export class WaterproofEditor { if (!this._view) { return; } - - const currentText = this._view.state.doc.textContent; - if (content === currentText) { - return; - } - if (this._mapping && this._mapping.version === version) return; - //TODO: save cursor position and restore it after the refresh - const cursorPos = this._view.state.selection; - - this._view.dispatch(this._view.state.tr.setMeta(MENU_PLUGIN_KEY, "remove")); - document.querySelector(".menubar")?.remove(); - document.querySelector(".progress-bar")?.remove(); - document.querySelector(".spinner-container")?.remove(); - this._view.dom.remove(); - - if (this._filef === FileFormat.MarkdownV) { - document.body.classList.add("mv"); - } - this._translator = new FileTranslator(this._filef); const { resultingDocument, documentChange } = checkPrePost(content); @@ -187,12 +168,14 @@ export class WaterproofEditor { const proseDoc = createProseMirrorDocument(resultingDocument, this._filef); this._mapping = new TextDocMapping(this._filef, parsedContent, version); - this.createProseMirrorEditor(proseDoc); - - this.updateCursor(cursorPos); + const newState = EditorState.create({ + doc: proseDoc, + plugins: this._view.state.plugins, + schema: this._schema + }); + this._view.updateState(newState); this.sendLineNumbers(); - this._editorConfig.api.editorReady(); } From 995aeeba78a2a1b10664b808f4e270b4abaa47db Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:57:25 +0200 Subject: [PATCH 49/93] Update to use inInputArea variable --- editor/src/kroqed-editor/codeview/nodeview.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/editor/src/kroqed-editor/codeview/nodeview.ts b/editor/src/kroqed-editor/codeview/nodeview.ts index 119d4893..3c90f20d 100644 --- a/editor/src/kroqed-editor/codeview/nodeview.ts +++ b/editor/src/kroqed-editor/codeview/nodeview.ts @@ -80,7 +80,6 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { const inInputArea = this.partOfInputArea(); const optional = inInputArea ? [lintGutter()] : []; - this._codemirror = new CodeMirror({ doc: this._node.textContent, extensions: [ @@ -131,18 +130,7 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { return; } - if (locked) { - // in student mode. - // TODO: This code is essentially a duplicate of the inInputArea function, but using - // inInputArea here does not work out of the box. Maybe the `this` binding is wrong? - const pos = getPos(); - if (pos === undefined) return; - // Resolve the position in the prosemirror document and get the node one level underneath the root. - // TODO: Assumption that ``s only ever appear one level beneath the root node. - // TODO: Hardcoded node names. - const name = outerView.state.doc.resolve(pos).node(1).type.name; - if (name !== "input") return; // This node is not part of an input area. - } + if (locked && !inInputArea) return; view.update([tr]); } From 7395468e55c68da4a88f58553638c89eeeb7a26b Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Mon, 14 Jul 2025 14:20:04 +0200 Subject: [PATCH 50/93] Remove logbook --- developer-resources/messages.md | 2 +- src/extension.ts | 4 --- src/webviews/goalviews/logbook.ts | 35 ---------------------- src/webviews/sidePanel.ts | 7 ----- views/debug/Debug.tsx | 2 +- views/goals/Info.tsx | 2 +- views/goals/tsconfig.json | 2 +- views/logbook/Logbook.tsx | 49 ------------------------------- views/logbook/index.tsx | 15 ---------- 9 files changed, 4 insertions(+), 114 deletions(-) delete mode 100644 src/webviews/goalviews/logbook.ts delete mode 100644 views/logbook/Logbook.tsx delete mode 100644 views/logbook/index.tsx diff --git a/developer-resources/messages.md b/developer-resources/messages.md index 4e612e11..2283fa85 100644 --- a/developer-resources/messages.md +++ b/developer-resources/messages.md @@ -165,7 +165,7 @@ This message does not have a body. ### `renderGoals` #### Description -Message send by the extension when the set of goals changes. Panels (goal, logbook, info) listen to this message to update the set of goals that they show. +Message send by the extension when the set of goals changes. Panels (goal, info) listen to this message to update the set of goals that they show. #### Body ```ts diff --git a/src/extension.ts b/src/extension.ts index 33a62cf4..c8065796 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,7 +19,6 @@ import { checkConflictingExtensions, excludeCoqFileTypes } from "./util"; import { WebviewManager, WebviewManagerEvents } from "./webviewManager"; import { DebugPanel } from "./webviews/goalviews/debug"; import { GoalsPanel } from "./webviews/goalviews/goalsPanel"; -import { Logbook } from "./webviews/goalviews/logbook"; import { SidePanelProvider, addSidePanel } from "./webviews/sidePanel"; import { Search } from "./webviews/standardviews/search"; import { Help } from "./webviews/standardviews/help"; @@ -162,9 +161,6 @@ export class Waterproof implements Disposable { const executorPanel = new ExecutePanel(this.context.extensionUri); this.webviewManager.addToolWebview("execute", executorPanel); this.webviewManager.addToolWebview("tactics", new TacticsPanel(this.context.extensionUri)); - const logbook = new Logbook(this.context.extensionUri, CoqLspClientConfig.create(WaterproofConfigHelper.configuration)); - this.webviewManager.addToolWebview("logbook", logbook); - this.goalsComponents.push(logbook); const debug = new DebugPanel(this.context.extensionUri, CoqLspClientConfig.create(WaterproofConfigHelper.configuration)); this.webviewManager.addToolWebview("debug", debug); this.goalsComponents.push(debug); diff --git a/src/webviews/goalviews/logbook.ts b/src/webviews/goalviews/logbook.ts deleted file mode 100644 index b41c5f82..00000000 --- a/src/webviews/goalviews/logbook.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Uri } from "vscode"; -import { GoalAnswer, PpString } from "../../../lib/types"; -import { MessageType } from "../../../shared"; -import { CoqLspClientConfig } from "../../lsp-client/clientTypes"; -import { WebviewEvents, WebviewState } from "../coqWebview"; -import { GoalsBase } from "./goalsBase"; -import { WaterproofLogger } from "../../helpers"; - -//the logbook panel extends the GoalsBase class -export class Logbook extends GoalsBase { - - //array to save messages - private readonly messages: GoalAnswer[] = []; - - // constructor to define the name and to listen for webview events - constructor(extensionUri: Uri, config: CoqLspClientConfig) { - super(extensionUri, config, "logbook"); - this.on(WebviewEvents.change, () => { - if (this.state === WebviewState.visible) { - // when the panel is visible or focused the messages are sent - this.postMessage({ type: MessageType.renderGoalsList, body: {goalsList: this.messages }}); - } - }); - } - - //override updateGoals to save the previous message and activating the panel before posting the message - override updateGoals(goals: GoalAnswer | undefined) { - WaterproofLogger.log(`Logbook: updating goals with ${goals}.`); - if (!goals) return; - this.messages.push(goals); - this.activatePanel(); - this.postMessage({ type: MessageType.renderGoalsList, body: {goalsList: this.messages }}); - } - -} diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index cad0b66a..b7a7a928 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -121,7 +121,6 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { -
@@ -129,7 +128,6 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { const vscode = acquireVsCodeApi(); const goalsButton = document.getElementById('goals'); - const logbookButton = document.getElementById('logbook'); const debugButton = document.getElementById('debug'); const executeButton = document.getElementById('execute'); const helpButton = document.getElementById('help'); @@ -143,11 +141,6 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { vscode.postMessage({ command: 'goals' }); }); - logbookButton.addEventListener('click', () => { - // Handle logbook button click event by sending a message to vscode - vscode.postMessage({ command: 'logbook' }); - }); - debugButton.addEventListener('click', () => { // Handle debug button click event by sending a message to vscode vscode.postMessage({ command: 'debug' }); diff --git a/views/debug/Debug.tsx b/views/debug/Debug.tsx index fcbf2137..eca0052f 100644 --- a/views/debug/Debug.tsx +++ b/views/debug/Debug.tsx @@ -27,7 +27,7 @@ export function Debug() { //handles the message //event : CoqMessageEvent as defined above - function infoViewDispatch(msg: Message) { // TODO: make this change in logbook as well + function infoViewDispatch(msg: Message) { if (msg.type === MessageType.renderGoals) { const goals = msg.body.goals; diff --git a/views/goals/Info.tsx b/views/goals/Info.tsx index 9e1d0c52..c626946a 100644 --- a/views/goals/Info.tsx +++ b/views/goals/Info.tsx @@ -29,7 +29,7 @@ export function InfoPanel() { //handles the message //event : CoqMessageEvent as defined above - function infoViewDispatch(msg: Message) { // TODO: make this change in logbook as well + function infoViewDispatch(msg: Message) { if (msg.type === MessageType.renderGoals) { const goals = msg.body.goals; diff --git a/views/goals/tsconfig.json b/views/goals/tsconfig.json index de7f68c5..fb654185 100644 --- a/views/goals/tsconfig.json +++ b/views/goals/tsconfig.json @@ -7,5 +7,5 @@ "esModuleInterop": true, "jsx": "react-jsx" }, - "include": ["**/*.ts", "**/*.tsx", "../../lib/**/*", "../logbook/Logbook.tsx", "../debug/ErrorBrowser.tsx", "../debug/Hypothesis.tsx", "../../shared/**/*", "../lib/CoqMessage.ts"] + "include": ["**/*.ts", "**/*.tsx", "../../lib/**/*", "../debug/ErrorBrowser.tsx", "../debug/Hypothesis.tsx", "../../shared/**/*", "../lib/CoqMessage.ts"] } diff --git a/views/logbook/Logbook.tsx b/views/logbook/Logbook.tsx deleted file mode 100644 index a7277b3e..00000000 --- a/views/logbook/Logbook.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import { GoalAnswer, PpString } from "../../lib/types"; -import { Messages } from "../goals/Messages"; - -import "../styles/info.css"; -import { Message, MessageType } from "../../shared"; - - -export function Logbook() { - //saves the goals for the logbook - const [goalsArray, setGoalsArray] = useState[]>(); - //boolean to check if the messages are still loading - const [isLoading, setIsLoading] = useState(true); - - //message handler - function infoViewDispatch(msg: Message) { - if (msg.type === MessageType.renderGoalsList) { - setGoalsArray(msg.body.goalsList); //setting the information - setIsLoading(false); //putting loading to false - } - } - - // Set the callback - useEffect(() => { - const callback = (ev: MessageEvent) => {infoViewDispatch(ev.data);}; - window.addEventListener("message", callback); - return () => window.removeEventListener("message", callback); - }, []); - - //show that the messages are loading - if (isLoading) return
Loading...
; - - if (!goalsArray) return null; - - console.log("Logbook: goalsArray length is ", goalsArray.length); - //when messages are loaded, we map over them and display them with the Messages component - return ( -
-
- {goalsArray.map((value, i) => { - return ; - })} -
-
- ); -} - -export default Logbook; diff --git a/views/logbook/index.tsx b/views/logbook/index.tsx deleted file mode 100644 index b19c1c9a..00000000 --- a/views/logbook/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// This is the script that is loaded by Coq's webview - -import React from "react"; -import { createRoot } from "react-dom/client"; - -import "../styles/index.css"; -import Logbook from "./Logbook"; - -const container = document.getElementById("root"); -const root = createRoot(container!); // createRoot(container!) if you use TypeScript -root.render( - - - -); From 88ae36a10a7806578473e186889efe607e867846 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Mon, 14 Jul 2025 14:22:07 +0200 Subject: [PATCH 51/93] Fix build config --- esbuild.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/esbuild.mjs b/esbuild.mjs index cd1ef046..4d634e4b 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -107,7 +107,6 @@ function viewBuild(file) { } var infoView = viewBuild("./views/goals/index.tsx"); -var logView = viewBuild("./views/logbook/index.tsx") var executeView = viewBuild("./views/execute/index.tsx"); var debugView = viewBuild("./views/debug/index.tsx"); var helpView = viewBuild("./views/help/index.tsx"); From 5bc68af05d6604ce07d88550cb19f5dc5c1aaa2f Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 15 Jul 2025 09:56:04 +0200 Subject: [PATCH 52/93] Fix creating documents --- src/extension.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 7c641aaa..cfbb3b2d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -331,7 +331,8 @@ export class Waterproof implements Disposable { } private async waterproofTutorialCommand(): Promise { - const defaultUri = Utils.joinPath(workspace.workspaceFolders![0].uri, "waterproof_tutorial.mv"); + const hasWorkspaceOpen = workspace.workspaceFolders !== undefined && workspace.workspaceFolders.length != 0; + const defaultUri = hasWorkspaceOpen ? Utils.joinPath(workspace.workspaceFolders![0].uri, "waterproof_tutorial.mv") : Uri.parse("./waterproof_tutorial.mv"); window.showSaveDialog({filters: {'Waterproof': ["mv", "v"]}, title: "Waterproof Tutorial", defaultUri}).then((uri) => { if (!uri) { window.showErrorMessage("Something went wrong in saving the Waterproof tutorial file"); @@ -359,7 +360,8 @@ export class Waterproof implements Disposable { * or by using the File -> New File... option. */ private async newFileCommand(): Promise { - const defaultUri = Utils.joinPath(workspace.workspaceFolders![0].uri, "new_waterproof_document.mv"); + const hasWorkspaceOpen = workspace.workspaceFolders !== undefined && workspace.workspaceFolders.length != 0; + const defaultUri = hasWorkspaceOpen ? Utils.joinPath(workspace.workspaceFolders![0].uri, "new_waterproof_document.mv") : Uri.parse("./new_waterproof_document.mv"); window.showSaveDialog({filters: {'Waterproof': ["mv", "v"]}, title: "New Waterproof Document", defaultUri}).then((uri) => { if (!uri) { window.showErrorMessage("Something went wrong in creating a new waterproof document"); From 9e428151c32e8845871f274506e3c93b2f5d9ab3 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 15 Jul 2025 11:30:38 +0200 Subject: [PATCH 53/93] Hypotheses as separate box, remove count --- views/goals/Goals.tsx | 65 +++++++++++++++++++++++----------------- views/goals/Messages.tsx | 1 - 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/views/goals/Goals.tsx b/views/goals/Goals.tsx index 4a4b0c19..26063105 100644 --- a/views/goals/Goals.tsx +++ b/views/goals/Goals.tsx @@ -12,13 +12,11 @@ import { } from "vscode-languageserver-types"; import "../styles/goals.css"; - - //type that contains Goal and an ID for that goal -type GoalP = { goal: Goal; idx: number; visibility : HypVisibility}; +type GoalP = { goal: Goal; idx: number;}; //component to display a single goal as a Pp string -function Goal({ goal, visibility }: GoalP) { +function Goal({ goal}: GoalP) { // https://beta.reactjs.org/learn/manipulating-the-dom-with-refs const ref: React.LegacyRef | null = useRef(null); const tyRef: React.LegacyRef | null = useRef(null); @@ -29,29 +27,9 @@ function Goal({ goal, visibility }: GoalP) { } }); - const hyps : () => Array> = () => { - const hyps : Array> = []; - if (visibility === HypVisibility.All) { - hyps.push(...goal.hyps); - } else if (visibility === HypVisibility.Limited) { - hyps.push( - ...goal.hyps - .map (hyp => - // Filter out all names starting with an underscore - ({ ...hyp, names: hyp.names.filter(name => convertToString(name).charAt(0) !== "_") })) - .filter(hyp => hyp.names.length > 0) - ); - } - return hyps; - } - return (
- {hyps().map((hyp, _) => - - //
:
- )}
@@ -82,6 +60,32 @@ function GoalsList({ }: GoalsListP) { const count = goals.length; + const hyps : (goal : Goal) => Array> = (goal : Goal) => { + const hyps : Array> = []; + if (visibility === HypVisibility.All) { + hyps.push(...goal.hyps); + } else if (visibility === HypVisibility.Limited) { + hyps.push( + ...goal.hyps + .map (hyp => + // Filter out all names starting with an underscore + ({ ...hyp, names: hyp.names.filter(name => convertToString(name).charAt(0) !== "_") })) + .filter(hyp => hyp.names.length > 0) + ); + } + return hyps; + } + const hypBlock = (goal : Goal) => { + const elems = hyps(goal).map((hyp, _) => + ) + if (elems.length > 0) { + return + { elems } + + } + return elems; + } + //if there are no goals then this is displayed if (count == 0) { if (show_on_empty) { @@ -98,17 +102,22 @@ function GoalsList({ //One goal, the goals is displayed } else if (count == 1) { return ( - - - +
+ + + + {hypBlock(goals[0])} +
+ ); //Numerous goals, only the first goal is displayed, the other goals are hidden. } else { return (
- + + {hypBlock(goals[0])}
diff --git a/views/goals/Messages.tsx b/views/goals/Messages.tsx index e64f37dd..cb1b257a 100644 --- a/views/goals/Messages.tsx +++ b/views/goals/Messages.tsx @@ -17,7 +17,6 @@ export type MessagesInfo = PropsWithChildren< //component that takes in the MessagesInfo and displays the list of messages export function Messages({answer} : MessagesInfo) { const count = answer.messages.length; - console.log("Messages: count is ", count); //check if there are any messages that need to be shown if (count != 0) { //the Box component is used to display the messages along with the location From 5d1e4d7843f5c675b125c03fb25e2b14b0e7bd48 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 16 Jul 2025 14:59:06 +0200 Subject: [PATCH 54/93] Rename and move documentation to enumDescriptions --- package.json | 9 +++++++-- src/helpers/config-helper.ts | 6 +++--- src/webviews/goalviews/goalsBase.ts | 2 +- views/goals/Goals.tsx | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 3962888c..d22a85e8 100644 --- a/package.json +++ b/package.json @@ -394,7 +394,7 @@ "Coq Layout Engine (experimental)" ] }, - "waterproof.hyp_visibility" : { + "waterproof.visibilityOfHypotheses" : { "type": "string", "default": "none", "enum": [ @@ -402,7 +402,12 @@ "limited", "none" ], - "description": "Visibility of hypotheses in the goals panel.\n- `all`: show all hypotheses\n- `limited`: show only hypotheses not starting with _\n- `none`: do not show any hypotheses\nFor didiactic purposes, we recommend `none` for mathematicians learning proofs and `limited` or `all` for students using the Rocq language." + "enumDescriptions": [ + "Show all hypotheses in the goals panel", + "Show only hypotheses not starting with _ in the goals panel", + "Do not show any hypotheses in the goals panel" + ], + "description": "Visibility of hypotheses in the goals panel.\nFor didiactic purposes, we recommend `none` for mathematicians learning proofs and `limited` or `all` for students using the Rocq language." } } }, diff --git a/src/helpers/config-helper.ts b/src/helpers/config-helper.ts index 4876afe4..893c5e8d 100644 --- a/src/helpers/config-helper.ts +++ b/src/helpers/config-helper.ts @@ -78,9 +78,9 @@ export class WaterproofConfigHelper { } - /** `waterproof.hyp_visibility` */ - static get hyp_visibility() : HypVisibility { - const hypVisibility = config().get("hyp_visibility"); + /** `waterproof.visibilityOfHypotheses` */ + static get visibilityOfHypotheses() : HypVisibility { + const hypVisibility = config().get("visibilityOfHypotheses"); wpl.log(`Hypothesis visibility set to: ${hypVisibility}`); switch(hypVisibility) { case "all": diff --git a/src/webviews/goalviews/goalsBase.ts b/src/webviews/goalviews/goalsBase.ts index b752b007..9b3e82f6 100644 --- a/src/webviews/goalviews/goalsBase.ts +++ b/src/webviews/goalviews/goalsBase.ts @@ -18,7 +18,7 @@ export abstract class GoalsBase extends CoqWebview implements IGoalsComponent { //sends message for renderGoals updateGoals(goals: GoalAnswer | undefined) { if (goals) { - const visibility = WaterproofConfigHelper.hyp_visibility; + const visibility = WaterproofConfigHelper.visibilityOfHypotheses; this.postMessage({ type: MessageType.renderGoals, body: {goals, visibility } }); } } diff --git a/views/goals/Goals.tsx b/views/goals/Goals.tsx index 26063105..e21944b9 100644 --- a/views/goals/Goals.tsx +++ b/views/goals/Goals.tsx @@ -79,7 +79,7 @@ function GoalsList({ const elems = hyps(goal).map((hyp, _) => ) if (elems.length > 0) { - return + return { elems } } From 981918761eee9fbf8df30c3b8a27734e60e91f82 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 16 Jul 2025 15:00:11 +0200 Subject: [PATCH 55/93] Switch quotes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d22a85e8..94b7ac33 100644 --- a/package.json +++ b/package.json @@ -407,7 +407,7 @@ "Show only hypotheses not starting with _ in the goals panel", "Do not show any hypotheses in the goals panel" ], - "description": "Visibility of hypotheses in the goals panel.\nFor didiactic purposes, we recommend `none` for mathematicians learning proofs and `limited` or `all` for students using the Rocq language." + "description": "Visibility of hypotheses in the goals panel.\nFor didiactic purposes, we recommend \"none\" for mathematicians learning proofs and \"limited\" or \"all\" for students using the Rocq language." } } }, From fb7801459661c831fe24fc228d0dbfc7d6abf794 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 16 Jul 2025 15:16:52 +0200 Subject: [PATCH 56/93] Update src/helpers/file.ts Co-authored-by: DikieDick <26772815+DikieDick@users.noreply.github.com> --- src/helpers/file.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/helpers/file.ts b/src/helpers/file.ts index d630c36c..c7b551f7 100644 --- a/src/helpers/file.ts +++ b/src/helpers/file.ts @@ -15,6 +15,9 @@ export class WaterproofFileUtil { } public static join(...paths: string[]): string { + // We filter out empty path strings, maybe we could instead make this function + // assume that arguments are non-empty and let the caller handle these? + paths = paths.filter(v => v != ""); const sep = process.platform === "win32" ? "\\" : "/"; return paths.join(sep); } From 42e56482e787cefeba58790872a0c5ba05bb54d1 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 16 Jul 2025 15:57:39 +0200 Subject: [PATCH 57/93] Debug logging --- src/extension.ts | 20 ++++++++++++++++---- src/lsp-client/client.ts | 2 ++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index cfbb3b2d..c2001084 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -516,15 +516,27 @@ export class Waterproof implements Disposable { * This function gets called on TextEditorSelectionChange events and it requests the goals * if needed */ - private async updateGoals(document: TextDocument, position: Position): Promise { - if (!this.client.isRunning()) return; + private async updateGoals(document: TextDocument, position: Position): Promise { + wpl.debug(`Updating goals for document: ${document.uri.toString()} at position: ${position.line}:${position.character}`); + if (!this.client.isRunning()) { + wpl.debug("Client is not running, cannot update goals."); + return; + } const params = this.client.createGoalsRequestParameters(document, position); + wpl.debug(`Requesting goals for components: ${this.goalsComponents}`); this.client.requestGoals(params).then( response => { - for (const g of this.goalsComponents) g.updateGoals(response) + wpl.debug(`Received goals response: ${JSON.stringify(response)}`); + for (const g of this.goalsComponents) { + wpl.debug(`Updating goals component: ${g.constructor.name}`); + g.updateGoals(response) + } }, reason => { - for (const g of this.goalsComponents) g.failedGoals(reason); + for (const g of this.goalsComponents) { + wpl.debug(`Failed to update goals component: ${g.constructor.name}`); + g.failedGoals(reason); + } } ); } diff --git a/src/lsp-client/client.ts b/src/lsp-client/client.ts index 01bdd49d..94941dbd 100644 --- a/src/lsp-client/client.ts +++ b/src/lsp-client/client.ts @@ -234,6 +234,7 @@ export function CoqLspClient(Base: T) { } async requestGoals(params?: GoalRequest | Position): Promise> { + wpl.debug(`Requesting goals with params: ${JSON.stringify(params)}`); if (!params || "line" in params) { // if `params` is not a `GoalRequest` ... params ??= this.activeCursorPosition; if (!this.activeDocument || !params) { @@ -241,6 +242,7 @@ export function CoqLspClient(Base: T) { } params = this.createGoalsRequestParameters(this.activeDocument, params); } + wpl.debug(`Sending request for goals`); return this.sendRequest(goalRequestType, params); } From 77a4aaf5a89c9825730360e7a603f0c1e0aca1ff Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:45:06 +0200 Subject: [PATCH 58/93] Add options to toggle the menu bar items Menu bar options are hidden by default. Using the `showMenuItems` options the buttons can be shown in student mode as well. --- cypress/e2e/basic.cy.ts | 8 +- editor/src/kroqed-editor/codeview/nodeview.ts | 12 +-- editor/src/kroqed-editor/editor.ts | 8 +- editor/src/kroqed-editor/inputArea.ts | 56 +++++----- editor/src/kroqed-editor/menubar/menubar.ts | 102 ++++++++++-------- editor/src/kroqed-editor/styles/menubar.css | 2 +- src/pm-editor/pmWebview.ts | 4 +- 7 files changed, 106 insertions(+), 86 deletions(-) diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts index 5240a3b7..af6c8829 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.ts @@ -13,8 +13,8 @@ describe('Basic tests', () => { it('Editor opens, contains all parts and displays file', () => { // Editor is visible cy.get("#editor").should("be.visible"); - // Menubar is visible - cy.get(".menubar").should("be.visible"); + // Menubar is not visible by default + cy.get(".menubar").should("not.be.visible"); // Progress bar is visible // TODO: Progress bar only appears after clicking in the editor? @@ -32,7 +32,9 @@ describe('Basic tests', () => { // TODO: This also immediately opens the markdown editor for the H1 cy.window().then((win) => { win.postMessage({type: MessageType.teacher, body: true}) }); - cy.get('coqblock > .cm-editor > .cm-scroller > .cm-content').click(); // to reset h1 + // now that we are in teacher mode the menubar should be visible + cy.get(".menubar").should("be.visible"); + cy.get("coqblock > .cm-editor > .cm-scroller > .cm-content").click(); // to reset h1 cy.get("#editor h1").should("be.visible"); cy.get("#editor h1").click(); cy.get(".markdown-view").should("be.visible"); diff --git a/editor/src/kroqed-editor/codeview/nodeview.ts b/editor/src/kroqed-editor/codeview/nodeview.ts index 2c530009..c5769ae3 100644 --- a/editor/src/kroqed-editor/codeview/nodeview.ts +++ b/editor/src/kroqed-editor/codeview/nodeview.ts @@ -117,14 +117,12 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { } else { // Figure out whether we are in teacher or student mode. // This is a ProseMirror object, hence we need the prosemirror view (outerview) state. - const locked = INPUT_AREA_PLUGIN_KEY.getState(outerView.state)?.locked; - if (locked === undefined) { - // if we could not get the locked state then we do not - // allow this transaction to update the view. - return; - } + const teacher = INPUT_AREA_PLUGIN_KEY.getState(outerView.state)?.teacher; + // if we could not get the locked state then we do not + // allow this transaction to update the view. + if (teacher === undefined) return; - if (locked) { + if (!teacher) { // in student mode. const pos = getPos(); if (pos === undefined) return; diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/kroqed-editor/editor.ts index 12879803..3e36ac1f 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/kroqed-editor/editor.ts @@ -372,14 +372,14 @@ export class WaterproofEditor { // Early return if the plugin state is undefined. if (inputAreaPluginState === undefined) return false; - const { locked, globalLock } = inputAreaPluginState; + const { teacher, globalLock } = inputAreaPluginState; // Early return if we are in the global locked mode // (nothing should be editable anymore) if (globalLock) return false; // If we are in teacher mode (ie. not locked) than // we are always able to insert. - if (!locked) { + if (teacher) { this.createAndDispatchInsertionTransaction(trans, symbolUnicode, from, to); return true; } @@ -432,13 +432,13 @@ export class WaterproofEditor { /** * Called whenever a message describing the configuration of user is sent * - * @param isTeacher represents the mode selected by user + * @param isTeacher Whether teacher mode is enabled */ public updateLockingState(isTeacher: boolean) : void { if (!this._view) return; const state = this._view.state; const trans = state.tr; - trans.setMeta(INPUT_AREA_PLUGIN_KEY, {teacher: !isTeacher}); + trans.setMeta(INPUT_AREA_PLUGIN_KEY, {teacher: isTeacher}); this._view.dispatch(trans); } diff --git a/editor/src/kroqed-editor/inputArea.ts b/editor/src/kroqed-editor/inputArea.ts index 3d885cb8..72cdadca 100644 --- a/editor/src/kroqed-editor/inputArea.ts +++ b/editor/src/kroqed-editor/inputArea.ts @@ -7,7 +7,7 @@ import { EditorState, Plugin, PluginKey, PluginSpec, Transaction } from * `globalLock: boolean` indicating that we are in a global lockdown state (caused by an unrecoverable error). */ export interface IInputAreaPluginState { - locked: boolean; + teacher: boolean; globalLock: boolean; } @@ -22,7 +22,7 @@ const InputAreaPluginSpec : PluginSpec = { init(_config, _instance){ // Initially set the locked state to true and globalLock to false. return { - locked: true, + teacher: false, globalLock: false, }; }, @@ -30,41 +30,47 @@ const InputAreaPluginSpec : PluginSpec = { EditorState, _newState: EditorState ){ // produce updated state field for this plugin - let newLockedState = tr.getMeta(INPUT_AREA_PLUGIN_KEY); - let newGlobalLock = value.globalLock; - if (newLockedState === undefined) newLockedState = value.locked; - if (newLockedState == "ErrorMode") { - // We are in a global locked state if we receive this meta. - newLockedState = value.locked; - newGlobalLock = true; + + const meta = tr.getMeta(INPUT_AREA_PLUGIN_KEY); + if (meta === undefined) { + return value; + } else { + let newGlobalLock = value.globalLock; + let newTeacher = value.teacher; + if (meta == "ErrorMode") { + // We are in a global locked state if we receive this meta. + newTeacher = value.teacher; + newGlobalLock = true; - // If we are in lockdown then we remove the editor and show an error message. - const node = document.querySelector("#editor"); - if (!node) throw new Error("Node cannot be undefined here"); - node.innerHTML = ""; - const container = document.createElement("div"); - container.classList.add("frozen-thingie"); - container.innerHTML = `
💀
DOCUMENT FROZEN!
Reopen file...
`; - node.appendChild(container); + // If we are in lockdown then we remove the editor and show an error message. + const node = document.querySelector("#editor"); + if (!node) throw new Error("Node cannot be undefined here"); + node.innerHTML = ""; + const container = document.createElement("div"); + container.classList.add("frozen-thingie"); + container.innerHTML = `
💀
DOCUMENT FROZEN!
Reopen file...
`; + node.appendChild(container); + } else { + newTeacher = meta.teacher ?? false; + } + return { + teacher: newTeacher, + globalLock: newGlobalLock, + }; } - return { - locked: newLockedState, - globalLock: newGlobalLock, - }; } }, props: { editable: (state) => { // Get locked and globalLock states from the plugin. - const locked = INPUT_AREA_PLUGIN_KEY.getState(state)?.locked; - const globalLock = INPUT_AREA_PLUGIN_KEY.getState(state)?.globalLock; - if (locked === undefined || globalLock === undefined) new Error("Input Area plugin is broken, it has no state "); + const teacher = INPUT_AREA_PLUGIN_KEY.getState(state)?.teacher ?? false; + const globalLock = INPUT_AREA_PLUGIN_KEY.getState(state)?.globalLock ?? false; // In the `globalLock` state nothing is editable anymore. if (globalLock) return false; // In teacher mode, everything is editable by default. - if (!locked) return true; + if (teacher) return true; // Get the from selection component. const { $from } = state.selection; diff --git a/editor/src/kroqed-editor/menubar/menubar.ts b/editor/src/kroqed-editor/menubar/menubar.ts index 4245fdaa..2efbf30b 100644 --- a/editor/src/kroqed-editor/menubar/menubar.ts +++ b/editor/src/kroqed-editor/menubar/menubar.ts @@ -20,10 +20,11 @@ type MenuEntry = { * @param displayedText The text displayed on the button (can be HTML). * @param tooltipText The tooltip to show when hovering over this button. * @param cmd The command to execute when this button is pressed. + * @param buttonSettings configure button behaviour. `teacherModeOnly`: This button should only appear in teacher mode. `showByDefault` shown regardless of menu setting. * @param teacherModeOnly [`false` by default] Whether this button should only be available in teacher mode. * @returns A new `MenuButton` object. */ -function createMenuItem(displayedText: string, tooltipText: string, cmd: Command, teacherModeOnly: boolean = false, showByDefault: boolean = false): MenuEntry { +function createMenuItem(displayedText: string, tooltipText: string, cmd: Command, buttonSettings?: {teacherModeOnly?: boolean, showByDefault?: boolean}): MenuEntry { // Create the DOM element. const menuItem = document.createElement("div"); // Set the displayed text and the tooltip. @@ -32,7 +33,7 @@ function createMenuItem(displayedText: string, tooltipText: string, cmd: Command // Add the class for styling this menubar item menuItem.classList.add("menubar-item"); // Return the new item. - return {cmd, dom: menuItem, teacherModeOnly, showByDefault}; + return {cmd, dom: menuItem, teacherModeOnly: buttonSettings?.teacherModeOnly ?? false, showByDefault: buttonSettings?.showByDefault ?? false}; } /** @@ -59,7 +60,7 @@ class MenuView implements PluginView { } // Update the view - this.update(view); + this.update(); // Add event listeners to every menubar item this.menubarDOM.addEventListener("mousedown", (event) => { @@ -75,21 +76,29 @@ class MenuView implements PluginView { if (item.dom.contains(mouseTarget)) { // This item was clicked, execute the command and update this view. item.cmd(view.state, view.dispatch, view); - this.update(view); + this.update(); } } - this.update(view); + this.update(); }); } - update(view: EditorView) { - // Whether we are currently in teacher mode. - const inTeacherMode = MENU_PLUGIN_KEY.getState(view.state)?.teacher; - const showItems = MENU_PLUGIN_KEY.getState(view.state)?.showMenuItems; - console.log("SHOW ITEMS", showItems); + + update() { + // Whether we are currently in teacher mode. + const inTeacherMode = INPUT_AREA_PLUGIN_KEY.getState(this.view.state)?.teacher ?? false; + const showItems = MENU_PLUGIN_KEY.getState(this.view.state)?.showMenuItems ?? false; for(const item of this.items) { + // Hidden by default + item.dom.style.display = "none"; + // Shown when the user is in teacher mode *or* the item should always be shown + // *or* if the user (in student mode) has enabled the shown menu items options. + if (inTeacherMode || item.showByDefault || (!item.teacherModeOnly && showItems)) { + item.dom.style.display = "flex"; + } + const active = item.cmd(this.view.state, undefined, this.view); /* From https://prosemirror.net/examples/menu/ "To update the menu for a new state, all commands are run without dispatch function, @@ -100,11 +109,6 @@ class MenuView implements PluginView { // And make it unclickable. item.dom.setAttribute("disabled", (!active).toString()); - // We hide the item (set display to none) if it should only be available in teacher mode - // and the user is not in teacher mode. - if ((item.teacherModeOnly && !inTeacherMode) || (!item.showByDefault && showItems)) { - item.dom.style.display = "none"; - } } } @@ -129,6 +133,20 @@ const LaTeX_SVG = ` `${cmdOrCtrl}-${key}`; + const teacherOnly = {teacherModeOnly : true}; + // Create the list of menu entries. const items: MenuEntry[] = [ // Insert Coq command - createMenuItem("Math↓", `Insert new verified math block underneath (${keyBinding("q")})`, cmdInsertCode(schema, filef, InsertionPlace.Underneath), false), - createMenuItem("Math↑", `Insert new verified math block above (${keyBinding("Q")})`, cmdInsertCode(schema, filef, InsertionPlace.Above), false), + createMenuItem("Math↓", `Insert new verified math block underneath (${keyBinding("q")})`, cmdInsertCode(schema, filef, InsertionPlace.Underneath)), + createMenuItem("Math↑", `Insert new verified math block above (${keyBinding("Q")})`, cmdInsertCode(schema, filef, InsertionPlace.Above)), // Insert Markdown - createMenuItem("Text↓", `Insert new text block underneath (${keyBinding("m")})`, cmdInsertMarkdown(schema, filef, InsertionPlace.Underneath), false), - createMenuItem("Text↑", `Insert new text block above (${keyBinding("M")})`, cmdInsertMarkdown(schema, filef, InsertionPlace.Above), false), + createMenuItem("Text↓", `Insert new text block underneath (${keyBinding("m")})`, cmdInsertMarkdown(schema, filef, InsertionPlace.Underneath)), + createMenuItem("Text↑", `Insert new text block above (${keyBinding("M")})`, cmdInsertMarkdown(schema, filef, InsertionPlace.Above)), // Insert LaTeX - createMenuItem(`${LaTeX_SVG}
`, `Insert new LaTeX block underneath (${keyBinding("l")})`, cmdInsertLatex(schema, filef, InsertionPlace.Underneath), false), - createMenuItem(`${LaTeX_SVG}
`, `Insert new LaTeX block above (${keyBinding("L")})`, cmdInsertLatex(schema, filef, InsertionPlace.Above), false), + createMenuItem(`${LaTeX_SVG}
`, `Insert new LaTeX block underneath (${keyBinding("l")})`, cmdInsertLatex(schema, filef, InsertionPlace.Underneath)), + createMenuItem(`${LaTeX_SVG}
`, `Insert new LaTeX block above (${keyBinding("L")})`, cmdInsertLatex(schema, filef, InsertionPlace.Above)), // Select the parent node. - createMenuItem("Parent", `Select the parent node (${keyBinding(".")})`, selectParentNode, false), + createMenuItem("Parent", `Select the parent node (${keyBinding(".")})`, selectParentNode), // in teacher mode, display input area, hint and lift buttons. - createMenuItem("ⵊ...", "Make selection an input area", wrapIn(schema.nodes["input"]), true), - createMenuItem("?", "Make selection a hint element", wrapIn(schema.nodes["hint"]), true), - createMenuItem("↑", "Lift selected node (Reverts the effect of making a 'hint' or 'input area')", liftWrapper, true) + createMenuItem("ⵊ...", "Make selection an input area", teacherOnlyWrapper(wrapIn(schema.nodes["input"])), teacherOnly), + createMenuItem("?", "Make selection a hint element", teacherOnlyWrapper(wrapIn(schema.nodes["hint"])), teacherOnly), + createMenuItem("↑", "Lift selected node (Reverts the effect of making a 'hint' or 'input area')", teacherOnlyWrapper(liftWrapper), teacherOnly) ] // If the DEBUG variable is set to `true` then we display a `dump` menu item, which outputs the current @@ -167,11 +187,11 @@ function createDefaultMenu(schema: Schema, outerView: EditorView, filef: FileFor items.push(createMenuItem("DUMP DOC", "", (state, dispatch) => { if (dispatch) console.log("\x1b[33m[DEBUG]\x1b[0m dumped doc", state.doc.toJSON()); return true; - }, false)); + }, {showByDefault: true})); items.push(createMenuItem("DUMP SELECTION", "", (state, dispatch) => { if (dispatch) console.log("\x1b[33m[DEBUG]\x1b[0m Selection", state.selection); return true; - }, false)); + }, {showByDefault: true})); } // Return a new MenuView with the previously created items. @@ -182,7 +202,6 @@ function createDefaultMenu(schema: Schema, outerView: EditorView, filef: FileFor * Interface describing the state of the menu plugin */ interface IMenuPluginState { - teacher: boolean; showMenuItems: boolean; } @@ -200,7 +219,7 @@ export const MENU_PLUGIN_KEY = new PluginKey("prosemirror-menu export function menuPlugin(schema: Schema, filef: FileFormat, os: OS) { return new Plugin({ // This plugin has an associated `view`. This allows it to add DOM elements. - view(outerView) { + view(outerView: EditorView) { // Create the default menu. const menuView = createDefaultMenu(schema, outerView, filef, os); // Get the parent node (the parent node of the outer prosemirror dom) @@ -210,31 +229,24 @@ export function menuPlugin(schema: Schema, filef: FileFormat, os: OS) { } // We insert the menubar before the prosemirror in the DOM tree. parentNode.insertBefore(menuView.menubarDOM, outerView.dom); - + + menuView.update(); // Return the plugin view. - return menuView + return menuView; }, // Set the key (uniquely associates this key to this plugin) key: MENU_PLUGIN_KEY, state: { - init(_config, _instance): IMenuPluginState { - // Initially teacher mode is set to true. + init(_config, _instance){ return { - teacher: true, showMenuItems: false, - } + }; }, apply(tr, value, _oldState, _newState) { - // Upon recieving a transaction with meta information... - const teacherState = !tr.getMeta(INPUT_AREA_PLUGIN_KEY); - const menuState = tr.getMeta(MENU_PLUGIN_KEY); - // we check if the teacherState variable is set and if so, - // we update the plugin state to reflect this return { - teacher: teacherState === undefined ? value.teacher : teacherState, - showMenuItems: menuState === undefined ? value.showMenuItems : menuState, - }; - }, + showMenuItems: tr.getMeta(MENU_PLUGIN_KEY) ?? value.showMenuItems, + }; + } } - }) + }); } diff --git a/editor/src/kroqed-editor/styles/menubar.css b/editor/src/kroqed-editor/styles/menubar.css index 93de205f..e6363f54 100644 --- a/editor/src/kroqed-editor/styles/menubar.css +++ b/editor/src/kroqed-editor/styles/menubar.css @@ -52,7 +52,7 @@ body { font-weight: var(--font-weight); font-size: var(--font-size); - display: flex; + display: none; flex-direction: row; justify-content: center; align-items: center; diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index 7b3cbd36..11676bf8 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -198,7 +198,7 @@ export class ProseMirrorWebview extends EventEmitter { if (e.affectsConfiguration("waterproof.showMenuItemsInEditor")) { this._showMenuItemsInEditor = WaterproofConfigHelper.showMenuItems; - WaterproofLogger.log(`Will now show menu items? ${WaterproofConfigHelper.showMenuItems}`); + WaterproofLogger.log(`Menu items will now be ${WaterproofConfigHelper.showMenuItems ? "shown" : "hidden"} in student mode`); this.updateMenuItemsInEditor(); } })); @@ -408,7 +408,9 @@ export class ProseMirrorWebview extends EventEmitter { break; case MessageType.ready: this.syncWebview(); + // When ready send the state of the teacher mode and show menu items settings to editor this.updateTeacherMode(); + this.updateMenuItemsInEditor(); break; case MessageType.lineNumbers: this._linenumber = msg.body; From eb263ab7bd1c261d10fbe6df56792a4381224523 Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:04:24 +0200 Subject: [PATCH 59/93] Fix typechecking error --- editor/src/kroqed-editor/commands/command-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/kroqed-editor/commands/command-helpers.ts b/editor/src/kroqed-editor/commands/command-helpers.ts index e5b33668..b1b682c6 100644 --- a/editor/src/kroqed-editor/commands/command-helpers.ts +++ b/editor/src/kroqed-editor/commands/command-helpers.ts @@ -129,7 +129,7 @@ export function getContainingNode(sel: Selection): PNode | undefined { export function allowedToInsert(state: EditorState): boolean { const pluginState = INPUT_AREA_PLUGIN_KEY.getState(state); if (!pluginState) return false; - const isTeacher = !pluginState.locked; + const isTeacher = pluginState.teacher; // If in global locking mode, disallow everything if (pluginState.globalLock) return false; // If the user is in teacher mode always return `true`, if not From d38c4632a93b82e4d91c61f271efb7edc21c0983 Mon Sep 17 00:00:00 2001 From: raulTUe Date: Sat, 19 Jul 2025 13:39:12 +0200 Subject: [PATCH 60/93] destroy previous progress bars --- editor/src/kroqed-editor/editor.ts | 11 ++++++++--- editor/src/kroqed-editor/progressBar.ts | 12 ++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/kroqed-editor/editor.ts index 2ef892ea..dcbecc27 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/kroqed-editor/editor.ts @@ -151,10 +151,15 @@ export class WaterproofEditor { } refreshDocument(content: string, version: number) { - if (!this._view) { - return; - } + if (!this._view) return; if (this._mapping && this._mapping.version === version) return; + //if (this._mapping && this._mapping.version == version) return; + //this._view.dispatch(this._view.state.tr.setMeta(MENU_PLUGIN_KEY, "remove")); + // Hack to forcefully remove the 'old' menubar + // document.querySelector(".menubar")?.remove(); + //document.querySelector(".progress-bar")?.remove(); + //document.querySelector(".spinner-container")?.remove(); + // this._view.dom.remove(); this._translator = new FileTranslator(this._filef); diff --git a/editor/src/kroqed-editor/progressBar.ts b/editor/src/kroqed-editor/progressBar.ts index 531bf732..2c025411 100644 --- a/editor/src/kroqed-editor/progressBar.ts +++ b/editor/src/kroqed-editor/progressBar.ts @@ -32,7 +32,7 @@ function createProgressBar(progressState: IProgressPluginState, progressBarConta } let progressBar; - + if (resetProgressBar) { // If resetProgressBar is true, remove existing progress element const oldProgress = progressBarContainer.querySelector('progress'); @@ -43,7 +43,7 @@ function createProgressBar(progressState: IProgressPluginState, progressBarConta // If resetProgressBar is false, we just update the existing progress bar progressBar = progressBarContainer.querySelector('progress'); } - + if (progressBar == null) { progressBar = document.createElement('progress'); progressBarContainer.appendChild(progressBar); @@ -62,7 +62,7 @@ function createProgressBar(progressState: IProgressPluginState, progressBarConta const progressBarText = document.createElement('span'); progressBarText.className = 'progress-bar-text'; - + // Set the text of the span if (progressParams.progress.length > 0) { progressBarText.textContent = `Verifying file, currently at line: ${startLine + 1}`; @@ -96,7 +96,7 @@ const ProgressBarPluginSpec: PluginSpec = { if (progressParams != null) { // If lines are being progressed, we reset the progress bar const resetProgressBar = (progressParams.progress.length > 0); - return { + return { progressParams, resetProgressBar, startLine: resetProgressBar ? progressParams.progress[0].range.start.line + 1 : 1, @@ -129,6 +129,10 @@ const ProgressBarPluginSpec: PluginSpec = { createProgressBar(progressState, progressBarContainer, spinnerContainer); } }, + destroy() { + progressBarContainer.remove(); + spinnerContainer.remove(); + } }; } }; From 3de6c4efcfb2f9e8a3b147cd9f0518b77cd43d40 Mon Sep 17 00:00:00 2001 From: Alisakondratyev Date: Tue, 22 Jul 2025 16:02:03 +0200 Subject: [PATCH 61/93] checkpoint for new flashing button --- src/webviews/sidePanel.ts | 47 ++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index d3f69fa3..fa260b44 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -26,7 +26,7 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { this._manager.on(WebviewManagerEvents.updateButton, (e) => { // Update the transparency of the button based on the event // This is done when a panel is open - //this.updateButtonTransparency(e.name, e.open); + this.updateButtonTransparency(e.name, e.open); }); } public updateGreyedOutButton(buttonId: string, open: boolean) { @@ -38,12 +38,20 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { } public updateButtonTransparency(buttonId: string, open: boolean) { - // Set the transparency state of the button in the transparency map - this._transparencyMap.set(buttonId, open); - //update the list of buttons that are currently greyed out - this.updateGreyedOutButton(buttonId,open); - // Update the button styles to reflect the transparency state - this.updateButtonStyles(buttonId, open); + console.log(`Updating button transparency for ${buttonId} to ${open}`); + // Instead of setting permanent transparency, flash the button briefly + this.flashButton(buttonId); + } + + private flashButton(buttonId: string) { + // If the view is not available, return without flashing + if (!this._view) return; + console.log(`Flashing button: ${buttonId}`); + // Post a message to the webview to flash the button + this._view.webview.postMessage({ + type: 'flash', + buttonId: buttonId, + }); } private updateButtonStyles(buttonId: string, open: boolean) { @@ -132,6 +140,16 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { opacity: 0.6; cursor: default; } + + .flash { + animation: flashGrey 0.7s ease-in-out; + } + + @keyframes flashGrey { + 0% { opacity: 1; } + 50% { opacity: 0.4; } + 100% { opacity: 1; } + } @@ -210,7 +228,20 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { const message = event.data; switch (message.type) { - // If the message is for changing the transparency + // If the message is for flashing the button + case 'flash': { + const button = document.getElementById(message.buttonId); + if (button) { + // Add flash class to trigger animation + button.classList.add('flash'); + // Remove the class after animation completes + setTimeout(() => { + button.classList.remove('flash'); + }, 300); // Match the animation duration + } + break; + } + // If the message is for changing the transparency (legacy case) case 'trans': { const button = document.getElementById(message.buttonId); if (button) { From 2e658c1238ee5a7586419a6113f7a1727a637b32 Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:38:04 +0200 Subject: [PATCH 62/93] Add pdf version of tactics sheet and add getting started to readme --- .vscodeignore | 1 + README.md | 15 ++ scripts/tactics.md => docs/tactics-sheet.md | 206 ++++++++++++-------- docs/tactics-sheet.pdf | Bin 0 -> 90812 bytes scripts/README.md | 20 ++ scripts/createmd.js | 8 +- 6 files changed, 169 insertions(+), 81 deletions(-) rename scripts/tactics.md => docs/tactics-sheet.md (78%) create mode 100644 docs/tactics-sheet.pdf create mode 100644 scripts/README.md diff --git a/.vscodeignore b/.vscodeignore index 91cc25c5..51cebbdb 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -23,3 +23,4 @@ tsconfig.json ExampleFiles/ developer-resources/ scripts/ +docs/ diff --git a/README.md b/README.md index 7309afbd..b6b99a66 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ The Waterproof vscode extension helps students learn how to write mathematical proofs. +1. [Automatic Installation](#quick-install-instructions-for-windows) +2. [Manual Installation](#more-extensive-installation-instructions) +3. [Getting Started](#getting-started) + # Quick install instructions for Windows Install this extension and follow the installation instructions that pop up. @@ -132,3 +136,14 @@ eval $(opam env) opam install coq-lsp.0.2.2+8.17 opam install coq-waterproof ``` + +# Getting Started + +### Tutorial +To get started with Waterproof, we recommend going through the tutorial. The tutorial can be accessed in VS Code by pressing `Ctrl-Shift-P` (this opens the command palette), typing `open tutorial` until you find the option `Waterproof: Open Tutorial`. + +### Tactics +Waterproof makes use of 'tactics', information on the available tactics, together with explanations and examples can be accessed via the extension or through the repository: + +* From the Waterproof extension: Navigate to the Waterproof sidebar (accessible via the droplet icon on the left) and click on the `Tactics` button. The panel that now opens shows all available tactics. +* From the repository: The repository contains both a [Markdown](/docs/tactics-sheet.md) and [pdf](/docs//tactics-sheet.pdf) variant of the tactics sheet. diff --git a/scripts/tactics.md b/docs/tactics-sheet.md similarity index 78% rename from scripts/tactics.md rename to docs/tactics-sheet.md index f3f3a591..e216e4ea 100644 --- a/scripts/tactics.md +++ b/docs/tactics-sheet.md @@ -1,10 +1,10 @@ -# Waterproof Tactics +# Waterproof Tactics Sheet ## `Help.` Tries to give you a hint on what to do next. -```coq +``` coq Lemma example_help : 0 = 0. Proof. @@ -17,7 +17,7 @@ Qed. Take an arbitrary element from (\*type\*) and call it (\*name\*). -```coq +``` coq Lemma example_take : for all x : ℝ, x = x. @@ -31,7 +31,7 @@ Qed. Take an arbitrary element from (\*set\*) and call it (\*name\*). -```coq +``` coq Lemma example_take : ∀ x ∈ ℝ, x = x. @@ -43,9 +43,10 @@ Qed. ## `Take (*name*) > ((*number*)).` -Take an arbitrary element larger than (\*number\*) and call it (\*name\*). +Take an arbitrary element larger than (\*number\*) and call it +(\*name\*). -```coq +``` coq Lemma example_take : ∀ x > 3, x = x. @@ -57,9 +58,10 @@ Qed. ## `Take (*name*) ≥ ((*number*)).` -Take an arbitrary element larger than or equal to (\*number\*) and call it (\*name\*). +Take an arbitrary element larger than or equal to (\*number\*) and call +it (\*name\*). -```coq +``` coq Lemma example_take : ∀ x ≥ 5, x = x. @@ -71,9 +73,11 @@ Qed. ## `We need to show that ((*(alternative) formulation of current goal*)).` -Generally makes a proof more readable. Has the additional functionality that you can write a slightly different but equivalent formulation of the goal: you can for instance change names of certain variables. +Generally makes a proof more readable. Has the additional functionality +that you can write a slightly different but equivalent formulation of +the goal: you can for instance change names of certain variables. -```coq +``` coq Lemma example_we_need_to_show_that : 0 = 0. Proof. @@ -86,7 +90,7 @@ Qed. Tries to automatically prove the current goal. -```coq +``` coq Lemma example_we_conclude_that : 0 = 0. Proof. @@ -98,7 +102,7 @@ Qed. Tries to automatically prove the current goal. -```coq +``` coq Lemma example_we_conclude_that : 0 = 0. Proof. @@ -108,9 +112,11 @@ Qed. ## `Choose (*name_var*) := ((*expression*)).` -You can use this tactic when you need to show that there exists an x such that a certain property holds. You do this by proposing (\*expression\*) as a choice for x, giving it the name (\*name_var\*). +You can use this tactic when you need to show that there exists an x +such that a certain property holds. You do this by proposing +(\*expression\*) as a choice for x, giving it the name (\*name_var\*). -```coq +``` coq Lemma example_choose : ∃ y ∈ ℝ, y < 3. @@ -123,9 +129,10 @@ Qed. ## `Assume that ((*statement*)).` -If you need to prove (\*statement\*) ⇒ B, this allows you to assume that (\*statement\*) holds. +If you need to prove (\*statement\*) ⇒ B, this allows you to assume that +(\*statement\*) holds. -```coq +``` coq Lemma example_assume : ∀ a ∈ ℝ, a < 0 ⇒ - a > 0. Proof. @@ -137,9 +144,11 @@ Qed. ## `Assume that ((*statement*)) ((*label*)).` -If you need to prove (\*statement\*) ⇒ B, this allows you to assume that (\*statement\*) holds, giving it the label (\*label\*). You can leave out ((\*label\*)) if you don't wish to name your assumption. +If you need to prove (\*statement\*) ⇒ B, this allows you to assume that +(\*statement\*) holds, giving it the label (\*label\*). You can leave +out ((\*label\*)) if you don't wish to name your assumption. -```coq +``` coq Lemma example_assume : ∀ a ∈ ℝ, a < 0 ⇒ - a > 0. Proof. @@ -153,7 +162,7 @@ Qed. Example of a chain of (in)equalities in which every inequality should. -```coq +``` coq Lemma example_inequalities : ∀ ε > 0, Rmin(ε,1) < 2. Proof. @@ -166,7 +175,7 @@ Qed. Example of a chain of (in)equalities in which every inequality should. -```coq +``` coq Lemma example_inequalities : ∀ ε > 0, Rmin(ε,1) < 2. Proof. @@ -178,9 +187,11 @@ Qed. ## `Obtain such a (*name_var*)` -In case a hypothesis that you just proved starts with 'there exists' s.t. some property holds, then you can use this tactic to select such a variable. The variable will be named (\*name_var\*). +In case a hypothesis that you just proved starts with 'there exists' +s.t. some property holds, then you can use this tactic to select such a +variable. The variable will be named (\*name_var\*). -```coq +``` coq Lemma example_obtain : ∀ x ∈ ℝ, (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒ @@ -194,9 +205,11 @@ Qed. ## `Obtain (*name_var*) according to ((*name_hyp*)).` -In case the hypothesis with name (\*name_hyp\*) starts with 'there exists' s.t. some property holds, then you can use this tactic to select such a variable. The variable will be named (\*name_var\*). +In case the hypothesis with name (\*name_hyp\*) starts with 'there +exists' s.t. some property holds, then you can use this tactic to select +such a variable. The variable will be named (\*name_var\*). -```coq +``` coq Lemma example_obtain : ∀ x ∈ ℝ, (∃ y ∈ ℝ, 10 < y ∧ y < x) ⇒ @@ -210,9 +223,11 @@ Qed. ## `It suffices to show that ((*statement*)).` -Waterproof tries to verify automatically whether it is indeed enough to show (\*statement\*) to prove the current goal. If so, (\*statement\*) becomes the new goal. +Waterproof tries to verify automatically whether it is indeed enough to +show (\*statement\*) to prove the current goal. If so, (\*statement\*) +becomes the new goal. -```coq +``` coq Lemma example_it_suffices_to_show_that : ∀ ε > 0, 3 + Rmax(ε,2) ≥ 3. @@ -225,9 +240,11 @@ Qed. ## `By ((*lemma or assumption*)) it suffices to show that ((*statement*)).` -Waterproof tries to verify automatically whether it is indeed enough to show (\*statement\*) to prove the current goal, using (\*lemma or assumption\*). If so, (\*statement\*) becomes the new goal. +Waterproof tries to verify automatically whether it is indeed enough to +show (\*statement\*) to prove the current goal, using (\*lemma or +assumption\*). If so, (\*statement\*) becomes the new goal. -```coq +``` coq Lemma example_it_suffices_to_show_that : ∀ ε ∈ ℝ, ε > 0 ⇒ @@ -242,9 +259,10 @@ Qed. ## `It holds that ((*statement*)) ((*label*)).` -Tries to automatically prove (\*statement\*). If that works, (\*statement\*) is added as a hypothesis with name (\*optional_label\*). +Tries to automatically prove (\*statement\*). If that works, +(\*statement\*) is added as a hypothesis with name (\*optional_label\*). -```coq +``` coq Lemma example_it_holds_that : ∀ ε > 0, 4 - Rmax(ε,1) ≤ 3. @@ -258,9 +276,10 @@ Qed. ## `It holds that ((*statement*)).` -Tries to automatically prove (\*statement\*). If that works, (\*statement\*) is added as a hypothesis. +Tries to automatically prove (\*statement\*). If that works, +(\*statement\*) is added as a hypothesis. -```coq +``` coq Lemma example_it_holds_that : ∀ ε > 0, 4 - Rmax(ε,1) ≤ 3. @@ -274,9 +293,12 @@ Qed. ## `By ((*lemma or assumption*)) it holds that ((*statement*)) ((*label*)).` -Tries to prove (\*statement\*) using (\*lemma\*) or (\*assumption\*). If that works, (\*statement\*) is added as a hypothesis with name (\*optional_label\*). You can leave out ((\*optional_label\*)) if you don't wish to name the statement. +Tries to prove (\*statement\*) using (\*lemma\*) or (\*assumption\*). If +that works, (\*statement\*) is added as a hypothesis with name +(\*optional_label\*). You can leave out ((\*optional_label\*)) if you +don't wish to name the statement. -```coq +``` coq Lemma example_forwards : 7 < f(-1) ⇒ 2 < f(6). Proof. @@ -288,9 +310,12 @@ Qed. ## `By ((*lemma or assumption*)) it holds that ((*statement*)).` -Tries to prove (\*statement\*) using (\*lemma\*) or (\*assumption\*). If that works, (\*statement\*) is added as a hypothesis with name (\*optional_label\*). You can leave out ((\*optional_label\*)) if you don't wish to name the statement. +Tries to prove (\*statement\*) using (\*lemma\*) or (\*assumption\*). If +that works, (\*statement\*) is added as a hypothesis with name +(\*optional_label\*). You can leave out ((\*optional_label\*)) if you +don't wish to name the statement. -```coq +``` coq Lemma example_forwards : 7 < f(-1) ⇒ 2 < f(6). Proof. @@ -302,25 +327,31 @@ Qed. ## `We claim that ((*statement*)).` -Lets you first show (\*statement\*) before continuing with the rest of the proof. After you showed (\*statement\*), it will be available as a hypothesis with name (\*optional_name\*). +Lets you first show (\*statement\*) before continuing with the rest of +the proof. After you showed (\*statement\*), it will be available as a +hypothesis with name (\*optional_name\*). -```coq +``` coq We claim that (2 = 2) (two_is_two). ``` ## `We claim that ((*statement*)) ((*label*)).` -Lets you first show (\*statement\*) before continuing with the rest of the proof. After you showed (\*statement\*), it will be available as a hypothesis with name (\*label\*). +Lets you first show (\*statement\*) before continuing with the rest of +the proof. After you showed (\*statement\*), it will be available as a +hypothesis with name (\*label\*). -```coq +``` coq We claim that (2 = 2) (two_is_two). ``` ## `We argue by contradiction.` -Assumes the opposite of what you need to show. Afterwards, you need to make substeps that show that both A and ¬ A (i.e. not A) for some statement A. Finally, you can finish your proof with 'Contradiction.' +Assumes the opposite of what you need to show. Afterwards, you need to +make substeps that show that both A and ¬ A (i.e. not A) for some +statement A. Finally, you can finish your proof with 'Contradiction.' -```coq +``` coq Lemma example_contradicition : ∀ x ∈ ℝ, (∀ ε > 0, x > 1 - ε) ⇒ @@ -339,17 +370,20 @@ Qed. ## `Contradiction` -If you have shown both A and not A for some statement A, you can write "Contradiction" to finish the proof of the current goal. +If you have shown both A and not A for some statement A, you can write +"Contradiction" to finish the proof of the current goal. -```coq +``` coq Contradiction. ``` ## `Because ((*name_combined_hyp*)) both ((*statement_1*)) and ((*statement_2*)).` -If you currently have a hypothesis with name (\*name_combined_hyp\*) which is in fact of the form H1 ∧ H2, then this tactic splits it up in two separate hypotheses. +If you currently have a hypothesis with name (\*name_combined_hyp\*) +which is in fact of the form H1 ∧ H2, then this tactic splits it up in +two separate hypotheses. -```coq +``` coq Lemma and_example : for all A B : Prop, A ∧ B ⇒ A. Take A : Prop. Take B : Prop. Assume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii). @@ -357,9 +391,11 @@ Assume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii). ## `Because ((*name_combined_hyp*)) both ((*statement_1*)) ((*label_1*)) and ((*statement_2*)) ((*label_2*)).` -If you currently have a hypothesis with name (\*name_combined_hyp\*) which is in fact of the form H1 ∧ H2, then this tactic splits it up in two separate hypotheses. +If you currently have a hypothesis with name (\*name_combined_hyp\*) +which is in fact of the form H1 ∧ H2, then this tactic splits it up in +two separate hypotheses. -```coq +``` coq Lemma and_example : for all A B : Prop, A ∧ B ⇒ A. Take A : Prop. Take B : Prop. Assume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii). @@ -369,7 +405,7 @@ Assume that (A ∧ B) (i). Because (i) both (A) (ii) and (B) (iii). Split in two cases (\*case_1\*) and (\*case_2\*). -```coq +``` coq Lemma example_cases : ∀ x ∈ ℝ, ∀ y ∈ ℝ, Rmax(x,y) = x ∨ Rmax(x,y) = y. @@ -387,17 +423,19 @@ Qed. ## `Expand the definition of (*name_kw*).` -Expands the definition of the keyword (\*name_kw\*) in relevant statements in the proof, and gives suggestions on how to use them. +Expands the definition of the keyword (\*name_kw\*) in relevant +statements in the proof, and gives suggestions on how to use them. -```coq +``` coq Expand the definition of upper bound. ``` ## `Expand the definition of (*name_kw*) in ((*expression*)).` -Expands the definition of the keyword (\*name_kw\*) in the statement (\*expression\*). +Expands the definition of the keyword (\*name_kw\*) in the statement +(\*expression\*). -```coq +``` coq Expand the definition of upper bound in (4 is an upper bound for [0, 3)). ``` @@ -405,7 +443,7 @@ Expand the definition of upper bound in (4 is an upper bound for [0, 3)). Splits the goal in two separate goals, if it is of the form A ∧ B -```coq +``` coq Lemma example_both_statements: ∀ x ∈ ℝ, (x^2 ≥ 0) ∧ (| x | ≥ 0). Proof. @@ -420,7 +458,7 @@ Qed. Splits a goal of the form A ⇔ B, into the goals (A ⇒ B) and (B ⇒ A) -```coq +``` coq Lemma example_both_directions: ∀ x ∈ ℝ, ∀ y ∈ ℝ, x < y ⇔ y > x. @@ -440,7 +478,7 @@ Qed. Prove a statement by induction on the variable with (\*name_var\*). -```coq +``` coq Lemma example_induction : ∀ n : ℕ → ℕ, (∀ k ∈ ℕ, n(k) < n(k + 1))%nat ⇒ ∀ k ∈ ℕ, (k ≤ n(k))%nat. @@ -460,7 +498,8 @@ Qed. ## `By ((*lemma or assumption*)) we conclude that ((*(alternative) formulation of current goal*)).` -Tries to directly prove the goal using (\*lemma or assumption\*) when the goal corresponds to (\*statement\*). +Tries to directly prove the goal using (\*lemma or assumption\*) when +the goal corresponds to (\*statement\*). ## `Define (*name*) := ((*expression*)).` @@ -468,33 +507,40 @@ Temporarily give the name (\*name\*) to the expression (\*expression\*) ## `Since ((*extra_statement*)) it holds that ((*statement*)).` -Tries to first verify (\*extra_statement\*) after it uses that to verify (\*statement\*). The statement gets added as a hypothesis. +Tries to first verify (\*extra_statement\*) after it uses that to verify +(\*statement\*). The statement gets added as a hypothesis. -```coq +``` coq Since (x = y) it holds that (x = z). ``` ## `Since ((*extra_statement*)) it holds that ((*statement*)) ((*label*)).` -Tries to first verify (\*extra_statement\*) after it uses that to verify (\*statement\*). The statement gets added as a hypothesiwe need to show{s, optionally with the name (\*optional_label\*). +Tries to first verify (\*extra_statement\*) after it uses that to verify +(\*statement\*). The statement gets added as a hypothesiwe need to +show{s, optionally with the name (\*optional_label\*). -```coq +``` coq Since (x = y) it holds that (x = z). ``` ## `Since ((*extra_statement*)) we conclude that ((*(alternative) formulation of current goal*)).` -Tries to automatically prove the current goal, after first trying to prove (\*extra_statement\*). +Tries to automatically prove the current goal, after first trying to +prove (\*extra_statement\*). -```coq +``` coq Since (x = y) we conclude that (x = z). ``` ## `Since ((*extra_statement*)) it suffices to show that ((*statement*)).` -Waterproof tries to verify automatically whether it is indeed enough to show (\*statement\*) to prove the current goal, after first trying to prove (\*extra_statement\*). If so, (\*statement\*) becomes the new goal. +Waterproof tries to verify automatically whether it is indeed enough to +show (\*statement\*) to prove the current goal, after first trying to +prove (\*extra_statement\*). If so, (\*statement\*) becomes the new +goal. -```coq +``` coq Lemma example_backwards : 3 < f(0) ⇒ 2 < f(5). Proof. @@ -506,9 +552,9 @@ Qed. ## `Use (*name*) := ((*expression*)) in ((*label*)).` -Use a forall statement, i.e. apply it to a particular expression. +Use a forall statement, i.e. apply it to a particular expression. -```coq +``` coq Lemma example_use_for_all : ∀ x ∈ ℝ, (∀ ε > 0, x < ε) ⇒ @@ -527,7 +573,7 @@ Qed. A synonym for "We conclude that ((\*statement\*))". -```coq +``` coq Lemma example_choose : ∃ y ∈ ℝ, y < 3. @@ -542,7 +588,7 @@ Qed. Used to indicate what to check after using the "Choose" or "Use" tactic. -```coq +``` coq Lemma example_choose : ∃ y ∈ ℝ, y < 3. @@ -556,9 +602,11 @@ Qed. ## `By magic it holds that ((*statement*)) ((*label*)).` -Postpones the proof of (\*statement\*), and (\*statement\*) is added as a hypothesis with name (\*optional_label\*). You can leave out ((\*optional_label\*)) if you don't wish to name the statement. +Postpones the proof of (\*statement\*), and (\*statement\*) is added as +a hypothesis with name (\*optional_label\*). You can leave out +((\*optional_label\*)) if you don't wish to name the statement. -```coq +``` coq Lemma example_forwards : 7 < f(-1) ⇒ 2 < f(6). Proof. @@ -570,9 +618,10 @@ Qed. ## `By magic it holds that ((*statement*)).` -Postpones the proof of (\*statement\*), and (\*statement\*) is added as a hypothesis. +Postpones the proof of (\*statement\*), and (\*statement\*) is added as +a hypothesis. -```coq +``` coq Lemma example_forwards : 7 < f(-1) ⇒ 2 < f(6). Proof. @@ -584,13 +633,15 @@ Qed. ## `By magic we conclude that ((*(alternative) formulation of current goal*)).` -Postpones for now the proof of (a possible alternative formulation of) the current goal. +Postpones for now the proof of (a possible alternative formulation of) +the current goal. ## `By magic it suffices to show that ((*statement*)).` -Postpones for now the proof that (\*statement\*) is enough to prove the current goal. Now, (\*statement\*) becomes the new goal. +Postpones for now the proof that (\*statement\*) is enough to prove the +current goal. Now, (\*statement\*) becomes the new goal. -```coq +``` coq Lemma example_backwards : 3 < f(0) ⇒ 2 < f(5). Proof. @@ -604,7 +655,7 @@ Qed. Used to indicate the case after an "Either" sentence. -```coq +``` coq Lemma example_cases : ∀ x ∈ ℝ, ∀ y ∈ ℝ, Rmax(x,y) = x ∨ Rmax(x,y) = y. @@ -619,4 +670,3 @@ Either (x < y) or (x ≥ y). We conclude that (Rmax(x,y) = x). Qed. ``` - diff --git a/docs/tactics-sheet.pdf b/docs/tactics-sheet.pdf new file mode 100644 index 0000000000000000000000000000000000000000..77782329648887163e883e97d8921eb6891c991e GIT binary patch literal 90812 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

3Mw9*Dq*H{Y>Fh_-haNS&j*7pH>YS zqRT#P1{qY!Dqq)wzs0^nIR*`r_pvf(`M715+C6(7iTl3ZHi`IuHi-ATYF_u zj(LhWV4xqqg4sJ-((ulKG}uGnGTXcwT&Q|zym5WBR%u2D);rO~FtUkiNmRd#)VL!u zlfy{qN@Jb|h+Wf^(!YzL-$%MMpghzCs0%qf}+KmEM@Ke0uRfI(`eJCj;u) z!bg%tf^&UhA<(Vd@*wY*EAN$b@-B4m(c$lRr~|C!llp7un(kB6)=J?W8tw+}6KX=& zprh<;2*ixmM;}=yf-kEIHT8+DkU19)L?Ls@HbwfVokaI#-H2xE9_+>XTS)Eot+Z5| z-@uLX7zd)$j&T8XAQL|TPq8brO)n_5A9Oo|0CVV+(Q7|C(bXR$Es)#SZ+R0+u)28A zMgV)v(o@{>juR~K^AjJK)ZY(ZLf7;XAV02Dbzi_N6eG{>KQpO@*^X2>&~3bFmWM?ZY$m_ zrvG;2VEZ1-m3sKd4DYv~i|Z>iAcYnHHO(gFVvgcwp78|5q`FYatZSMIcp&bGm4oxTp-q06<|XH>C`7@7}M1yGr7GM&xoX+xHq4L<%ggzukm2VNHYFV#)G0>@@uX&-{nJ|oQ^oiIB zk6Dp1v~fB_Jd89`30S%y|79<^`2%_bV=o) z$QGovT<2f~1vX_jW(0S2FL|N|t@;An7eBgA(zub0lf97hPi^^a$LZ9|!_VcO%A>{* z@4G=7W@>~H&-9QQep2#$ePOGnUfbhK-i0~f36jj)g05Bg`Q1JBR1KNAk{+wP9I)D1 z!v8!qnb#e<&{sTlY@W$Xv3N|$wBlC4HSOmd&Dg5^k=d~=5u4^<(_GliI+59S!vPp8 zUFj7lorS*CWie(*c+#$(^`5<%h~B*DYt_2=_WMq+^;q(uShkFk$Ujo+#$#sjhdCbZ zw~3)V`}2-m8s`B#vVkE1j}~77o`-tcBc$Y)=HWyU+Mze;mL|> z3vbWr?fIbsC_E`g=p{#VgszO~Mb8?CnYR1sX13E8tBWJ4O7(rhxG*c!QtPSZ7GyhM z|AZ}pZ**~!d-p@e%OPc z43^OKEQ;U0SL7)$;8$^Z!1lM;?i?9Xt_JTIt$SJ_0|`<0X@Ot`_uLR5B>42aB658r z^4TT#e=bNHa?-rHd_YvZ-uxscQ#ARR?NZ8BItpEcldeszs$neFS6k~%Jbl3qxX^6I zI~`=T{M=$?zp#&{s?KMHVp<&Dx{%})b;qQc?QpB^a_0)G>KjoEZxVdNDr|?G;p~w> z|14aAOfTbVAvJ~j%nane)fie#tF0=I7eHsm_loUD_O-$A4wu>r|MzbF=Uyqf+x?yF zmo>0Zbh5^ymBFK@qx+xfe+Ne=JXUtb|NkEW{56~YaVT&|P1+Wd6`|`w^)~I5&wdo& zR1Ji^(SPHwS;`XUQwlzytXWWvq6QWB`Oz(Z*hE}qbq&<4m$tdo{kO3l6Ru5?ru9(4 zVMu;b1}ph|QP@DVZ%7NfTA^3lCEQ+Bt9-2Ppb)8?Mig)r<#Xgzf}Dng6j|Gd6g{S1 zu|cH-{d;gJ40n$dAP4B3AWo1?&(p zz$lag5eTk!Aw2CU3@HgZWwZb_DfB~#K`{&x5!gT`nBd#|z=|Y#@?dX3NGM<7Jjze8 z0D@aWx6<2!SrB~EAv_53B`GB42tEzaB0yS%lQyaMS0`UXptLYUgwK}#NBH2{ zf2At>H5Hd7N4q{tj%=&k2rI4Ot;Aj_E&7RXKMa-M5%7m028C!GfWNsfbF=1<*o-iv zPC>!?30v|OCHQ`bT*i}_rs$42CKQ<+C*i@{?2OKGc9@%koL%828A{|Vw z6SjK;b5zk*`C70cp=R+MGgMNypHTf%D>?H5XNV!k09&9AZfu{2>15Y_&`L=3vTURj zsqK;>Ya}<=0jKwt<&`|-V>T51TChaROS~$6Fu@DY!<`@M21;SyjDI|C!TgL9_4x9R zV=;`!OYKbX1@&6+RP4}@tsg4Bi-GoFq7#PvVO}wUQRb<9K5RHLYsAul5aXtd$I+6= zt>bY}6hY`E=7HgxjaD$`K_Rpn1MDZ!DDg8@HoJ&b4FUgz)Yb&sr?PRcMi_UQ^3 zc8I4u#&fSc5xN88uI1IRnDSU$soaZ2C1cgt4ABeu2>|ZqKG~}?@6!QIFgF~Rw6)Ex0nGpk@maNP37{(}8V<=ESPzQz{K|HIff24@xp z+kUY*u`{u4+x8b@V%xTDXJTjKOl;e>ZNIs7-+TA|xb^DQ{&DK;s(q^a$L>?zz1He= zv*Fox#dF`yJSUO6i{7*ejq%sRGfM}jz0)Q>Up5)#8-_o~eG*pmRa#msEk{e43Y&|C zR$mRK7@V4ZTUvUqM;e=#cf=19nSR{g=cbRIj+`^CgeM9)hc1}8eiIUF&klrl-8n5C zSZ2hv??0?;xp(~VZ%l#;!_>DQ+g2Q#jt;Fb9rG&fK8uoekMrP%weoKcyi=cH`Poyx zK_YALy#9v(%g*%wnZUFDH@rI$(SIS{ty;goPunt{qVD{@zsGw91Z{fy7)Jn6pkP5@ z$@qYuSm@YT?nED80O@tu%>T)N{~qK2Ut%sy930H7|M!phUj|%u!g!;Jt#RrsE^a!R zGAF$gvGB2wf{~IEBMbK>ND&DVL6PtBi!c*|IHrR|hx?ECh(bgVbr41Q-vX4F!a*I~ z1ER{x&O@My3u)UUm89lgbN`{7z_r}AKf9j2{=+HOX;l}uMV0AK-bFq@A2cx^FdEv? z)Kp@y%7$tUNkbkidYop8E{px>Vis}D*rI7M8DSCV6ddJJ1Ww@*S8R85-`bW$H=7gm zBVfW@R!ExP@n?Cp+(UxT4Tc(17bn>6{qZ!(AsDBSFtiu|@Oj26dKl_mw!zpTAD^mw zyog#g0A|XDA8|E2d@J?=X4}{8piUnlvpd%ov~j+%z5%t7wt;?y^ZY}KRAdx9QKn*~ z^T4+uvx-b1zB>`_!c8Bmd??8VC>6YT9H>@We8?I1ijBviOf>0^MnR6 z3WG@qk47FfYEarCpjFdbQHPkHmY@6zEf4iMGHh^u&n^@S9tlSsM;&(w+Y(_N-j9jb zV#UUN&3@*lVprE#1{$*v<`(7_sXkaV6dpDMZx6PO!bE7KAwEqcHobGQbMo|v*Qv?l z&#S}zk0Z0m=`5s7eU@$u-{p^xw%(7NSF?LN41Jk!vcW`KY`YBe4C@R_Epv}q$G*FO zLzeO9)N>52IcZla*~lc+-^9ultiVkAO}gr|W@!c~sOoIBI#1wdwa^lB<>cBXjwwQm zI8Xvuy3T8z%WjudE7$PYjL?j}y0k)2&u8J)|5S4sdFj<*{@et$R{ccqWbakw6Yi7F zx0q{7Xgpo7P(NMQaUu4^{NDNWk3spw_9SsRy*lmO5+)W>4YQ8(B)W2sxK-f5iC2Vw z8cRFOIm{}NVq9cgyI;4TzK<}h5lh3@eZIOuQ?Z zE`Ig2ZnJv1(&_u$-*KKP&+arSlSyc^we(?qpK`?Brfi$Az6M-hOaGKK=kdkVA6mYx zZ4Eh}LO7gjW!fOVLbzgmMtMg61A5RLC~XRX6o_6$;YWO(K=_j3&Gn7k9aOuozZ-XF zcUStt5~u`?`Ud?YM8`0NGBOj9rY=&xE)d%QVsY;up4;J-PW>jx5Js~X))WF4Aqzo) zk*17=*NW4`ef)($GXSTmhk=xo^d~8|iR8#c{ObfP0TP$F_z}RVk!x7TybGzyAnO(T zp8p=?zVe7;;yPV`$?wOaAf>1#V8?z-pAuVis7_b)^q*NFT+{{XO~5u(BL$$R_rQMGAZiO}Cswbh8+$Fd zqE_vw)Pb64tb<)_UKC8g?p;Vdp*+nRKxjSPK3P2RdKLJ@c7j$J)ccTW_U=D*yS9WC z)`1uzh1ejiANL^67Pvit(~N-Ar0KQ-`qNt%P zqC7Lv)CHiIxj|hwU+Oiz6PrS`a#eb`+~;MTXLeiICX63=7(jF@(U^CvxK>>ntdH9; zGhOQjIUkU(pchdr*5*B*f_73H7_(L z*pRI4t?#XQHhVOduZ3?=f0KXz{HFaT^&`$piW`+UB)K#6D9fg$7`=5AXp6(63mZpV zC3i~#GZ9#ocuH+z-hR_QpRN{Y^xgEVaq&}eSE(%_kc%yuA(u&NqxxO0tzxfYpdy&$ zm-N;Cgu9Eou(I&tw2u^%hQq+_yZPd=Up~}=8^_MSskzMAQJ>G$?Eu{WJp>c8kL!dZ z!4_+cx0Jq-KEs*M)WB3&uVEZFR3~0OyRlNuRK}D)+dW%5+cH}^TRFR6Zq3T-$$Ey< z6q705mC)h6e`lBG*ZHx3=%@2_cc;&g*X4b7r=7Ol>ivEAPOqU$-=X8#_To5u*f;)% z{#ZZPgX__8@7MHr=r}>zW134^NN96tYUnmjCQc1UwiV~R{oh^tekX)!cHYe!)&82$ z9E3OyYYX=(LzVs_1Y8^f3-_{u5<}ho%+N&y8r)P4-j%vmLthaO5m&JGg-_)c8IUw@ zhI0BPeec&j8BG1ZFMWfbn9q9NS9>g&%lbM_4v&NAm}$D%UJgqkfp9iGpI%ect{)PS*q zT!6fw?{N5+f&lZc>V)5E8x?CEq-MSWYXtzRnQy2@3&1HXBH|hCReoSp5KfF2z5!$p zF2D|K0@R;7zKHV(;0=C5CI|so@Y+E4e+SgGft(!pI`+ec;3q=ZNE#SG;lRP15Y#`_Kl>FQpdkEBWquCO^XDH)^@9K$ zi0OqsQ34hU!4F2dVFX`!;OTC9f-aytks9yvNgUzFBuV^X-u%MYp^3oXwnf&V4DcR= zT>C@;`AJB@7$zkCv>tURAZL0Y?#B{Dq>%u{=ezP5OF;h9frLn7oO}J|L3I+RJCi*81_cHV6z;6#EhAz?ElTu=!L%1BIKnBLhvEN zTMa;Z!w@Rnvx}^O7pw#^KgbJs5`uU+&JTG+FbLw^VXjj5_aj(yfxW5QW&&)60>y?Q z^A`bDclr8YRfYhsJwu!RB-ooBiqK8in|u|pl~9n6IH+zme;=$ey6j*K>wz2UhwU(o zi#|Z}gdg-fPMs9V3-vdZFmX00=n1nZ0-znu{Jh8?;E8Yx26#tzo(b#>_P+v0GB;#E zMJ-=Zm1ON9I`Ibg>2!`2%0pp8b*-pl1grk{* zx+9Y5!|_eytDzlP{XlO*_6SYGm5v|>2`9Dzg1pFvnC79HzrL{Fh%{Jq!7e1gA%&_& zgnb|nOhE|fh^*=b8XxdXNCLg-NI=T$0(9th1X6Ejp!Dc|pd*jx=T!N-b2O@;BNGp4 z_LS1W6(H3Qfp!^rpuEKi!tnx$Lc@FFAYK{-|DqF@Xi<7Ye#*>%Dqw{F*lU>yyr4zy z9#l6M(!+=k8_N+Vt;0lLi3evVUM)akCk>&>56r1SgFuGz2@g=_1-x`oLjrf?Kyj6X z^b-9wVD=(mhop+o6=&b)(G?LwF0tnO$Sk&b~&_{ZxDf2uYdCcyEGvY|0?$Gv@f^{;+!qY`Fe+5bbq+e7G|OpzDnu z)cK*0*2sTs48ASveq_~xT88Ja=g&e0YJlOhLjhhwa6aZB9UB9(7kmmrc&%vy`LoxF zV%PljTtFJx0Cgoe?u~q~fO;_mrY*?&en{8+Ezrvj*fOCK_Ha!0kv7Zlq~>dM#h_{b z%mn-kU4;56#QOe!h1a(A-q1@T9<~88FAhMy212Xhe$a&hoaKnplRd;je9=WcY#A~C zs?f9RO!XUWqhL4wUhtdUjPO-`kWE=H!KDGF>kS0kONLK0sAZYPg%$uAZo#0%^#?-2f8BFA3I6(x%g$DDmD(Hd*!$l0jDu(a~71DtXYW*GZa^er%0y($pEx{-9f_lluFiHYj za`neICzjys8O9?bp&J=Xr~w4qANb>40I*LfV;x{mdZ6$We{czkb4McGb%9wDz^KY?SP`0SAtNq2x>84@5zaK%hkBBeloCD+)sG|D=OGArcS+%}dC~ z!=H$XRuzP5-$vlxLSWK{Vlv@aVD30|zq#FH^yL`Qf$D0+{V?NrX-7P0#C>SR{m_PL z8o1(yaL%7DN&070?PNaK8?qwQqX^5*HoSu*RFfpjK$>YC8U8_)Y5F$&tss=6H6-5T zj#GPWK}hzT>>-S!O{n)4yq7n=PLdEL^ML54n)xwE4qde|_oex{z zF*%dq+dIlv89d+&;qzy(fGueLzP|rL$*&HGhYmhc@$8P_&nTZT<|HBln0R(I$rx5i zILLAdf)Rh-1eA<;%sX-Z5zJR&KQw<(C1hCfO-1P6esI4rX&Qv;P7z~&gyNM!oke&S z*rNq_7D=Lo6eC1A{naDpL=h$&IHlu)I`MP?b314D~Y%t@ui zie}7{L+6wr$3%^1bSdF}72&aB#E9(Ag0rH?2`9~>v7*a~%+JDu^9Q6Bp_`)vgV^(j zXYywCXAm2ITnZ}`Zk|v*uvUmn=bO#yJ>ac{{;EfR8&saaAHKN)mJ&U5BVV~)+!cA&qEttAMD^(WF>NpcVBQ-*G z2Bzlosum&+a-A7k>Z?X$2&?Br5D1Pd(mnNdQTwU+ODk)sZK@KV;{#(XXGBg68wvO#*+If^XVo7l9ys43vIf~<8u{IDO+C-b4JH_!74CpU@R=@BXXuCL}uN+nh5AS+` zaLJMzz|o8IFp26k0+wjSqBzalXnQ{Bk+{G+;XJiLfa?*`1qxK!un&G9Z(2b$MZ*RR zTrh97`ALFt@J}O4J;#ba3@Y{^ogf&mNDbBln|F#&@G5ptmik8E`nc%v>bjuuW_0h> zce8J>QlJ`-g9(v+?2vyhMe$Sf*b5yt!*xf#+hd=ArFR1kEXq#jGAH(Ji(##boqA!TorK*?dY+eB|d}@ZHVZ z{5!*A^pFmt%=e^#eI%a6Y>cHdkqglEgiPD@9Ak8it7%t&4iPA4d6M!7nX}xt^y;%L zqAB1w`K!1!fCR$2)?Kx?JwIi|)?rW_Y+{pIsi_wj+9Sd43xj zc#OM8S1a5c65m@srr@g0>$JK{F6_JApHsKP<8mCVvB`Rp&HS+5Iv71|OCpa;TCUm} zkwcPol-K?;>*P0Zs|)kpDC^xtK}y+RV`LMon^*2%e!acMQSS&C-IZ;X+w|z<^%)JmMJV8sEgl)+YS^4M;ui@yhm zU3OEI<2MD-KVwK z;g+Y7T3a?8f&J(BcjiKO`idYiN4r4Z=o(`4)X^zXFA9fgwsR`C63v z{JV>N6#u>mxy90@aZ`k9HoLpEsyjixI%!h%&dfNt7RU$ar^dUjGXlLOgQhSh&be_n z|J(aY3ZOjJwqo!Bgria5T^QoXb&QPMGXERRMj_@-iJv!X`rk407anIGoZV}*e4g2o zUJ-liZpsZzlajj@9{3eo5d&t21Wt97Qae&p)5B6`e}nSI{m>oGsn`~tKIknjOXtnD z=nD-~zn}Vgo#f-^rtPd|Ou1qIH{+OO3ggf65hcY8-iQ=YvD{ zcKhpwPt~@M;&k|o=AA&k7|lDRF=A88!{gfoLjl!82^GI&(&*RqIvk9vPh6x3+}niY zTfGqozt4F29nf#&LH@?N{-uu82ywAQ>+w1T*o`lyXSu(>dAz>aQ+xTdCa`R-Q`zq?{LuT_1P(IZw!i5*rwfyt9B(<|Ep-k4!pe?%u8&-ZgL)lLeHw9qm^#J0NpM zi3;H)%%d3GP!6vw0W;?rHXgtg;O+fs!5TW~z+YlD@X> zNW%l>n|;=q9K;G)o+a*}ktCd2;GZsbgCq|pH+`o%bFp4_g+GgP`0jNR3sA3h~^e&`w2wrVtK zbZY$3=+JNj$M2PBB{_ih6XpC#poFhPIEyz+FpKbQ_D@gY=lttwpR+@|WxHp4DE-GY zvP)iMsSyT5a`>6ezN~m{7n2_H5#Gi6RLxG*=_ge5Xfz?C=W08f)l#+oAJflgf$xvc z=jB?b<-MWk2;BIDA)A!#r0vx0gzf&>R)N_5+4V!O1F}1?J3e=^E<%PrnvAPVqFQ*U zO$(*;`LKmMo$5N}5KA`84s-H+Y<$XFQ8*QEc$v_6>yVY1t_96{5Q=}w+Dz9wUvzeh zw`vxTMYh?MDtGigHC9lct|D$+UWK*=ft29jkSO0fib`mSF(sP>P5fEzs^i2(sr|q| z-IJ2-VOZJ*)kPA-iS3wweSs0B!d^s=aT(@tAx*}HUP2^P<0ADU_S;yw;;LXnY8y^; zYGL+*)?YUWQ54;3?B|mk({;8+U;a)mE-n-f2k~B3ESNP41fJ~*6DiB4c-69nwQ4ie zb;F$bEl@Zh51z|!mdoa@9t)AFY;!fXO;2)rrrV{SZyqFjKg9B*iCFS`F$H} zH&?B`-P}cl!By(Ze4@csh7aBz75RPASG}*-f3jD58Qj-+MbTh9oZLo|`aI1A=1A)W z^8xf>Jr9V+$a&~ig4~IOc@t4)%Pk>f)h+Avq9!c$9v0j=;nPjF{l$nriu-!L7=<5psQ z#BGwhH5(lE6Ax)LU>Ukl&*G=YV_uyO^nKryZ2Qz+480lj%q()=$EI>>dA~25-qXLU zKz!!XsS7pD@{z|-VuSBM^Jx7=BLo@aY(@$}feLj8*e;)z9Yu=i=3rWVb4zRo8B2Bo zI}6qIeQgdLsnELyG}6|`_*GqquAcThGb|s7N@(8T>x4a3?SI|VoX}|Dm1xMEzE{6% z_g3ZqV&1BUyUuUs0h!*Z97W{d^YrZ)v5~L@p*hG7KP`JK_W|_yrW9K6PoL)PCZ78$ z#8C=1W4;{U{Gm=b?a<9KH!Ny&TTr&-<_pvbHqETU$?8ytBrym| zLG(>JHg3#p^9j30Rr>#^AJ-PG-%S#K((>1k( zn^1W)+!4>3kNt-1^5`r+J+8ERx5ko=ZG-oJ-GRRSHp9Ge;w!SAlfNBtT)Kzb0rx8!^tzQ6Cs4?sYMXsSCp4XQ^!_tu7+M;RsHXyVwUMmwowMP6x+6mGXK%uRXa|__Uo# zKCfKti(u8)nh%XvzGDMZ8H&?)<@gN5N9VT)R*J%uGvJp89R{n9D)QLdSw{GY{48BI z;`(p<#0jg(-KCaPCkf!u{0i&en#V#OhJTe{5^UR%mNVd$2SN2hq`&G(Q9uI~uy;bo zeunHs)JvqISRQ)&Nv*Usw^Y8#*ISx%xjkjP8)d3<6{mK`2lplt-mYx^pTz%>Rd4Bj zxK_v>t0%CV#@qrJbDEDCDR;hZQujd~YsVG4f5;KAbtLBE@=(k$_|D|Rfaq?~Szc;h z&`RQ?D3A~;mC1(&>r+jbw>Q;%0C%hj(NjP@bDaG2Hg#HE{z%%?mBq^em5=4)$23I4 z4nS_yv8g>RbnvnP>s{=epM``i@=B_ewy^iV20cl-=7Mql2C{mxil?=bdG7vVE(^{95(B z{6j70SKne;-~xwlL4#(~Q8m!GBRG)jFBXGFFp_+yM{MpwX8ND#APgH`RnNnMyGW?_Sk`z@XBQxE75YfS@+ceoE=WYG#0m7p;uWvo8 z?nU(6OP}{|*AwxTOWvHWBXNzdk>xq{?nc_R9q)jgb>~yh9a-gugE893MgV~`;?s;1 zq;A=&k-8OU3j2Z)6NQzFDO{Y7{>>`BqE#P5{-(Fh3HkgHfob&=YL&2I?`aIh?N1;Z6 zi?iBPJkp+3a`ng}}f{ zC&=XJLJ(>x5M?E?I1fyQJ77%HJe_;F;Mw7EszaA8!_}d9z~xK9aMjAy42lAQ)qBD) zr4pWakq@K5020feg_tguo${4RG)O!y)Mzfi(N4M``9>P0(LeZHD`ln-wXk1;n8`T# z1=2aD#28zJ#XRlybepn>GxB@nFx-B+Sj%v*^?JfcF0JIMm?d$CsFJjKMDVH<=5MEO z=T3@zgtCJqhMGJJY0LL6!WGP-58F0zMjM{B*cPYtrgaydkLGtf zq!ofzz7fX7OaBE<<9*pDP1E%H&o`uj`U$R-9Ly&DtAFjFFURYR;oN>GBt&n++xgNg zzc&bmZN|%!gFulVOWAmMnyxO-HD68k*u3Rv81tG*^w zXZj`G3cJ9BkeU>GwNA757_3QbZ(XJ%L$0 z8SKOJ;XI6mR%FdqAWQ&mG`(KS7oV^zvlc?GGhO4u+zv z<{xAGj?@)03s^DBwX+nx3s;+Ww>D{bl#0da*a8KIdkM$ncy#pK{93lD0)ul%grCaO z0)HJ>S;TGENN*2YAP2hP`=W-oG~;sa(kR?`o9t*72?vCrMka9|!OF4g^?U>etY-Hi zZ_`un)q978A8r<2XF5?to3V~48`W9$f_!+%VAYX&@fCiYI{n&mU}wN*NWfeiKem5S zVF>5Xy3S0|m`4?xS~6J>C|$V)^&tJ1B(5gNOXP3iUCbcYvTgZhHko!e<%07rU3mN* zuGfndBt4S|*V9`{*r}t~SPQ!RfEm<^Ij&KP*tFc<;@_C`=1f2Tt@FLoWil5@MzgeB zrPEmW1vFl(xyV=#<8No)tKa*v?dxT9RZ`|j|1~j8n8$VBqHxFnJV>?eh5AY2dZEIQ z%??3ffUdqD?2tsDeg~bR&zPhi%pzz^6z=aU7xoxu#Y9o4M1Z5ju@Uz6(`ishLrc8~ z!;FJ!puvU#4%Pdnch!wSOGSU%Okr?d!2t>=;Q(d1-2LHv4m(@wavc2^PSy6^u+y+E z7t*8WicHZ5aH?QVC^0O01yGoC)}2vAi6M@`Z)j zlDuQ|3fjD|e9rZ9p}H(In$oh45>bT8bLYb{TKNvK-E*{Cr2tY2`c0z=d{M;iQ&e3VkMR4m6m%i5uCQOAs0@NEPIaV1W2R5rGu!m6KQxy_7&q zS?$MQL=P8UKU;&Uj8B#E74p}7UMoDWN^=D z(df}=(&*AiH%33kPfk$IKu%H4UaloG9_J6f!Y`OU24pwpg)EHoaa%_o8?hqwdaZZ8NUd+Oa> zu3p`3&f49#pP0V=(<}WOkRPG^PWYpH2;0Vad|}c_d}6Wo{#Il7}GL#J+2=l=i~2X3`~%ib3q#4 zf5FpCJ1Xv)vc>l}tutidmyZn*l*j39P$`W@sIMkr3vsAxP~Q<5MO9Q08k#N%sg zm?;c6#4~-`_q|+XZfCB%7p@v0$q31lC&R2MokBAw0Q(ImnNrlz64u6Fc43}GLBd~d zSlOP<&B{=)>t{)cVPs0o(PG|lisa-E21H`y=doBJ&zw9BKVt=38dG~8a7$R1m>wFJyO;^#+SPS zI)6D)xNdETIb7P=k9K6N|G}hBzq>0_JFn@=^D%g;(r~)c{rbcGYn>#oi^T$y@Y~4b zKzFrBViiG18}o|Wq=hgxW-_a_70Im5TJvE*k7nP6YDACj1KYbkj-`jFjBgagq;Gfu zs?jnh6l|Z>6T4jH;YVHrxQ;KCtK@dtmBpt62^JV1wgk3pb2#z@DAv5K;F;W-hNL4y=4`A{LsKbj=sm7NS|%%e0U^7W|SI2@N4r_o_fxLpx$|1 zo>{zZ2yg&p?@o)8JWmHl`ic)3NW2*LECvdDj!(gOKX0-Hj=#ush(PSxn%V$j+e_P*s zeCFBlr|6o!lcP~SFDQ>KpRxX(3Z&%>hWoksB3O_`u8I5m&q==`*41bT4BucbbqBXF z;q)Ws-k`gh{v(l3B)1xi4u`aWJf9GY9*2^C4|)Py%q zAAX3WqJE_XkIUvvHqOBKnju1ytSYtJbd8`XF1I)twWvl*u7B`U-6oyL+wSTsJ7YL% zawV1p{t8f?-CihB$JscHGB^2SdXmeT!IIx#I!Y<#rQorj-Ii1(IdlKU4mkl z!xX+)qhD(PhNmTejwu#L%cM0LMZyylQ|Nm<18KHmPq~MgIWv{QNEPh5J5=UaM_4wu zgK%E$gEK1JsOwB-;VItO`J3?l*1EIs4=Z8up;QZsm*~_OlbSx7&v#jIU-E^vAK9DG zk5Ekl66X_OCs+$cmVxfskppOSG1XTHAB)=hvX_MUCBNUED8Lq|Op-l$GxA*%R?ny% zYk@o|UCo6MOwD?vo!Xd=GTMj^GTMZPW>*%k-YWP70?R|$X?T737$V3*dJM)7sB!M{ zuMUCrhpd(2t^L#C#N9ue-Z^T2*hA>cl~8qO?6f#Zl5R3Oh7VyJx|vX%RFbu8WcK`< ztXV}gZ=#>Qvb6NI&QvtA2Nou-)M47~bR$Ta42(*-Qr;b(lBp&+YYU>&-%r@6ac=8! zQTFWGaoDHWy)uRK+ZE^ZD_HIG9TwYW=!E2C9f|H!B#i_C?Z-3RU76nQU?@J!a)fIz_`cs^BE$ z_fFYub{@Afyu7~dCZ%J(c5+qCG`%C%H0x8im5SRs(F1jz26tO1$8y@g5XbLfXR2^D zy0nV-41K)azFK?C$!lUgy(QQh_Oruep=UWhM37<5KmzYKss!GV)d2&!jh#n$vQ1v_ zWp-xnT>u`J zp8UI(Au&S^R6q3C!7u8D?j}x}sP*`!KlmBeh30iW%8&*Vh0B|gr~b5_IlEUMzDOaS z+MN#iFK1D0eNK0+qmCP^ZP_&kRd_9VRu-eEQ=@P<4Y<2KKQ{M6>YiyRr^cA6gFZK+ zsx8n)vQs6_Zx*>fbn$V!gTFSp3kV=Upe~fCG@Iz6Sv()j_@IRgVXgUqHc{@@s9&O2 z^>)m7qC)CA17u#=BTy3 zM?w7l;&}YfQVq{NbDWIa+ZD&p4RjTfUG=}eMOXDtEg*lu0z!1+oYWd7)z0Xv{R#3F zHstM`B5>*-Y0-LTdantrRAY2Ph!n=ujdG2iPvu7^j zSqu`EKARo~S$fwU&aundIyEYEPxKD)$P&i<6cajVnD|K9t)EBd|f zzTfTa{V3q|Ld5y=J1bV{B7N0?_rZ>k$LH+ZkE`qUQRIRH_beluAf$*(QWTH5aPk2Y zzt7!T81!9V{iCj6-wkhmR>_T!ghbx;nQLIb9RC}(5JlkR>VC#=brUn5!0+-ZYNW@b z_1)5BFZ-$sF~t6S(+6o1avN#W2kgwuBk#+O@h{{Q;vY40!uf5Hj(0reeQUS8p2v(U zexy%{O&<`5ot6zB6qo+(j(^|SZ*P0&PVV{D_D-;6xWRJO1%eKQomEtg98+YYmJlb@FsmooYCPVOC`Cr#If zJwIyj0tSbsrmCf3PEuDTdmcO{8xdDU8nrJg`~(ILB$z(y5z@03{gx*v@RXbY2;Z8UQL!y$$7WQ zc?1$GnXV0M!(~1dZ4GEiR_fJrbhi zF(Ev*v9iuIGYfkeN!FHrPK9B8ZbHUh!o|&z*MuHj!<(LTexy*}%Ud^Q6k43PX_2M? z@^I9C*(w1Q!=aJC8$72E)0Mx%rF&ZNZLLZ`5ftABiK5B-i%B-CQ<3_W(`Na11 z3X5W^KL$2dfrTMPe*e#%hIGZ#R9U8urHmrik3-!m<$~tOQO3d~SM%=l(?7b)9G@+q zrV<}N_oCs8KtB9DcWPU6|bqi`%SoX9H3c_bJMopVO0K`_U26<8Mmu49 z@*&Y^^}Eqh`xo3=iE#yXCj>35;zoil>7#^Gxoic`34`gD#(;MlU!l?(&8Ct=sznVc3X6$AhvY2b=cYS(Ce}2qIHQhBs}1VB zNWW2)W3068!9K@~SX+r+LX|m}PVGZS9vqSl&%E&?^iLsG1~&;J%%a*>xUDA|bo|uS zD~=XZlr)~Lwsvy^{(b7a$(BuWhBfY&E*D7r?OYd|oGQ87oJQ@t3E89P;vDm8$9Itq z@}u6(!ga}skSjw+dIv9OY&>a<@m~pxi%dm-zR)wjSBzn`ey3?_@0QFC(w^n5NA+!9 z8oBe2LzBr~=N$fe@An*B5)1n-pH{CsS_PvLaOfd%;A-Zb{U~O?K!M4$upT}wkOZ!a>MFuMWG{7viw_rJp9O%KlNCr_ z$EFO>X2;DKv@h*nWHndW*guIbu>VDyJ^ux*9z%UT`>jnB_KKJzVWC7h8F0>_aVf1v ztzDnf;LJG=`a!bkcKU4`KX#+^Mw0&9T%aguYS&geDLtCfG}4JuF?nd}WU+oGHo#$3 zHdiXzEMyISsNcS|_9n59QjxVlku~|#59Y@z^qTw>G$VUn4R@xl z8s)gN6<5?H*mDp+kWh<3sBqKO8*NeSxq~eFRkwDgSPKyN=6f1FSio3v=3i zhKNz(r0V#ID7+1+sdob|}_iX#uuq<3n%P9(7{_g$|6}!ok#=4(`T8wIL3wX*Vk!k3{k1g`ce1e9=YI zET=59v1SFzajhxD3O>K?XI#WVuK)Ra(1hB%Bm z{Nw1vYN^s~WQ)ofm489Wlc%b{L!+ckd#ODHS@E^BP8f=;(uXodtgp8$c1a zNmC2{I6^MDS`3kLFLczL z8=d%%`RZZetCky8Fvl}AB3m>te9X$)~9xA$pFY2^S@isa=%pzD-iOs2*rZ!Pe$%GeEHjz151DL2U`dTJAW9hW> zPZe>_gMKK$NG#ePk%W?}8k|j)Qlngb%ogbHs8B&U0a0SwzM%}p+%w|TaiawUbycEA zPfP8v9dB0E(mkRc2?_a$Uf8}xH&tEo5+!pV1shpba5z;BJ|BcgG&wnKmQ7Pw^p&EG z=&Uw5NoNLk*g+j>n!?EZA8ccqs{Ev~kfS4zL|E>koZLul4P{z-eH6{B4S8r=p}pK? z`Ej{uL8H+P%kr|Qaxj_0n#2vGU$mF1iiXOYjUC}|7gNQIjNOgA-NFqnFNHBRo4z6q zO3O`5^#QJP8~iNu5L=@4>H!3lKklWYxS^dkRbcc;4pUPCq7J9B&kH%wzCiBjBB>^- zlVP1`07aF*D|HOovM_QP)KU&QV47WGXxKyg1?H^9PIb;!WnEO2+U9g{gnIPXpi&Qy zCP#B1q5~LGlU)bm{I=`bX(5E*B%LSe@U|}3qY#JS(XsGw^U~+0^ZYG$d6Z5JnN5w+2btEkxnbnCTr<}CBoXlhf z7BPath=!pn_DK2{^;jvL^oYchJWH#qq+nluXs@$5;t>Gan~_j<6Y2bD}7Kj zX66t&5O~twyn<-vAz#QkncD*s(?jQYW_~svGGXa4>B$mO_~|il$8vr{$U_I(ktAw{ zemRCkl+Guw|Hauk#&#Bk>;7N!Ury~#ZQHi(PHo$^+o|2DZF_3lnA)~(&xezHZth9$ z$xYV#VeOrb>|`Z7>v^BwbLOw;+??5GNGWOI&ybmT(@|%jOZl|lHbzxu(~+$W6ZcIG zy|r4Hi%K-=kXAz&OX^i88?_?ca2!%SimWv6~u-SZg~XCiOg-dlX2aAt(&< zEKUg|yNjYqD-p(~p>sLviri?c#hf_Bgt%dHqm!vPCTyq>ZXzG7za>s%>gg;+35-bA zhEN}mx^yY|=jc}6o&wfBR&}4+K{ZCGj5g^sSrr@ny)~1|C>1FMD>WXqS;>5Jpi0&_ zJR@a+E}VlHo%(w^vgm8}((hA^B~rD|2~ePMkT#t&ELv|?XkuS==G4va8-~r=V=kwt zf*d^HMLZ5WUa-NK{zn46oQC?$l8NkiF~0FH8XtQ#*wODpY?Cdq*W1ZA zLUaQwuj;-!kcE^e9tGfbW#WW(wZnexXpdi8tWdBr^|nMlh_Z1^U6GX5WlRB>LdQ- z|G*@DcMf{T6#XPhA~3%7fp0q${_Kv>fk3Gl+qd*t*EGZV+JwHnNb%lXm))1*dl4gx zNev1SOV>dl12VO-gy|%YE{BbT)PRP-v5%++?7x%U{*md9A3e%_fz2J?XV$+xA-ipn z_YUb9-A{7i+C|mBy%D-q1m%PxKSCJ^V|v-~sk#o3*;-C)VDgYBGgHe9X5Jjp{}}0D z;_QmZ(n6)mN*v{8^6(<^kS4Pm+Xtp+ZTut3iXU}I$4weNW8#E;j(v^)>FM~~B;0S9 z-uZZ{vfCuQ-}LvrB1|Ymy(CdR`#oRbHc|$`miU2HvVWY36EGQovrpRlP#~i>LjHC) zdIwoQ9d({E>Qvv5!b6>8W_;U-QI<5S>;>Pi%xD}JaK2@N;`&6`A+tZ4-4#NPk(k{z zs&8B`OeQ~K{MIaB=*74CXjWKD8~?s4@mWNwZF0*&bK5G@>>fWECkmTsa;rg77V==l zo*)~CkHR*(uUK0pj+h{hafd?yi^e8Lij(~gna)F;6t?@mE4-htpaj-mDoK16u>Qbj zoE=*k0%vtMijB0Ep-{gjqBd&$M@DVL_=PE{R)=g{W?wL!CS???*myUWV1p zQlEk*ar6UjREW7vo(v9tUoU+kb(AMvMfEg2^hS}xiR=`jB!J`W z<`;+bf>p2xuVm%0oEaa#2w6}-a3A{!VLJS|Ac`>xB53S1IoK;W447CFNBaHM#n1ax z$#e5LdM=4$_Kf?G;u@J>1_seGue}$1f+SqwB7*c({vwv|!6U*&Zow@x>tXlRN5}QZ zVo6B5vcwc^na|gH`9iHSo_!;F`9!1Or(G4wXol4n9UAK=$e1=f`n#cR~A1imFdg zxU-)5j2XcmefLPD$FMC;Mz*t&EE8QDI4cqzdCmj$?3+(=r>bXmH-`wy!Tho6;eXE4 zI0$C^L|n6s#M@NGp+}BNYhREOF%(xCBSMObtPsV7>VJAm#;2xrl9F>#pKr2<7HH_2f;SGPxAZcQkrf!)&&JK(<^#fC>7hO+n)6*4`RqmA)))(3ioZ6G>CbN1jnd&Bw4kI5P z%TJlbpIsC^&G%gYz9Vc9e0kqHwYw1xKc^25YPXUnbyd}ZTKVsE8VNSHzHCr8GoD># zgT*_a+bPR$)ku~daNYbgl3yy<9#%8V-EYZ5&4$+tCeD&1Zl0Bns!td zeO|w=H2StswWzXSN9-!MtTa9_a&$+ABD!3j6?kK$ZgZWxRwBNAgk5chb704bUbbuo zb=Y3AbJK_VT&r$XpS zRXLpHxgNL~dRKFDr1a>e90v7gGw7d2hC?3Ch>$Wxi0KTFC5&taG($+*9GxN}pEFqi zPC`gp0z!$gC?A|DKA5J65XvH4BvJ$>;zvXx%swF#1rtfDTI=usERK1Or2j1US@(qd zvJ>UGl4ALYb}`-AsA3FJ`cS@%IK%;z1=ZI#F*qRQsQZ^|YLuoDGo z3*4bnQW#4NkgJYyuq1@a8;Qd@D%fX-wx~*-m&p<`o8AUX&LfujF8Rt0 zo+~VGa=?Re%5?c|q5b*FT79$J&V}7JT*>`9xWBKB{sh&TY+C;B7Rmnl% z?*D)#38*Z7a`;a1K5eD9H83zw)Q&J=K#KebU~rokg!l`A>_7`=toS1U&@yrKyTGEX z?N7QAWpaADjk(%y(EIlw1K|hE}k?FjMVRLOXaS zAkj`cd@C5|pQx73(u%8fJc&Ro2uhEU5aSl@MoP9$MP{_zF|ie&xtIk|C(tl|3IIOQ zB%jDj=sV61rRg+G;);`A1B%)wF1{s_LBwZo_7R=K*G_eUp)oj?@K6GbG2{zaTP_WG z9L#J##V%dGQmZ}(VJGbZjJjwHMFiw5U1&H$T^NTFE^RET^k3K;mz#KZ5pR7Sy3ah$ ziWfDf&4uB{Ugunkh!fv=u;Pbe+G28qnhb;tg!I@9ximiJ9uwPI3u~h0 zsb;}une~chu8yIt^&G=oYkgyVGry%z_BRX=FwkNsYuJVm&VhdewuqUyY|PfKD_OOo zhBaYbIC@vL-2J$evH7vHv30TQczbwGxbz$dTzrn+|DJ~&7V}3uZZ~g(ZpmImJ!;;? zU#4$MZZVMC!yCiO7c7aK=#x^-HOufW^r z>Hcth`tKB~e0d3cfy0t(fn*uJ8cUJ6-SpRFe+HD5+M;!hgCDAQx{i$(>q|$=>EGAh zSGtF;Q?F(6<)<0~wJ(tm<_Ci1uclA$F6|rTEUA#Wyw=SaTUBmjmA|^iC}PGOSU{BS zpyWia5g)=`s8Q5@+I|fpaSf_-6a*Lo*i=!)1y)H8A!kMyv!GxzR>|jj)1Je?L$CO2 z?|6ICq-*SX$ay1~{&m^uG(E-(D3~6j*O~jWwz2no;JR}&gSPrQlJ;+#o{NG_YqwSA z3$(|#3ssr^GLXL@cl`<)0vhYJgS9t?%seQ*BbTpZ5;-*LBh&{}ZEBwipBkUwB+g0k|bD9oVDJd%P59J;R( z?2T1iSuC{|UHK(2hr<~|(=_6*8RD$yK)XyNboM zQI$|NlQfdFlJuC%$!bjXCg(CzSXEhmctg^z+gH~-@cukW=nQ8 zU!|WSA8oIY5R~;wXd*F$f;>*Xcsh@ZRX;l0SokL#(g&27qtzvX?S-bs3zmQ&rs_NFc52t6y) z&33BIYg(3{{c|JDN%LrK8pw&^{5Wk!z{7oZp4{-A!R6sPqIstIzPMPU_j=k^lk;ME znI6e}Q1a#Q`hGGk@5a4(;{5N#6>3@Dr^DQEcB6n(v?1z}4qG*u@xV^`lk0$9Iac0CogOXB_a;gKjEfyx9Jg6DM23iZXk?S}=PgRgQNDZ79Nd=3A zh=SW(M8Q$Ov6uFd`EmY{-sVV^{*w9&5d-YazKlK1>=V17o;*jGt}i>x6LbLsgbw5! z@Eit}1JQaIpnHnM1iv;d$ct+L4md`_0y01X4$1d;2thYM08Xetz&?vHq(96KKA<-M z9Iydp&o2Po>xd`<$^(6k>5sYtBOzFje+m0Q3n(N6uEITN_;=(1(?jFI6kskI6zC!R zIUzKp0i7XW08@YAZ%&c`AGtTQ0@Mup4pXnHzj3h0w*u)MqEhMth+l>V%V#Skgb!S+ z4rIVQMM4WEQs7oW3KhZwnE_inQ2_h;n?NHj{LdT>JWn6DB#;5))Ecmpz<_uPix#Y^ zFb(SdZSfTJk$~6*Ul3P8`h+TRW1#nyMZ$IgFM09kH;Rv0RZn)IhwZJH34LDy zZ%jiF@&YV`sUQV>6}tgqg>atjKwmN_as|X?d*M*81#siyv7EQw(HjNrCT4~=02iXr zs{(O0gl-P(3xsYsDb$rVV0tTD6`jo>_-s^P%nAdZ%`z!H%{2b%7GrR%JEmY=bG_ZY zkZdMlVHr&dL7%H$!Z_B$2(Jdy{%ptJ@#VF``WYkZf`_iXqHtVhP~B_Gg4{nWdsh*f zOCj*rzKR3UABi66Rb~QnApE-_0sM`;`Juexz0T&_kgUhby=3MCUW5=Kt9jXQtSfq$ zBeHN_Nes0CweIHhq1eW#uH2340>F1zu#LvvH-_3xhT{q!jMG&rb3|cYaR}OLa_a#vjDL9 z2;d>r>jiZ^0n+>vL^rmMN@8Z(p#v6W<`}ak5WLggKN`2x@;hS)~Wtaz|YRyOAKovliA96E&ea5dSzF&@Jk(bMJ39sDuWe z2ZhW~gX(S#z~Tt_nlcEEHfWKX;UuT|1-|YQ$jiMhh-1C73IcfaAF5IS15R1YRENIe zqeaULet*WFD|HP@5I?V%+QcA!4q}iWhdPlr_H}3H5lkWGeLyahs4xWfxaKH+0}LE< zl>4AvaUkl5VDFcp9iu>Vu~eaLYQ5}POKGoHp_wej-dD|E>fwU zL2`|Hw~Pq&UIL~gL2?Pe-tWQuaKQNZ$RO7Y8W;`i7<=XUYgg0qsh<1vcp-loz^_yh z91BcPpuP7l4+_9Xx1p}>BpDIn{{?jU5;mlTf%N)L_8wt!EnCf8LbJ}Gf;p%4nHuq_ z_rbU0-Jt=V&6%I0Ft2wn2oc$Xwxb|;jQVmldbbAydxT$3&{43fqJeqihZ!1afnKN zu%37X)q!ZY3GOp7?wD2(tAoQ81`wa{5O|odJdJ@@E3nPYDb0e@6#*rSJLHw|s47AH zHIOe_eI=>`or?^)Xzv=VJKDg#%DlqG_;HmWyQaXNQphhIV7n1Sx@u5Xg`j?A33B-? zsQ>pFcq_ut1;B^ewH>R!HjrTmmZuz~-BLjBGtZB8r@>NS_}x$jxMBso9w9JR0CQjN zZU1z}9jS!e-$V4S1mRZ$V|s+jtOVh%2I1dDWYYv|)&YAx%LQj0DSSg!cjKyobt#u?(RT7CJ)YL(#!$hh^*~_}8zR_C)m;c>H!ri}v2PC8(>wdz{NZ zc<$-vFJK--yBsuKeFbV0@8ZMmuQvmUzx1TmNARqR_^K&?1Wuri_*sYle2K_Y3AJ`6 znDd!e_(0g;IRbo_7Xr2}2iesMR;$au`BV<=c-Gc=*3#k(c6(k!J;Zmc^}3!Cg)W1| zu+`$(Pf7%I4ndw#?&P6eqp`*R0c}eG^<9PZNCT}?3}9rdMgChA2s~c^a-G`-Kc+&1 zSuucg)n|ujLHc`~6ZiWgd_o%=r6Tcd0sV(|aZ9~G0bP$erMdutLLcIVGWhr|=u<&J zp7vvzf*GicAtM1n4XA&_1tRhLQ4hdBdu?0f*^wCV?EW^aAe=jHUdMByk@T&i6hE7XCA=?8sk46+RNN?!G#3y8wmko5=pHhVU)Rnt-)}a5ybQa2+aik zdLnLEfdC3E*Z^wjhW(a87+C>UsMG-00Cu9uN=W2P*FM`l$y(H5p^l36N?-%LIBTw zOZa=-&@*73D{6Hp_lU*J=5tp!URQ z%ty}^IWVW^l;=wD@%GyamP;%&=H>#cnEALc%(JA<0Jg5!!(@z49y0BYuJ5A<&3L0M(9Acr_!G_*amIIJ*_J_sb^jj3RnFd`!?M&IpE5 z2g)6CrNM{!1O6vo$liH-^kM&)rg?=1^%{$uMu=l2HbF1O-^JSX&+rYwOS0S@(at2l zM1pX&Qe47SbBKwqgji&_Ynr@ly@r$X4R}5rJMp6z>q+pmH(Dv=$`{a7m>+2Vu4s8! z1z6^%5f}-7{3(5KLSroo6;FiJ z+|cThJ_);nk0$7)GnXb0y)QWswT_s1jt&j(?J=7W%H`?`tw#HN^RCi+u9vpCy?(6s zrPYRK*+W0OJ;(&GylnL3%SdLNhj8{|7M@|NQOLeuD};vgR255v$l2yZY4#(B!)GpR zh32dTXD(bP%Kp8V=vIWfZ>M#77)c0v!BFgn|rP&uX6#{tY^_K<$JlVGR_)G6*uyD(5&6TH!8lqq3Ywwar1-hlpQ|3F>HdMw%> zFmXYO@zBQj+yzSOO-b1tAM}2WF22R}!mDva-32_}iJ9ZH;4fNwLYW-+Keygbkja)r zzM#EC@G<7xL$zB|szKf;I#HMWyXgx zZR2^b`22KS_@GguQ&FQ{ZI~UOkEHSaS>B=tVg zJI5}EDrssLec8V@D#LC_)ssd9#$-aMrOY^wMxe4yhr0qP$;o%RT5U>Q!ZQT_zhS4n z4l#}3so89rULXV_Xxn~I7!<4Z=2R+TC7%1ov+Iywt+g-G3~nS_$FIQk;P^a+sNSDZ zgx5Zkahgk*71kB**ViL74jQmP7F5QG4F>mAUaRYB|H^f!t9p}7gjOU=2m|%EC+*5j z-e(rF&n^^%dv+%1fha>HEuAic^M_aARD0VFo*dyb=9qm}) zLB6GTEUkv;>NwLaC+i7LYw0##jkb25w?`~@)wr^=dDF_X^{WwD$)l}KBW;T`$AH-$ z7-Kzz`TLb#C>D@eBDu$l%wXzu*vTs56BlCi78^ zC~-jRJ8@?i5dd(#GGOW1dOSW4_o8>|sT1d*-6X$K>F9Ze6Mi0vUYqkE47=hGAg{6& zJ8{I%K^&j@b63QufhY1+b-YA`iFc~0eO|8A)b0wR zTyV7BHc-#bLfJ}cwfyUDT4a_QHxqC25&Zqt`nFY4#vjZJ-h*}@33kP_jTRa+7!J+& zC@NA@hQg3uGXMz_5~;P->z#=Y&CJvfpImFy)q6~h9k%P+ykc*NaWDv8&Ma+n)aMie| zhvRzI-Aj-*?IUq*j_-?T9WO5_QV7K^n+i92jPb_CSjJtMg#xg$)7c0y@ae43fpNQ zA&AD7wDB4WkjJ7TVWbxVki~0P;QnwdXn*93OcZe#8iY}U-#L@-D0ENR_~cvZ_`J$|G6${viKgBqfo8eQLMAhWxv=DQK3$+(HiMxWn^+N zB#y3bi%WWy^Yk7@z{hXbg6m=aH^S*iu973zi3**j6ky=VOkiFPo)MU~#laeD(1{75 zF1h|p)yUGTXrhN!%cF&gjlD|{OZ-zfM#jb;U>E!<^|DmK3IY|hZQa0c?05WG%RYd} z#j;-YR(VtfeqT&I?@-YL!vg8jjpm}{LRwGzhytne{A;ARFOQbj6~m``!RRhE%^>PF z%h7a%Y-7Dp5n%>x1gf&|vey@*yB~z6)ke`4@o35GA!5!M>&Fq`%Gg7*bex24zk-M) z2<)F?{L%)XJ47t5!qT7^|KF`R!R++}H`);DEAJG^;V{Yf8(?V?Uc>Q-@1ZqIhzOx0 zyk`^z5R%P>als`t-b1)^O=_JUfoG#X;<6+y!k~!E2eH&i9=p7*h}NhZfmD>u`Ch1M zuHoafX|oydY%MM$zB%|}^H&j^W0nl~+H%fW)c!_|h(+w}+o0eH9cPdkOlnPc=W?T` z+%q#bwWPj5vCY{vv<1@~hANLmN(3Q~Ae6Jsb&QBoGtJ#?|N0gbb){qBiiZs9?>wP+ z)YGS(-*szSq69k7nK-eWH?X3Yq}rR7UfiqhqX!C>9Y6aJK+=W4f_G-z@nD;7k@Jux z<0b3T20`yc%XcXrvC?l zB%lS}z&GmF0qa^Z?w_u!1MPH`=v|Z5ix~;X84HrIJDBLnN?(xdyVS-7189ea_jPZF zvAPk?<;BqD#uKos>`i`i%iYpz{#`CjKkS#T+VuN7vld9hD5;C1>g36S@9S@6?rqXN zZ0WY>A>FH4O(TX~ zHf zXcD4cRO~`~s!@k)420b3vfz8Lmuk0w+uLn+mCY~z=*{LReb$Z8ZMDq^*Z{~9spq8+ zTZmMmUbM!fMa~b-x<{wQ!Tr_zN9^6xif;F#<#>{Y_}31WH^=s0Q`=3!1INAnp#B?A zsIR{d2=5pV2-k_pOHgUk*W$c7g6>sVRg04?8Kku5!661l1aPp6H*Bg@zXe$32ITtx zJT-8i`<%P2<~G4CC^}o-BE~1);u$D*YIzv!4M8Q_K4*JIqq%k6=EToD_X(#&5tQ>i zjGF1Q@xgc(c$E(e+8NCd=yXLxP8A*X`Iay0)6l zFtg9HN%xr9RbbBJxr|vx6Ck4mM z?*$*>z4^MPf2pP2HJzQ8eH(dWcTk#~PhRGkfz)M`Ao9uu+-2kIK7T&`-tfs~=gniM z0}>cWQ9_jtH?d7>J~>QSXyh#?6^tuUz#0GhKtQYL;ZWdH;z+<0sY2Ljsm=LMJ1y6y zdp~Y4snC9pMW=J?4GKs2*)T-$d`BPuV818&3Ol4ZXB0k+mF&2SdABMKl?#}%WHVbP ztZX?t7*u>0a|!N(Nv*l+`G&niLslg3lSB)y>MLnCoe-sYqqE4cZZI!nHr8@8~^ zgr3F)Y%ZevA^}N+%JG#-+(c4|f3|e&cGV~qvHO8&uTpdtH0xVbe9N5E+d45da2*fxDbei9{(ZlUBRSOTVeCWj$(st!f&KB6MsxdXcx#4tN^AU901TEy zP%K;Ricm{iO4~k3JT?HoeCvJZqymVfi&(cA^oM)0AqsT>WdIc~!ts~LFXLQH^)%{@ z1N+fxXmw*T4Ker7pTbk&BjFR_lrfq=)UfnUN|UL7qKj5XmqoVWqF&;i;_c(b>c1H^-i}_=mm`OFSfFq$V4s0dpkD_OV~r7VQ;W&XhNQ z&aXXoRd>O!?=MLKsuOI*=*&>9Lo+P7S!Eu_`Pi^Ah>z~JkI4J31nD0-alBrw!2@Q~ z_>enHcaF(EV^8)Mynh)KDA@Jtjn7_{0 z?BVpSyA1vt8Gf1K=5pq6xj!yLz-ty41ra2({>z&s(*mIP#WgTjds#F^WLkZMUOOn( z>dXPZl^6%sYCKRHp0hAgmPP18Y2QQ@3(I8T8h{cH(mrPF->j|SPj7my#B79J`BdN7 z>}({-gFcN&Z)Ef9cr(#MJq>HMz8+DyB7*U{PeS;vDnL`Yh!Sxx(Js*;pUWE>#)0&p zG!AQ7Y2ai3I%;&K#h%doP!ht5gj!9gXAK3xnq#iBzHfwXe!Dj1eZ7P(!<3UN z#@0ESccm-K4w`9KTZad~{j5zbc3m!JZyXyy>G+68Wj^lK&yeg)iwQ|Us2^hIqqvc7 zejpZ3qkKdwN)>y=8_AS@5DS-})%`qT-Rv>>>~Tp+#J?R~vZdmL+U34bJt#!2bjkVT z1#imHG0=kX9K)LW`2~=8_yNmn7uQRX$*4D9^-G1UZxZ@+wJx_lnhg#U*+I3r1fic! z!spn0^70skFYdKg+?pWd4m?}%NoCXr*yD2ouT6Fn(RW8EB z47r>Om#2y<*fGnoo!eaW&EukCZQDRJz_gk(tPSX;@)x!oWu*<(<2y5hb-lStv!hB) zvHIv7lh51fFpA!J+vn;H2)x?v=*Q`hg|bps8C z^>my!61sF9(_d-4^N8+g2|PGMVl=FqGcIGTOl24haX!US)_fg-@4?`e%il&LxtwZl z02a>|6u`&Gommy_hdJ=O4!UD*U7U1ZY7Om@v8}4@COUJ;tw9h z-#Lv|L@z{dGoINL272J*qGg^gndXYhx$^U;sYxVsd+89UriwO~e$NZkELHMj5jlAhS#Fep7F4cqJQlhEl0+K~DRe=y8~;|J zhJ?yxYjW0p>5LW@0;}rpd@XFhbUWrx+YAj2^IyB*>6e=WqWb0=qq1!q&yV=COA6ug zta>&ZeVjP_V4j&K7%xm((~6cE-F6OGlQ~tIW%gLjk}?K_G_#KbqKGvth6d~j=2;b0 z=8F(7M2s><>?)z7MQq0UwVFXRzFJjjhO@6G!H|M04)tBqVCQ-oWOEhECl%+l8C3b> z{ESpT%ULWWwjN%O00*#7QG(t zS6R<;N=E0rO8nkPRab}E6jKh^_&-O^dZ#wcSs(p=vDUJ%VlVc{8}X4NdB^s&4o5y>TAnNifUs&84jbM!hqhhrC0>r~sE zU#`a3SA^tpe4aKNjT-v5mbWfwGd ziP8noyMq;BcVxb^4NPaAExghqda%0dV~#sR3QWxtdwSh)JA?Vsg0a~mkMwa59CG&I zG_>6wN^BK2H&gCeY^)MTAK85Ea@W`<2>RzjkhWVBpU2U7Dp`~-Ud&4n7oC);VT+~N z1-JT~LjxD+jeDD8EmJb~SSnNqvGygR)>Xi=);dP``I0*gns9xq9M~yT1gi;aAEXF} zJ#m(z1kHHgo{)1c-og(rxcBv>J=TA{%@JmJi7vBUZ8HlulEo~cr_aUybE#bX#QV42 z!h+S%yw&5?azzo!)m)aZRLZx2ga{*=#WyvhysUg$c4aX$E-|F2!(Eo$-Q^cZ+qcD+ zr|ZrwUWto?jMku`RjF^SVcn<%%ORI6!pn&ClM$K!7g=8s5QZqG&DM>^fNNEOb*#-4l5>iRte$@}0;G)gxa>n1LWX=9LEb82Fu zJYdsm=s-HZhsC`(bv4+`6n1qMHCoy2)&qH+N7xPR#fR)2`XL5>6JcK6{K*{4w&15# zOW;n4^{ex>Mwx5*`KD?YSN6hwwbc1rS;uWlBM0NPwcxefX?0Fn)y30BBl}h#!A_Ts zi_z3?+8Ub!(`EGwijgjv9OE8YfIesHv>0)@+ux1Ws~?6(z&E*sOK$zJT1gI?YYE{V zkkpK+_v2;n`?uruE^?gHfKT~LiMq4CfZDT?w~xOYJho^pDf4q7&LU75)?qg0INh`b zII|jhORi$s4v2F>VY<}kyYmv*xm55!bk`Kkf8a|G-D0hQs`$f667eghk(gSQUk@bBD+~US;<>&u5E4ca=UK10=6Tr zc=9$JvZrlFh`CQI(qf|v{18ESn0DZ*SqzIjtC^JM5)$rpO!n!y)IC1Pj;NBOg4{HZ zTb|Wkcjfu-seV$L-R$T>K3Exe+Aj=l`@L{Lc5HxR^g=)cuyD;;BG#W5|F$vk8T> ziI~l=17|6)x!W*N`o!T9eieQ5*b1W(iSOlv-R45B3NGZP^;-i-QrvEQAX1O#S{pxH zKK^`qdOivra72#pgu$i%Dkc-=7D^?JSvV@-$(oiWdwDlDtmn{Ft{k70XGu;g&bM$! zXAhg$Kl1lJihMFZv^2FeJ2^Zxm7afaf=}5%F$}Tb)rwoTuNWo!MBX*LF`R^~qBhJn zj4H!c-K*MAyJh)myiR^jKD4+{8)zuTXgeCIc&8Ssx^$TryaaIe_mEHg;zABQ;Xhw6 zIx1bOnaX-Tm8rTNjp)(m1X-(#l_^%x7hPhNfy6t3ab%7swg=>wRU{Cv;fjFo=)PTtL>7Nk zVsiiG9<*5G#FrN-L|lWe_;&;A;9PZad}u7FMb6gFOpFjWvYpXv7iXMWM2xT{(UhEn z?&)D_V z&>C{}foncnB*)9hDF8pTnrtIbi3hn4fh;CL?t9$A&=-a7M5y%jWaz^fW zVo2!T?uJBSpPB!+V&E-yCKiMi%M#5R^DtifV+YhWQN^6rv7}F`he3DOP-MrapfoZ<|v2wYz>zfNk@?8rY8nWp?`+z%S0Dt`K<(U4l=5 z>Z0^S2ou8$O0f8qf70C&en8-TWb6j_@@neKUV_W&OAlX=9NmbYdo>WI;@gnjiS`#7 zz=Pqe$Uju;$qY_SeP%=F$oo}@Pw@x6QP`DwOxYIf@Oup>PMUYWa`R())mhNIr4t;U zXzV@@jN!D#k;@YujjFJz#r-nS4`MqT@!o02tOUV;!o&mw{Tz->OEJ2dFY}zGSHmP0 zf^)teLYLqzEO;zzb1`lRKiaIU(yEcOLrVMmUw6HiZWBTbU6S+zG2SLkooJOK%XkaOtx-bJkDOQ{}XtA-b@{ zbG=GRUaLafZ;-z@IGldu`>F%pqWjO>=RgkZ#?0RL9^4Y~fSTp!C`c&H%-Wd8w|3*P z3-HK-L-tjFkqn!G5yh((D5~~O^qz^X*XaB8$I-t_Ds?iYel+SiYdoENpm_SEBp2Pc zCmVMsgVR`h6fJpI4*k#()}O)EUb*BoQf4_%vpK5XNNau$Q#-QRVr~ImtjUh4PH&+c z+hL2oK7sd9%$vYSiUzC!_;G3$69JOAbH!Nte=y8D$IC;C)^#jkJ!yA1=RNV($PFSd zNJBnOsVHavxP_^NRKllY{4x#u%P)q-0QtxMHb_M#*TWa_?|T@p*D*{o#&BxxABnxq zqs5+x9kGU&xuJro;m!Yq=pTzB% zZ$G8-Z2BS}9XDrjbljfE4d5!PIHMouj^44B{OB*lA^UuwRD`lhsXNz4uY_A7#%Cx> zz2=(7Dz+bUm67xX$i2V%K=p6X{(ncxmm6#g`~~>RMb}Z`tQ;)g)N^Fb_jrGcdIrrn zEW7jGc=n0BhFN-T-J$OO_dgi7ekIUwih+c)UvvbKohPgav|__M?a`tekk#DR*i%Ea zyWPZP#_TUSxxECt^v#{Yqy3o$Fx6ZCNE~uCN*L>?k*nW!xFqPSq`8$lzSCx&7<861 z=||z!o18_rwf%ZN%lk}LET1<@S&OmtnSCu!ZQ6&O2k94TM(ip$$KT^$(*E>v&G&@K zhMx8LIodqwc8|Ng&}|=dc&A?7{@;`8uUwt0mF|MnP1CEh4)5i!J}Xci>Mwbn#sBuY zPlB#wfB08FzlRzk%~G(8VEAxh)5d_$?Oz_;7y#BdB+DQ#_7DDF1VfTJY+|0?WAled zaemj0FmnSEO_RbMVI{PLV5t`tQ4;^qJ=e61cU`^KJb#GAm)JD&t=VRk*a)>eX6;yH zCUhS)MF4jT+6A+^DPBWDbOs1M=OZv z-GwD7bfv)fE0i6KvU>_%1v4Wo=94cEK?Ox2{6YaGE9!8~0z?!3ayKU}ByKfMUS(0| z>=+N@A{deho8~#D{0LO-V5!Q0pq345A{$8ibu4=`62SPi7Wt9SiZv7?G@mCbj%ELNE__S6ipwn=(NHOlkZFVr(q11T&_HR{a|QJG9n^-AEsfy zI|S_j&E$nsd2he*--bJ9TdBlUQPUy9%(CushujVTDdx<4tas=xN_cZXFtZ7Wqr9(?eRf* zLBf7B+Eo?GF>f)^ioe+z#^oKDdLPv}e09~Z?KTeZ6@ouoUJbhSk4OP-h3gj3e&@`lev)}#X6yNK{Gv4fW0l}&Y6O5MVYI3`b)Y7Ims{;NoyusT%uS<1ZRd>=H zs^&z`1dcw^B&pE&Rzr94YBZFIi|~cq46-dRUwMTN<198!0CJl4MnkEC5#Ej^fP7P$Gi`IREUxa~E33tJvnVr0Kbdz?fJaq+fakptQs|eA{kUKtpNb{J8T$bBcB0Bvc0UYP+R0BWhk_1vlyF7f)b*o> z*ml(%v%e+46O7y42O#&pt}zPK%vmm|mh;r>br`?3P(qoTt1qFVElDo*q&L}AI_*SZ z4Hq2{K46Jj&Yu6JNZ6k|TG0y0LD?KtaU!Khl+oWPzMy-F%W?D7C7j(GRJu`^t~BJ5 z5$nMZUOZhNiO<}|JWO;)!Bc-i+tjg2lEb-e;2N)CojUbZ+AK=c^KN7E+Vg(X;V6bLx^U@6eyJjuL<4B9*C0 z%Ie*39hdoKl*16>m0b<=2T{r){0Hx4NKJ5Y#NGezCtPe8FEg-9I@p|f&oTYV41N7R zy!1U?Cus%mg+EDi*TP~BCp8fNB1T*D)h)^a>(@xT7M|M9|?xMe`7CY}L`A z4fVQd-x~y306 zBsx*9L3)@+K3*ve2H*Ir%#D&^zl;1!!g<%cB98`9;v76qo-Sm6b#$Uy23nbtX41x= z7-OoY)9wp(ir(;NiyYJGi)Q1mqetoE&gp?T6c8Cj&SVsuD_T;AW~(G@Rg3{MX?>h* zA@_t}J~DwHma_6HzB0y*54TrZ*~$yvs#IZ(|Lg?B$WqQ6G}s0Ul|eD?Rb_cjxD!|Q zCBi9!^@k1ma5-$!0vlwy4E&ll`<0mvrmPKv=!<}Kkk>C@Y!UM-!O34A6JQwIYd|95 zJN{ybruB!{`N-O3+7qJd&&$;+isjtnIb@_TvW(XJhXg+}y#@DTL7g$v*Q_YYo(*oN zmT}z7hM81Pa~(MSHD)x0uB&g0?qABj8M);h6l`W}m#$g|b{uPl-tNPWRrQ+(XCn6- zCpmvi7c+-%oY|o1>_?@fGzc|jlhely>~FQt)aDz2NoudW?M{f%?~bz8p2muj(O+wQ z4R!Z4tKKNr-;RG!yjz%SjP7)`kHRljUzt!QcU$Ik-#znjoP~|qEz7cv33ivkcQ&rm zIa`1}nJF)oru51a9Dg4GuX8-dG9FAPc=PcUrZ!48iSNzM3mfjAX3B&i$09PBri?pN zdCfgsWFwyempDuQpYz9${0$%a+(=1U+0dZRx?AGol0%^z|0iCF7tuqZDwgMUOwN=~ zi`?rmIR$&SKGOH4$&@{!Y=C69b1~+jloG)EsmHyVyeX>ov&)&{*#AyPvi-O%>5YhA zJ<0E&^PhloHTnD4q0~*!zK-Neq;qj@RJVX{d(p+zg3ONpxr=1`wu|HkL(Wf0fqvaK zNa)WN`E>FUk__&$K-CclX@( zpPz3+-U^a%>aK4Xo(#1+dGjpPj*Fby9IU=rPojP~Itpscs&YTaaxb1ye!C+)h_qXt zARTM{B$J$|h#zGbOH`fD@QbB#eOn=w?D?Sbzd-O`~jct%n z67yS`?{oSZKlV9!D*0*hSF+X7m10Z8L#zhUVME~qGJg|MsV9$c4L0n}zSTQnD=_DS zbE^HI8%cBqvxMM%JiZ4dLH z*GoxH@}jZqc@TpO13fpWj}%#nhnozKIG&p3&u_4^_!!gQ$@^)kDSNVG6XtV!7~qDX zXeFw9%J@ZU5Ich7ZK88G)RZi+0Lm1Zif<+*m|mC#faJ2u5X;gO5q~MUEMB%;C3+@E znq2fHqgc*%GhxYeog5^mfX&c=^6)pj)RK|Ejm60`7)hht&C@ZGc^U$^f`bZ-Rh8J7 zbXi~$?VnvFLFO~la*D-Wk((z5T?HYK$fEBl1xXU>AX!vbe|5{dIRWH@7j%p$xG z%p^o~k(1mACW4;KT}8Dpb#Lhy224(JmNdWBCqoK+UWAJY%t{c-G^v43w0n|V`;5kZ zj({A6D(l4yUQaE@9O!K`6^gJdsqHDrrp}@WEjBbf45c`oUtQ44Q7y)nau>;2k%HLE zhK`ZHRPXhb%hjX<`@zw_h;r0Q5XyV=hR2#W5r$=&g+LbNy+#x#v4dJg%Xj6Mu|yEr zKYnI8!t7KwhvgAE94`~fQuEl9@ZwR>vGM+2%j5Umy<{ewbQ&@-iGJq&#(W%+S*3rP zT3$UQWUr;gNC~fft#^i+HwlgTmYR%~i;V2^EUH?gKe$(>lgIraw%Ob4R}&g5>l~G= zwA8rAVMa_xK~7>$W@=hJi6TiX`!R{>IQ?Qpz4-5jt9Ole^D-I~rWUZsU}*PvsZ8uF zw!OWToY={bNV`Rq<9i=8B0v`CVG!dABQcke)11r&UuyGdlul|BkKcg}vth+!zRCf~d)*>RD(eBsej-V8VMCyIU^pdG-kt|m z{)UQE#tO*{7=+TSGBYrI07S;D^in(nb;U%_Qml`83eyQK2XA=vW#-@~HGPus`1al@ zJs?>KT~bRC5Mc7O6mK{*A`k6!%&XbY+>oJwlg&;_AtyH}gC|6hl)(i*U3`O|I?8H5 zeg%YO*a&7wLGgVsJ`d1yX$(^r4_5cn~+8z^$e*vvBk@2Awnyx2i6Utp7RT z+5Np(F858vLy!r(`Xz>qgf7su?oXjt%Rzu_zhl;kSNiL>^Dv2`_RY@Y=YL>hWdIr) zpwu6hbVZFxIs)N;^QdeGpnWdUTI`qtwE?fdnsJ&S!+lwcW@c&-9=!Aztl>yZeIKj? zrPCy3@Dx@BHLh>@tgg5IO;xaCzB!5#v z@^?<88_s7PcI7q>vvqPITRDxQtt0?w^Q&#y8c88nMf_~mkyJu>;MkX%z5ET3ocL;r z0L~O;s(M!dCyL`zCyM8^uU0Esr>yw7St^Yc1&nI`!yB}?8E$=uY)rx)QVQ$8;h$;`@r`TCOSMU{JsBG3qh7F0-Ysw;`}GSBv) zIC6V8^3T*W39EDhP!BCnl3m7^Pr*|#x;mcWs%? z^L86j(zmuXm;^;bSpKwi!x0Q~_B91d5S)i+%y{c$u;Ef&dRz0<9?t)R0Upf^U5-YE z`nx$BOSg-XWzewkM@G+n<^Dn2`aNu$9htx{;3aUR$#sth?OBaP9pfh6!fEi=M|Ye~ z9Oc$}E}c(dg7oRg(&@0Y0!Qfb01-$+~cUZrri6sfsfJo02ES!w?6G0NV28cbi= z>E77)n^0kX^$`&H4F}${4Z*VtNv;M+|1bfqCZUIV&-m!$8sF({`P=przLU+j=OcNN zK5^J*D(6FBRB;renWa0gxWUyTLa+}hmm$ZA+q|px4$(9^8Nzz1BmAMA1qof4_2!O^wc;dA3_Yatw0rw$LvRc|!g@KwJ$FWpSF;{z znVow(`Z1IY)O*jaCN07_$|FBRc`FjKJ@;_XR<2zdYsunyyEbTf31pkKLCOJl%1a<1 zY?-1&lW?;pNG+82x>eGHG?LejG=4vk_DHEI&PH+MKpdgZER@%ZIFk?tlYKu?oIN8{ zJP0JlvO=5~j#2NIt*B9x;3`&+300*lzF5f)W4)TVKzTFmT5q)v=%Wp-)M3)oc!t&F zir2q3Xsk8j)!g18ilIcJHhHf1Fl-Y}9R$7xj=usyvlL#2WwCTKKq(J}ba70GQcQ86 zD5g`cFy_zNA8;7;y}}0olN{j#+JXI#{w%IOB6MbR#5Ruj@yU9)hC&b@Y;7?#$q|Hq-qHR%84bpMC(Z?jnI1~{m#QejU^Y7-{Cn4@nAXr zL*^)1B=TyJFG6TZWR0;Z4%7fxVxiGDN>eh;6xlZFS)7U&+>=X@$c6rTN&Jz=(kHdY zyVEMy7Z`HIi)u!m-}_e~G(?BW_9e-n6-G@$fe_}q93(CIHz>&OXdS{fu-EDl7~C2d zMqpa>zLB1Y*JEUqq1XsuL5d_$N47tRy+S3-3X~%-+buB)>QI>Cudb4y5o*yqq|p2R z@+~DAun58aZQ`__foSdj3=R4yt%H00{6?HN%>Um3ow)voyftk2=Ogs}*?qY>4X z`$tLl(?cuHzUi%%`0*EU&1MOItuGE!pgH_#$7lt8xU1^b+ge&XN4-P)dYOH2=wZjH zPOtj3yvHeX$@SWg?%Z>!hhySgTh-t0n(K?W_JL2&A+UIRSy=qu<;!K)?8*`K{&86D z@BaGHBHDVoGvHlBy-;L34XtDT06>+QC(Qiv|@VEPFIJ z@Sah;A~}5<|EeW8iU8{f7g0aWj{5cxfbO$_2guG$9!1@1|{+UXNYPJ()gPzA?YC`r-KD_P`6o>WAh2=dy9Kp-CPu_C6r@=Q{wOI+#)c z5rOAW!9hPHhgnm~0z7mvdc9Pd#yNcq*tt{w0M8}2UBEEVM zat!UxIIAc8_HWZYy*27($bZFWMp3EGMYd#-A&nj1w&#zwE5(PORp3W{f>4h6@U`{*O6Cjki*zK^w+wr`J~ zwxf1HE1euWicl`uRPM&6NKoXp;CO=Xny*#U{71d)bisNd_JsZf@m}v9?B4L+^4|0w zZy1SSiJP6v&cViU{i1d4nq}BGjxR2kC&z_%-(zs)Jarw@J&0%J+||5exP!qxeBF9o zy{*RibM_rizSI1-c}eN$+{JU`q1+T-PB?G8N6y{b>aF(_Am@>fAV;28-?RIg(raT= z_)_MQ_Y(Yae{*oNX>+(UyvyEW>)!8x?|6Z&0&{w{iD{Md9JNX!GoOA&&62~~-7?-< z{)IeuC;Fb^73yW3zsJ6oVI%E$>_PUC{(AVy`2G6hSI+^_LGHhr+pVL|#83Xu%eR`R z&y!EOJSbEM2=MOS1VJ-m1_wz4sXS^%76nvlBsy3+gb;Aoz>?mSz@|WJa1JOtxHF_q zcFu$xpr7%J?(t)h0BUk@ssSS>87GSdZWdlA=`wC7+mpi;$KS)C9ymYrH|rrP9P_XP zc-=%CAQxi=cLhxa{po0>>k7@v=2_#}8K0zgr+1LI-KO(cj&G@-w-|TB_PDM21^?}I zdQZ;rg7%$VS4&H2i~2utvW_D_BQU2(f6mUbmQ|BBRjNp2_Od9g_%oRwm6{j@z3~US z4jlUWxZlvn2!9%2zqV6$ll~IuPa#0|splu@C+kQHk%BS`UM##)Szca@1j)}vM6Cgf z-dLetvGivtH#}&H052dY796sL+i#~;JkKC^m*fkT1~(}H4r!K03_5o+Zwv2Wa8z$! z`wn~IB5+KSgFfXT-&VSK3z7HFZYnqjB8Q4s-qY{!IrvaMC!UwcTh62V0o6WA{Z{TV z^_N2JK*#25$fNt_=Uw;Zd&eVs-#85Vo`rGKwN}2M5OA@;cFxTb8WPsXC$byNml~T^ zLE&Hx3cWQT$i#nIU{wEK&7=MO*1;#e!j*Ym!1wXMWI-!p**pW8JYq(AMhI$ZgmE|> zls~ZZP-@>Z0)y@YWd;9c2dYCu&n}kgCwJ5Ps=ovhBa{nyf2IA6fIg0-_6?FJDE=E{ z4uQgztFoL@(+uX)muY+Aq#8 zEHCzVrT3s0@-9K5iuhUaqyBptzCW+`Dtw~yvuJl78|7nsTX#MSx1Lk_UYjSLcPa;E z`K4i5usKpZ`+WOc`}`?hR?}r*rAl%Xx%Ih?ew98l?Q*WZ3-_K&b=8Hs{>NT5r^VG+ z9XZD@o=e@;`?_qp3%cqq^j}JoY&8{~r?b_^y2|a8uCb@>e*hhT3Tum{gzCGWRbz80 zt8BG^Vn7?93{VHi2UL$O?4LQg47#keSL&wzY?D6W-FTKgT*~&pd@HyOo>b@2zwjx$ z4V~O_)!TRuo?M(PnH-vYnQWh2oixo}$WF-?%|_!f<{`*t%6^d3E}flOo7uRQ9WILl z64<+TE?)Z#&k?-x5S~1F4nq(;y05P}nhz5Y@NhS~uPIO z>U>EJ55L4U$2o{=LUVat&UD0GaC)uH#ELU;b01i2&M1nD6Hf4DIJ3T5EGiBwt|^`> z-YVWRV8=wEx#6~Fm<>+SD#8L#^J=cWE6 zy4+S(yJ8g;ljpt8exMuu)#nUYcWQKc&i7#VYRSV!I}|$gc}UdpB*i92jnM>F5i;^( zgjLNK-;8<=$Sce{70S+=5)yzktP+tAdsK)#6V-WkC|6d@DqMzAMr7gg|=YTQBTN&R51B+fUa%A9qp>M5J%o8 z?rJ9dSC=D^(l(zx>0{K!~^#(vb8}y5Oz?n9@GZcS*B(C0?v&avIUt{tksvo^V@kShz zpc)DND7AnNT0im+bNg=b^QAVs{@Zu1eJ~3{JjmwcKonD!AMImvKNM9HVBSd^F_9N@ zZr~f;f=!+@WLYJ0LfsOCZfqL$zT*0jdh?D)`eE!utkAwsTDleqonZ8g`HO|0B8~J= zjyz@Oc@@(=ko0eb)#9IOjY|RlsGi*#dqYO|!?PE(;ddTX-Lc8HLLa{JTGsLHCHACR z4M#}26nkwHV%`z)T_MK#P}^3%$Lm)|961Syxe2zYVUyU5$GqhU5W?xi=fEZx2y%-S zx+%0EY1wt_AO1?!o5lQg+KZ3)PjYBI?iD@>g({8@3UpNlvFt$nVKPt!@#Nq@_l8N& zxa09f!WD$m;S4Z+mOZl$kfbtF{a_)n0}(@ZyBODOhcRGmQeqeN)?APyUsWtmgEQDY z$wdl9cSZ25^(K3BTJXMHTQGd_(D)g5p(Zy-?w*_2r*ZG7^1DWfO<`A0;gu4|)~{mm zya38G!bp$y)8bhk$YX?8YPCzZMFC;?NV(pOHP9N$1_JV2Ka4S!=ak{V`O%k$gYmZm zWigH}+UAJ&FD(p>k;lYwL$tE3Yj)l+SyNeYzqR_vDW8xH6bJ7UEZ^gS#VE_v{uXa=X9c3LwIO?W-IZ zGlp`z6&7c(sfrUDn=-`1c(|8?zFrQ}?TR3}c*S%91Q zBneS2_m*Ot2%o(KSnAWgL>Lmi{ew7=?JY*~oDSEq6o!9Yhe}88=kF0J!J9#39|`gL z_n#YCen5aUEkJtqS%FF?F0^kKqTD0X$B8(F)PHiALgJF?EfH8GW;wVeGk*>AXK~n( z{}Q@fKMs%a!`_7FdJ{t4>5FN->yPtW2JxcOpVams`ewZ*ioYi;=>)q0emH037k;uM zJ4NGgZN{%}LnDs6=lKp2v)G)7cV87AWF%6(=i-=%jrUHFf9uRlJOv*i0Q-q|M))lZ zqBa}HlP2I^?AuMoU!DXgaLn;Pih(l0|2gsokB`WMJ@J3r%Fq{wdxjW6J8+*ac=#TG zz;cEC?I(ByB+Cw+f>8p_X^J4tkG5X;btq(_K+z-j{4VeiEhDjWIAZzl32EaaTVVgl zde4J-kp<6TzG<5cn16Fb?Zxf&fjZX;rqfXQ9+3IXDn?WcPUgFTJlxVNdoW$Z<2NUn zZx6M-X<(I*;L}s!@6gcj^#QZJR0P)sV0N<%;EwzwipRZz+-J6-v9p2OE_>8~v!uAr zGzQfrzFA3~ai34ubFs+0Z zx$hEt>XqT^NukZX5$Z{?%*j&g0~G;#qm%3-QTQ#;{F}j9wu5?JNQ=zgjGG67(bwJv z&Uib{NIT|8MjKG;mw`l&#ND>oE#|pG><0hl?Gfs?+gPs1{9!C02q$}~%>SX`9qE^m!zqq*GJJQyNiX z8d0#H@fbw}51>*xCUT<6qe_rUl7h%X;fWzY#7R=?)VF%cC`8v}W$=`iIVTm?LB4I!j^0XN8LJ2|~*`b8!5e?=z9ho5@%pNLp;vNv3IZ}pFS%M^8swDBa zC_+cdEWwBwI8NFi3DXvnoW!gocaYF#LYOX(nGm|?R3AY{sh&V#LKBz`x;G(Pk+CBe zpyU~myJ4x%T$jF}dQ9Zo1G{mqkLQ&jBt-(g?|t(Oc?;r|BP5eg2;NKEqj8FO%iy)e zmFX+W2$9I9$*v@MOrWd>cQs$9Oo0IkL`TEABJ zz|Lsa@nGQ8c5)DR+yRAICOj`|raW$9Dbq)K53n6#>(4r?v|XgO6sUrYc!x{9eLX=N z?!DE}Ba#_m6;pGp+(BnWT>oCU#15ug{-nfFq?}$-4zYE>m(B;=%} zMM0dZItgo$>r!k8#f?ZG^WWrW1Uy%hxF?8NV2JA(8t)n1uzBhZ2qNR28GDXm?gp%z z-GtqwvIgo57Kfl~ub}5)n&x(ztyXbc#93xLfy)=(7;Mi!O9RVC;D<(Lv!II_3r--O z(i-2Kc?A@wBaK)Fl6{^XBpPb6NF7*1 z=g?%KZfmu=z*eVBh^OUlGgo8Dp*%hnK_|1m0kpgXE6Nw_95#`$-Gf>`5VR&zBqi$bU%~(SXPguQG?lkq3pLM#m(p@+jgf<9X+d)RR}!auv^`*B@S-{b2|Wr0sAM=IVTd4XW;*H7Ng3DDgE;&Y4zC#Q9nuyaw)28< z5=%P@K`|2zC5UUX3~;(`nrfypn&!YzmN585<_75R>snS>6q!2c9099MF|B_OY^>CW zxwm+HI$y80)3T{_jECDD2J$0!KgANVj7#ORxVPUnVAQ`J_VPAkB=QpOE9GXoo0Rc8Az6muZ`0ksk%8|E$!CR#QPE`1;_=(i73tT# zo$+@)wrx!APr3T^?Uqs>Y>*ViXeHBik&^XqpyYy$m)my#*}dE_Wd^wv75DYK^+Xll z_f)i7b6pO<$B&``J!a1Hf|=l&>Y1jy(Yl;E2(itZ$#vz~j&xg5^^3?l<%8PjCEnA! zo`#Ov*9ejvpYT;FEqo@*K4en0@Y{+0wqe8XPo=L<4&_pjFpi)=7aDt&b!@k#rqd;T z@3RY@i}kW0-;zWByFmW!+n+CI`T`|WM7ut>y)CSxO9Rgteq;y=sf&t9_lKmUr~;V6o%7S}m(Is79{J3w+A zT8^-3`E4S}&N67)q9iVowPI(AxiCl#l&G|682IBj$uUec>5G&MUqf*lDSrb9e}>FL zDrK5aK7ega{>R8I<{0Zz4BCG|yPGw7uYLX{(MOwIWZZzc1 zF~H?)TR?S2>>bV*LYM08g-)spVv2dkC#X@Ho?~SurEOn83fwF$cNR1#y0nfpq^_9_|^|2^IIaeAb@Z^S1lwmK5*;u!UVZGRBbuUc*sn_27^n()|Odq~`P zil@qUUc6M*#$d-E@4gtx^VkOe4x>SUx{s7GLr#vn5k$)U#f2}c`N{ezZ22iwT&p1Fy)^hK@esq>H{BwAMqUkT{Cos(ia)MQ9C0Gq0Eqm39_SAg&6Mq#%*x!KuV#M@is znPn`stM-VK?Q*%^y5@u9ov327s(WY3dU4$a=z^7UIPp`P*X2i(w{H!<-v$JPvVYHO zw()VWrOsr;5Fld-mdGX)QF&XN11I+qD0>lTEmIB1K8LY3Dq<_*rnI?XoF6brV+-RX zQxiDjMM2Sts3HoR*lp`*>m+9i%p#*%+S5#3arSVqe1K*Z@|RvpBdn(J_W024xNP%# z*hr(XKi+tuio<^2jpv^cm}9k?7-_ZBJ;m(C3-oy-SjQeIP3n9Dlgb(($E0%h5#%mm zq@BYy;viAP$HXB!+`EqW6xeUCl)qV7pxoU@`9p`lSnU%Vw!9A4s!Y8~_ zD@6^hK^V<6$-vDGE|Ro3p%i-BXJuG3`&mKiNyY07c)PENvBxP|Lb+#l#pp=rdoysT z_%p%aFH~K2>3#9=eU$wXF$vrQ-VNBYuJF7-l0Oq?|49;QCc37mj}o9^htOCJt|7xQ zN$hu7s@_C7ebk_h(%?yd0epSQau$_2K4aPhr$Lv^_8Zk-`p*K$VXiypA)-@$KXx6I z3d+)F(V5sRTy^BX&zf~cR7_cjX7$z~cnxpUSE$)@`?Ok0iUwa4i+6K5pDyMKgiw2^KT18GDyKC!F{G88Jf_fT zsXTyca#x#qhFqU|b8}c8BzTZ(j1!%q63*TNA=r^CH8sYeybBvZLB(Ue9=X{b3-f_G z+}#c*V?XMBRIU|x1#c5hfPLnD=N z%&|3QZ}NK7`Z-tyZm!K$Jz~nRT}AO#ti#4QbRdAf7SN|!t?sc(%K;R9mBVXr3fxbAB`Vm@3n7Aj|1*yk+l9LYVYP= z(VLZ9{r6kZTnvL*?98kJE9*t0vPw-&lDhITU}Bw^6saYvq1In-(|Pm?{^Uj!>3Q}7 zuzZnoS`^7QC8ZvDcFX#QptFsC4)XHu?YlFuQIB##ZDGI&qb76+wdb$KUg-6(!f8w; z78kp*bwd=?z589&3(u%FUr*Xl&SbLN-8`TB;}DU9!Dt~ z?bKyUMh`=-oUHt_yJJIWw5w$uVl6{}=obmPhB2v~V>WS6s$WXqGy5Q+W=wd%))$C| zG$8Wpai)o#?_HV*p3>y-K5iK^O*kRCD7u+3tqDuT*J~%$aXW7xemK@Y_P2klb-RBB zoGvA!OKTtEuRhNXbFI9H14}3A+xF^F`l;TZKYDzAb40~aGgN6Ya_t@X?FndG1rv!5 zDil+;;f~d4h$%9~h$6e|quVQS{By`YgX$8Z&>g-VGpxYxX%!2CASNIL}S)DuI^P&V>|Gsfh;(D5~ytGmL zLUg~&ntz!c^~QbB035!(#Zax&pl6<1VXrJE&qTOlCo2Y+HNX@v$ekrG&s9bZjLT3g zus3X&4KmByP}))^-PR|z%Et-d-;1+ai?g&m5Bh<80zYSBVT+@0#l6$js}qG6tJEko zaSbHvO{ZO4(^ahX?8@rs8(L-n#b&0l?TL0tb_PphOMuLU*n?jtPHFbY1Y8lNPI%1i zjYj?IYhz_^c^}8!9rshoEtB}kerwKkyzefQ&Tjsb_X}V3GilMr#iDrHK`2HY2o2#Zl10c&M zJNvs55Jx&P@MuLZH8~*>k)tcdBmqu3;*BVf)+(S5abJ4o&9IQSzpKAVv0IHM3tMjW z7yFFtB0?lyl3U^z{?|0^{v+fS&7I;xYBi-3Kw-&*@guB|YMQlD>-mC_;8l2QWJH)2 zNt%vc((7cC=x5eSeGb&!4sKS z33K7KhWp1O1Iy)$U}*Q?qM2wch?3#E`1>>f;3Axnw?(9y)zh}9c|&3qXr(zG``~6? zWy(@xE9u58Hu2hGrTwds$!WFtl{Rr;v+y{XeqA~B6F6JtzFy}#Sp^(2Gnyf6no z+&Vku#6$K)V^wK2SvdYK@h$UjKB^luMbi5SUP$Ly;=N7l;q7wqrj!Lz+WNoMt-13) zw0;D9?3)KGS<1Mnog%GDo6XQkm8Ix4*1DXoxQfKp8oK#K^@nbdt6J-3p3|$*QY13* z675r9pQ7fZ$Kz4-n-WhaEMshBQh>N#UPBwCFi$S$0^DD3I^3o0A5A1)SFKwr&oxUa z@76n74-l4-l*hE^5e9^f6pB&L)o#`5WmB59GEL12C1L9_@YgMec%rj*eI|B%R4cb)>Wt}kT-^%&85YTUhJ~?0S0CDgI1;F61*>MwJ@QD-Uu+zUl}68_ z6Y0azj0QiZ=B)2piZzQ<{O6E*KEC?~)AKnUa4fOoKLhjCX*~3UIq!kmx4|@yb(lD> zZ9?TVGkO(u6T!_04jB;ub~e-XUYNQA$CHp{$LfGv*|UUEbNt#A{W#qu>9Ztcn3qWP zSdQr3VfP$df6}xb2lN^Y%MWbXMReH(nGDSa3$=+<;BR0uFbkN(LQKUroV^RWmSdG~ zlxdV}lx>uEk&Vg37GN?o+Q}Ov)u%$p3#8_MaTTMC%A76i&XBgZeBb< z-K#_Km}58$dV|>@Q+xhdN9E10iHNZ``Dg~qprI5u0#r10hlnTR6#mW8dr5GUDivera{#Rlgg<~O{!nva~YoY9wR zxK$WWKX7|uI_wDw8VzgiNNEUgO&^PU)Kd(~pCQ7jMPl&w@H4&lUmbPdaJ$nuns|xA zs?lin8A=^{*t~AxNPE-DnU)sYkL~(Ho<>n5PTL2$uAbvS zpWt#Te^q>Em@hn=&*hnQHWK@r=>0(l5YCM*B>$qFH@30s$&mEJVIujOJpG*~hv`7Gb4KV$? z#Md6SYQ-y>9G-8{D(9s;=K-E8f@8$i*DyWm~c+~uQUYwL6j6^>d#M3l1@Xhn0y~(So)-0;~7bH#W8;Q!yiZU z&9pjXJFKn8mdDoO!l4SMhvm~6e@SWn{D$_@98RI`iH^&v zXRBzdR8(1Jl+IzI%i%Q-&m6JB%YVg%e0IUllJ7I}cM$sB)oXItm-8 zQAM`|o_bt6qjp(kOAWoS-ZA0?q6)RO$oc5`Ch$3jqIY5Om+D_vh@MCe$JX{AlgUk^ zIQvE?{f5Pd@+Vc*D_$4gOYUytt~n7iOmAIos}6QkNm4WE88q7IHx?ZiwX91g?6s}d z?0Ia7O#LS}30Szzd}Ue+7agDa9JgB?bU1(FW5vM@hwlD=qMzd^c#l07BVQVAJMQe` zDM{HTNyv(kzp}`4KgFQEe zDEu6j&?j-Dd0A3YL1ow%eKFffIwAALbCeDzea9Y)2NwAE&k2cndOz#?rP6bHYmN84 zCR?aP-%%Kqm?rX*iCCI`W#VsNRFicJM>fV_&FqZxZ?71NkRb3_jv-d2P`R?gTAu9F&>i2IV&1N9T( z$*%9W)KyhKC&Cnm(I%|pl+?)HP|1VCM*S+Lt%Pb=Dv!^TL{+_RrpnUnIx3>-yt$<) zg6fLa-_aB;8lr{Dl&}nu?S=dko73d!o2a%bEmd35{(6N5H55ckR|G9(>-{erepP#2 zcR@86S{A@EJpnkziRQ$5wY3;iOH*NCv1rE5{%@3AM-QZbD{G~r`Ch|iVNS%EoA$6Z zXjonA&r-QaZIc1VaMjZ*auC)x;TnseW%n?mcqoUSP00ra36?h~(i!oFuHdq+Efm_)`1!gmg?@Or> z;iune;dRSNQ{!%IP?WOpurRPyDUfH;w+Z)q#qsL#Bw!;YSdq^z!%=&#@co;$8YhIS zX?c}VW~O=#FcdTyS{oyH6kq4_T9|`&Nqm`evID8`e)_{1{q5z-y{?pfN}=@OFiKS; zIn=Z)4~3!!w@ztYBfStid^%(?5j849m+~n&LUEyL{I0mMlsp{310*U~54-<}dpBYqDo$2KMdr&n5m{N8-;Z7R5vC&93zqTQfrlxn zj5wbP*>Mdz%8D2%9z{VT@4X5*Cws)C!!qQj^PDEEHxKG9_99An$g%UVxN-3rkF((S z32VNy*r0N0nAA1CLk(jqW!}{!ZaLa3&8m^-$mAMWR=@EPN^~u9mS>!SUf23ipnnO9#j2K%5dV-T5+rP$P z=BR`9Me&;8_n-EAe|*r6Ij-pzg@>NSa=(~Len>;Keu*8jzGoSyYwwedqYjhFh!>w? zkD6<*hCiGM{=&lDZ_Y2`wXDxXEZ7_;>OTmNo#5#^!OYnTE#8~&nD*0~h4&ANmlunr z$7hjDWS*gurF3cJ=jcxp+vq`&Mbt{imTMkbbYknEY#d_bt)PK2BQ)a1khh%E8$Xfb zo7gn1_%pUSb_o+WXwgsSPRw;5b%a`Uw0xnLuAZ)soesUb+=GE{t4VeVwRtVkM2kf+ zk_zF)mQ!p}T+e--7&(wR8p&YHLa~4I#5e&DD+J%DSQok0s#xLiDU!NXXN`+4N|eXx za%Ou`Gb}jfM<)2oiN*VR$TgN4Gs*kYI$3uDpxk0F#e$oA&!5EDC;2m6)8x)z41p3< z-CQ@|t^J?CWSQZOO1HWAZ4=+OmhrnIM>8lX`=6RVPV&)` zT7))(T+TU(M2hvC2kIwzbbYPNhBF}fjYL00d zoQS}4%XyiZWh_P?(}{CV8mE!e;m6D*I(4YsKo#lNkhQU$9NuTHGTUTN^`k^k-%=w5 zuE%|pfxZ;G6)$B`am}9gEMjJB1y>CLX@&`{Bh{l~M4w;H4_*|_!9eG9!^{^9$+%h|Lp zhdEtvjp*Wxs)RP~HR$3&4zDv-h8}r@_k3-+Q`3L^)CM~e-@W;?^DZ~k(kYDUe@9Ux zb<G6B9i9nEX8U)Rbs&7XNc zR~&X!+X^}ks~*&P=9WU^JEFT3znAQrHA-=;`;NO+!xaUv-_^)%_wCAdYRG7dY#i))|8i^VILk%+=A>|?fu-A$u~iUH)`J=I(^Z7vNl@nt3|X2 z$6u-rmyly|?B`#y)PzeSQXM~zDE$1_pZ8U8=5b*xlk=0KZL!wYd+sV&H>~*HZ#RFp z3bSxNcjYxByl=-Yz3KZUS7viKD8>ID0TE#X!U%ITx=oawC61-L)SVWnIB??xym5BKLgZgRb9~5R$oyA^3WHcTK$+Gk`jSVuOPQ2 zz3KsVLWw^+j%}^AZs6<}5=V+_1;e4%q81F6dCpC#roOh0g2J5jAc)?n(%jITAv3SG zqO`HP4q|wIL5zBLXE=~#u(2L>r;|-v%}?5d{$*P;bau<=aZUsgS84O&C)^avZS>DuY<5gMeuf=57gO+p( zuE55rxM;zE(ZNH(#-v06`RAXPUk`3)!CvE3%bG3r*HOW`ruwf^a(>tn zA%viwV@ysoe6N^Br9E6|x3X<@aTk?}vVV93eExKow*K7o>k5b3%F?1^o@$j1@V{B$ zg@|*j{5C^we0?;-(WUkVVp{SF1hy4AYa}CD?X_){jY)!isOmD1M6)kZm7L=Tb7mxU zTg)mWQvrw*rf^=2Y&Dk-q%y|3Z#W@-HyV0^K7>n3S=ESsiPtZ$Z_KT#a>e4f5IO!mz6g%AtwW6x)~w z?J?g1A3S5qt1ae!=zo1oiw-rqj9Z#P>ekipOUVA8C&y?#`FqcvfHp_6SerfW>oOQy zvKhj#)YIOQhzc~ci9AfB`uutZzmNA`ZgXlx>>VdQyB@xXwwW- zbhI=JW~o+UEHV+1Fpkw0%ZK}1$`NLA3*>uknvE09`vvJ@M+RE_+Pv6?swx`8=~J|{ z*}QnA;E53tk>eO9mdcUi=8iv@+9p97DoQ$r4Bpb#JQbRBWi958DkR>LzgJZSn#wXv zGJBK4l)&PN=Bc!lbQ$bjys(JL2R~Vco)%-VgPG7-6fOOpO_@$bIMG@h3U5Ra1Dmezmuc^uT zl3$awX=)m7ZYcb;1=E_&w~!@h)6}$dl+*L$(l8VlcL8$Eiihyv=yZ~ri_J4A5D0Qq#K-$qs)xpbXL?c# zd9&V|{G3)=Vm#BjZYHJY3>q~fX5J+ZnAZj)GJNZ6)Q8Q#5$J;@4tV&Muq(}k=@w|! zt=Kgk%t{dh@k*}U5Z);dM~I;rnZL`=)k4((=EgQ@D=b7(x*~GMt^5V;jQXk*triHj zb1BfrOTRmrI%zY%30l7{1kk3CD|gZIaO<6_&~FYJhC1fRb$FincAmfC*h;JI#B}BH z=Jzr#OV&%WB<0D2aFdo#T6n7)MM76=5$Y)v8ufF`6nOdNJok~jys^D#Q$LM(yjWBD zl%^1D%n@&5)$#TEo<)H*J3Y4JYIP^Dggi zpR@5#*wxS1d!MsQz3flg)GK+D=gW+e&B8`6gs1MD9h#f=4#73H=q=q|w=Y)pwYGpy z*3_-yMjnMm9{I-HQyU4~4!&qx1w(=5wOALo$Z5ctjrC}`jqZfqc0%4})*xW>Pz69* zmzi}rj2-J_OyAKr&2GM-?YIgHXirSbS)-zEaCU302IEu8nEcgMSo&GYN#AL_rkCr#| zEL2MB#Iji&1v6EF!Ab9Rk|?5VTe1POiOwayzf>l9Y%3zE|17(%W);!!;qaPDpflvCw z5C4IsO?;&}$dD5R{#2X!yoeAHpWr*4hv2lo*rY2L)Z3uT_G9-lD z!lHo%b!C>gGD1wTT9!}pfglkLRvAGlK|nC2g@t^Gax&0fyf5$%X1MV`!s>22*r^Zw zPoi(c6Sr@IY*nbt|7mEH?f-LVl%1LF|BQ?h{>O-FaFU$;KI1R3=U22Gv0}IQeNuo3 zO^`~Sq~wCaZ`CP!Iavx4$=@jGAMcLddCB4n>C!5=A0O8pD-AQmAXYbaWjXe#ipUjo(9qknZQe9NSYxX+HTf??5_SFvH`xhX4 znO&Z({}dTdEOwXpCl%jZa(+H*`}r$Sb#P8J!eI>oKRcb~x))??&29 zRyMF*z)Ne_vr_JJH4kuf+r*WI%`PoL;Pa_mz^C$XPSG<2oh{C(BJ#8)J*9~yMVL(T zz_5j}+x?ZG9>E@=y1=?LgjxQ}#B>e*6V)!vC$5{1Um6#XCdMQqOCO}mN~`mFK-OJDAAZb4*8q>r z5->kkL?c^PHLv zVgyNa;V%hn391;@7^NEv%tyWhjko6f_?%H23u4LGHmWbPf2kFIWc)Rb-S*h-p`Xh8 z_LhDE6XU*}>>YnrZte-bWwW=ZbMTM*?C;#Xq5k_8W@6^}k1ed?>0nC8pkQRB>|#sE zAWz7|$VkW_Zt3LgLdeDZzfBt#Q>ULtHij;yqNc|7CO@ysn%bGWSP(L^FtYsLJ^mka z(K_1dI9w>cC+eq6W3qMEseEf$s3Bu;C$j7mpw4UXyqs>3ouB}WOsSXWcUAW)yS9#K zCi1jiP%A0Aft43`|=$Y0^AyZ zXd*^IZDD9TdDV2QJw z&C*4Q4OP+^;wo<Zd9dUbI2kCwpQVzuq|gv;8Ci&_QcAHe4+I;eqTU}pSdyJnX5DByfM5)qdX{Bz zIBUdZjLjK%U(y1eRykH==r>|jk&qtDbZ5S{aNK8};_9S!T&gh3jiES|f!57n%C8u3 zyK;&;LQc()Mgw3-Dl+QXpri!XuMNP+{Lc`{HEtI9D}jHzJ#V4@(Va<_-T-bkaQTk%DLKoBgFa`S@FTzbWd8i9Yw z`5Y)*^RT1Bhy-csA~K4wDGeBjGRxwyl7r=o$L8b;uoPa%*Ls6zH;w4im3WK+PLq)H zC~d(+oYFFNswQOpMRz%v6iNNZgER&(T}Ols8Ha9Mw9ttGvy@QxX=lBI%aIsWjWgT= z(Fo#IjT1St4cjh^*!&yp3TqRo+6oBCjW5ojSr4Gp?J6H)V&X$YE1j(wA8%0+hPWQ+ zko@n=(kkX%HpGe)uESCwK=WsML=KA^nDkOV8GUEoYI7}x2I`!dwHBVVO%jneAtUe+NS2^2U^(*~?u z-HV)m7hHHQuNbNI@3#pv1?XE@nN z?8gh5@gS!e6s2gw7iI|#}u`!w0Ng3joR3j!by0_ zE<3GksYys~%wS0xmCRdcl8D_v84u<;-_6HipanfuN?SNBPJ~QAmGaeRYto>-lk>a^ z6fg5;J<4uR(0aR@vdl2dwr$dQ>yCI_!kwWl%xA{nk3mR;d-?sa)F$DxAeo;@KGwya zsyzv?^{i?>kATH+ru$*e9=0peT_Mh=$lu8obIG!NP6HHmTOHIn2J+?>XwK$#|^mBfFkBbT-{faOj(e zu~BXpgv^`;ju<{v&L(Coq}*ilmt1qSe!=Dx<9*k9)c68^B#CfRc*jaYaqcQ4$4nCn z#b%_@gLkcii^i5FD>h63vboa>TXoQ+GK?K6M%xWz2ue}BgcAUSy0Q(AW?1WUU$4m~a5Z1{M)&2zfUQzmThig^p{_vdRiC}Y#$9MehOz@B^#C{4BPEcDWAROMi< zsGs3`QY6!MX&S4m*kx_0F4Walk)6LPEqH!f;e}O_Pi&kRMw`k#=22zw^%k`pEum?^}b)X z?4EY6Z!q4s_70~kpai^q*7hx|E;Oo#E@eji&&X37NYD_fr$Y9|Qj-~=Q}Tm7$P9Dt~mbNiQ0e|w-z;`s^aPeUNM z_9h)zO5A0+`t<>3(sc@5dGz9v;#iJnVwNSnKt-u+N&5oMmN1`Obj7Bv6TZp}hnfPN z+xF+l7Q@5)`a|}drJw(M2ioRNbF0ATdy3k(JNKhs>GIKc;IZMM!^hMyW7=ZV`CtS>&jQ^2viCg}^6Dp?v)N=ZdZw6}vAYe1_8k}PwC<5TF z|0x2~|8E3lw*Sc4{v&1cf%Z-md;9K8@3ee7wwNa`#bN z9C*~)`u6qh13JVedfJEatjxPVoE>@O6Z)AxZJ$gw6>F&k?*ZK7k7WKgcQJ% z4Fd{Gtw;qsWWgt>p(xTvAj%leI2MaGw>s}3Ec!0S^xG*2F=d1O0(8g40edjuRXn!y zWdySD1{E3cW2r_WJOnB-?7q_SBG%Z#Wog@B#O`~7FPB-}s&+^m!s{zYt_$4yIv2ig znr&UR8wR!7^*3%F+WiA?+I?cq#3C`rZmQtQf?7;DPqhmMLO1iP>gkyk}q{o=16 z77i0s`+{W6ClY1`)~b4hGWF*J)R27n}=BJN27)VaA~c@FP4i zGkBr65ts2Z@wLhJiQkm=$@-}~2|LLGvN7`X(m5$Q37yQZ(xpplJL#9W`;#sJ?ipu# zcfIS*vDd#3swWcDvDxpCJdi8JTPXmttMn$n_NE$()CsmJwk_4`j3cex_OZvj3#t|8 z8oyQqIOt-DVx0Mh1qQjDY+pjYwEQv5r1)&b+Pejp&$+rq!)?Rv zsvuekH7m8{f3w5CHx2=3wk+#rEo{xY3vSL{-W`QL7GA#ZO-~U|Ygdu$43<5B&yXjt zb&=d>fh0G|@7qQp-8y{-mD_*2SbZM`LUpVj>bGCJnSDKw0?;8zm_y}YNrlps|7KJf2)aPNB4S6+#)?#J+(-Op(^9QG{sp+31BJ!ZZu4cqq_a&=-G zD7K~R5w^*ID}a2*P>wl63*=?xJYRHwB^?nTLK4A=4 z=w;eICG5tt%?Qj$Vay4xJ@>XVt_!ZX25k!H{p0-~&&ZFAkCG3BOPPe9z@}sB& zdDk%$GQ1A598aa1@!yF=Gzei>xCMzaQ2S)3%e!Lm%T;b?j(^$~h z(O6g7Xs-e)u1ef|-g}>tp0b{#uE?$$*ZHn!)@yBgx_x}SR!$C)xLCoS$og6hjBZ(1 zIT$#ZIH@%7J1JhoZ?AU0b`^su8|JHM{e5~2pWuCdy*XQWorcdIY{65Rlv|eO~Fh-9R&}EPD4e*z$2OlhXjWN>EtiQ;Y!0j zA`S&l5-C$kzp6M=1tuqv+z_;Vj@n8rc z`rU6w&X5&|w7kDvcP~L<5t0Zfd4B&f823kpY~blWd#O0k$J2N7-oCSxFdy-ciHT9* zesaCMdTBi( zqv4@H*VuOS9chR`##_T1;D&G~bs6;iQ9u2<8-vX4d>$KCuzF6tVb~Hd^&9p*@E!O% z{e1gWe0P06^PTc-xY_WLc*fg)-tO+X+GxH>_ThQ1UgPk}^}ONmLcH8+dH(F;dH!;{ z8S(=4lDNX_c3&OI=L>!Mu#WXp*vhGh1$Z~T7RnK9brtF#971RjRD1*YHNH(hGaqZ0 z7|L?h1xmXcx~sa2x?A~ceI>gZ`HQ!IdfiSMr3gs71WqS|4#B(v$cV`MvJ9jd$uUt1 zP%V0%-@7($mV6b~bDMVT-8U*PVQuu-{JwW`zt;rBeQDGPz~arVE{FGgaM?nRQFnuT z%kWrpfv-V7Xy6b*VuB14xr0**K9O$W1i^vuMa@BvfqtXTdK%6G|K9mMTcY^atXJtT zmIFQz@2m%q6?r=hBa(wFrV`K!2s=>L-;UpZia>HqUxuixauZ6Fx51B>DTc?Y{Q z_3DGR&*BS)d3M6Ii|$4O_XzPozbK)(+~Tpg*mMgg&vZ(IiXumf}GfN59$djq@~`6Rjf4jfwu-;vn@oFm!+gxQeh zp2vsv!iDBC+w9 zgH`f7*``|1kZzQ|O#F5Q(MxipD*PR~@RQ0sKWL9Om_Mf!j>Ry4RtfLafIid^dt`&E zQJ55Bvq0R5rG%RDIf7su31bTOB4faSFq7bVZ&@I_b4h6!jb?zoq-VQ8xt$5!NJo#- zDg%NXV^~zW+;atCl7kGyFyE&FzY2BZ|8<39 z>#ZyNkZAhs)dm_hggB(e@}=yFVRy=f!#`}99rniR9KF>AExgNzfj9&X!j}q(#kK`UG_|bA-`mh ze+hX2ZbIIXe+hk1ZOVoCk_1P+Z+lR*!`=Ud zy}S7XV>@~sz)L@h9Qfk&De~hVc|{M%$pvn>Ep8w*4T%wP>P$~!j(vq`WA>$VsUeti{#na~Tu z#$M=ad{nJ3z!B^AD(VlLx>+v(i#C)Pg4tjI?xhZmeN2$o{BI4K^DemwZB44fg1~nNnSc>F? zfj9a0u&;8UKiP?NHsyDR4>{w!Sc^=D2TyJMo@gR!85WLVKRjW-bgz#qgm<$Gv^9>0 zUEedm8m+Dmdv?1dy4s~fcbrGqUe8a{7?k2UvKB&LtDPnPg@)URiH5hy*xxci>*1HN1sG+s~UI^kyEl5DpbO93*tXgT#X%vPa@Xc!Tdp3@C%#kLw8B z$aLu zV9*dz>9+#a=#b+rJotNl0GDH4WspjpVJ=jky~s{M*I%eId+-SOaW!~aP{ENW9OA!t zd+@6RZdk}}xe&eE)?SDnv_SXk{6N#7g5nzzy@{Mi8@{{cKre|`sJl);`=d|bxAerF z;TixLQ4eeMU2Z)BDEm+N-ls=re;(jfoU7fvNL--%u1~AoN1(wRJeO_{sK301h`<8y zg5HCffAt}I0e6964i=a|^+*E0JUAgyX5Pe|2=6pSdjaIQA)R+?y+6^cyulfV@j~bh zPLQ+3_w|tvTWiJrE&A!EdS3Ar&6Ntsi0ASR<`XT5kwLqaWd~)A!h$uVXv0a^jVSJl z2&4!<$AFmQ;Dqn`5iCt?e5nSdAQDv)#C1%dq$FT)q?zM68zkAshz6wSv4rG_NX96g zsJWt}$c;xeoVc+hrU?(E@W&`nM@5wcVa75A~c7 z+hR9=H0crJv_z57 z1c@}GS_is=_8)_*Lg?|u!xPb!(HwKq2jugD^mzxPTP8GZX)1~22i*DdFAaCp^O7kz zO)nwu@S0L~$Tk7GZ%W*hu2Mp0buK}?cKD(>=);&wa2Qx`SSb5S`&mk1w29p*G1!xb zN=|HTK{_I8@fV|yhY(J5ZRv!l@tLExhi*Crh z!i%gpU=#!^Vwv8J_OKlzIAfy`-V;M`(Q}czktD3e6S>yKuQ;#Fu7=j9Fy|_&=k#RR z_xQCQ+Mn33Uf0!|PZGJJSn|_=4+5*uN?_z0)Ed<&k=|gzGg8MEvcB*7wG69X5?)Z& zHCpdf>73}foB8W9o)0zef=*hqqBf_!#8;61aak;xiTbSMV*&!F#_ zU%t|CNqy#vlh6)u zy5$p7WM`POY7-tPAM%X`FR__hPOnsv-9RHkY4OL_4QyhMDL~E1g$iiJdAH`_WD4XA&Vii{!Dg`Gdm5^H+N?7V&ER!LGekT^l#rsag z03+tF*263w!8o+4j@$;`X3IE2?t-2eoyeGTA@YvY<$%#f64DOK{e%aR^P-ieuqqcP zOm8qvByS#Vq_Q8zeSK{k8(@z0Orle~CszWDHOX$Hz$8!2A^bYALxHp{D1Vb%S2+o# z@Os^=o3u1_KBVi9>knXD>#qQDQJWM>{p*zNem`K1owW zzezKkTu5G8$95!f(){JfE7SzSu3UgRkB|0mKN*{DY`ebFnNzxXS~r`^aoNP$s)4Od zTqfr2thbPWrm}ESpTNeDV2GAt8}m&P7h=aD{;3mgFp#c~8Y=-;tK&9m`AfDRRP741 zyhU>bPAUN|#$VCIz^{?6!fzaDq9a?wB$UmK^~<1|FO-{Zd?Y2d`cu3R@%!jdmAw?U z5LJRA+UlnBcnwgqQFhDvqvg1EhKict?9T6aIH5=q+`oU8OqLjtnBe@?{u;=j)RwcVIcX%S z{tGMKeAyC^Xx-Fr$(oZzeoog>Vw*r+YRjyi!*$|lL+@ESb;Sz>6IfcxAA_n_# zU;ykUWt42BO0Pt_Z+FyX5bnlO8f`EA8J%bQ3;Utu9HsD|qEUg`;zePRB7_u;RiQ+9 z!7`(?2-|#_MYvKldAwB=W{lBj8g)2!Md6`k;I>6v!D9slPC;1s9h&Z#2aoeQIcNtK zS7?x9Ya_vAiq9W&JG-7auG@af$QR2bF#?FY1>@Y45-3lea+4JeR`xk!ts)hvVK#9V zJr;3XhL|`fhod`de>4Bn6o3VO0{b2|p%p2U76Bd|w#%@G9LQDa6~p=?;1oCnSUlAX z%GEM7to#km7~q-o(`C1G9KqF|Rfg+h_m3*hud^*oeqJ6Kwy+fYIPtZgcZ!!=nMav+ zC0Qc*;zVqQb&}&POySlt!=f6%K3-WHU6Yyt=PI7-NTpjL!0{)1Pv-zlk>*hyn_$7F zIM%8q4Dl?NSst{$_0YoqbHx52$T7Iph}^VGDbKqcR|#fttIxdY0MiA2=`Dbx z%xfTPR$|>e{+Uz-wB^K9^g_}v)c>P-r?TDkc%|xIencj4nM=Q^h6%-7Aj2Sn9c!4Y zY>b_K%Bjg25|?CU9F)RqCch9WyDJy(%f^M%E}~KQ;LvgGKzHHjZ0zCabxaN_4)zE( zF;iE4LY-oVekaGf$J_5d@jh|vX|o7-WUz==LQE;Wkh^@mp4l?^V7TZ4&z-#5-9~=> zu&&NyuRh=ulcGo~Q6dAG*OFFPoKOo%LUEZT2)RMc+5CaaeEPNq-924h()Hc0qi1gaHfrbB|9oe z{w-1Y0U|EDpT63&)H1cHuVQN!u>uo`txwd(7*ZqVdh{ylFiIp>v-^$qB)$<+kM;Kg z51{X+TNIDMopFYRdEWX@g)A zucq_B0jd`lmuZr<@B>t=(5FMvdy3SrptV^miq;gDoqy5Xv3H`Dega~)zusqH1cGDCpM?sSfc7&xlMwH zF864kNMaOKU9@`Ex|f4f_SYisVN?H0q4=zI4h_HVsH&dgvfXy)gIxAiZ#g&_k&_Ew zHHzezF(Px$tq;X;K&h{ZiCzG7aL7OTOY0{LI`?$&=x|v;hAdMG7$x2FZ;Ba;&?gt> znQ70WitmW8&4MwK>e8Y%m5%$RJ^QvhqpF5slX`_QE4}CWbrK$A#-A#`Huv+n|Fv+&#kmK*;2mL00 zzPfPOHzx!M#~-9AYEFU|elu}E_ zuiU9QH_=DTUWH^%GKdS{lAXzl+J`seW&@~dP^QZeZ3S$v5$xdW++0GFDcJHyKC)U~ ze4ALje4i>Zu>^t<8Dgg}g&Pm95I?^*3fC*>{+{$qR>VFq01@ zQCt<@EEH*?oj4Pemb8LTus7u2Yl2+WuG$wdyv~y&{EsL##+6DC4L#k}Pjb4^Ce&%) z2gTh4A4)0p`z`Fo&ANn;j!)>qVq#JX;2{e8e&LaDLt;XvL8PG?F+Xa~X)2`sYIUK)=o4_cjEV6^S$8288199qhaOKixkuJ#529*o8$?do}V(mcD@<`V7Q zT=0cIG(Fp;FcpSJ>J=!kjl=6xEXOUO(qHbF{%-97$IeCu%)rN_DYR=Le?7aDa5m<7) z2~JMTUEU-=cJ;g#VxuKVr5uvb?HmqwdT+k=;d&!4e=vMKR=&WsDcX=J9cq3Dp zbYe4y8eS<=qvMR>jvK`_b0ogAD%5*B1bAY^bM31=;_*pdFYt??FL0#Q#y9ETu1h~P z%Dg<>rIui>Sm|WD{Hl349oAD1ePY!CRz#nHec!;<5)Mgl4o9B0A@ZE898_Q%>(%PS z1L}!-na3vL*hCK#dZFB0-W|BiyjG@&6h~;OH?*Ygw~fbS!wA{nMk#FL*jz@fJPvf& z=#EqabsXn(7QZ*f9Le} z$)ib8yTpp14;p93Dd1a8l(yw2W^y}w^(Hj~Iv`1kZ|cv>u2pI3=TWMfF`Y9QGw$|= zvj_k`P@hO9dvwd8Fsmq6XDdDafhn>CpQ)UknU7a|N^v`cybTXYev}dj5g{zCw z&k4gr-6dl8VD48^6MVtPH7c6Z~ha!$?{{UPmEYX{aYD{TIa-Nb4R zz@MjT6zZ*+w~WC3tb(M>eJi_MYropeHEPH1V{@1zH~JCb-&%d04Uyg0Wz0OaKU}-X zxj3u`seGs;P7!!g=t+1kMY9$i_A5Za`meF}iicOgd>{<)`?E=bP-BVKT6qnzWduoJ zoqc-g7&}rItoINSz}xjEiJJWdAJQ&u5X0y#r|#div@5#Ux>LlqtH>Gj6wZf)2J?yK zeY{WW+$W{FHs`wH<|I;#Xq_)|M>MgD$2=P0jICZLxjU_ooxnW|7^|Cav1pSpm&ise z?lrjn1k^VxFy_c&L$8FJsO$DetJzoM<)V{%^v3oS-6UK2et}VZ0z&eBqFSaQsIBCh_x8+K3 z^ZLr$4xOv0Gj+?<7Czpt_oIhytf)*IKX%UwOuR+eo@N&nZGOv21ir7iwTaUtYdt1O z1s9*I?4U23H-!?5_Mq^Rw=LpHEB)jdCVqCZ;q_+T=|Ot_2t=2&Vk|_=4451y0hB~w z&&;Z16>BadC=kSx`?(PyRku90c9YY0z9q^;ND;%z;FCgnirU}j-HeOi9 zbxj}EM_uQ1`IO*ibXV7s2khxWic2Z@O$F_CFBSQ8dA=^2?Trg!p0doFFjro4)?12l zZDPceq_xWORCe^oHT9)rVy?xhE{VsjQS6cPk#Bp`2nVWQveP?8j+VLSle`=5k@Ex5 zPScR9t=^NRY|>1bt!2wS&*iBHX~Zqb*q#j%k~^o%>pl1rE{0U%hIu9WccmxnF*ChL z6iY>mm$-dLC9lMvB2;l{x|v(%kuSzry=y6oheXL59>wdY+w8pXjjon%`6AWw)Z;%J zwM@(e_@fV9i?WAp?r?`YxQl|bgj7A)%vbJ}n^nD`@R zvZ6Dxyp2yLdchSJ^#nsI_p62vm<-gV$)`7(%?#>d-a}C43wJ6AZ_54lYlsdJoP5K6 zU!P*=J#jW~>weg+^b4f6PHr4Ryd*%=uF_j{rdvjLI= z!O;6L(Wg)7kNKGl+9V>rA-5y%q)$>Yc9Ydar|Wck*Mur{_k;1rSjL|F`dGb3H@m)Y>@finQ38zxc3B-DMpTk0ZqN7lU;* zsu>kKmHdjEG99RGQIicUbykL}rT1;uZ_<62teFoxB*9kxAGbb64B3wwJ)|~UPy)%` zN8_j#rtGmp!u-^^8|?)=;^cQP+4#raW-XzQhkw%zziF?It7?5o9DiXw`#6P4(=@p0 z!=gL*Q6o5O%vHm?2K~cxS=v+$^Qq48I{|BdaNEi5a9>(gj()1)VevXz-%kFU5lrz= zA3YLvai-dnvdC9?qFw5K zwTveZQjKhyclBO<)pw_HcZhb(YsPuKzqc@ss0o+i(X942-RAbxo_#rYjiH)?S@0Ra z#V_^oG{{nr&)u4L_?&Nhq%4~2T7{^WW*>i=PiIBx1unLl>dqUD>)`ECA=~#F^!mHx zfv|k$ht?UH4Jk#z`-AJ;`+-S&WkWtzA69zZ9SU6Y9K2`e<(vkIy)FGPqz|T(i@tf( z&s}RsAHzexzkhD|c`j>zSkj-VIW#{W)PFqu5T$vhVSUqoR>)kMvG4dFgY-n2V#7zl zx4pQdSH z9zN3qzj|fzGsrCFAKJAtdH96^dYSyuX2tsS$h-dar6hO$l;3|Lq+5T;(ds-r3G#n_ zh@JuF67z_hJPCfyjfIkCe6KN1AK|h8NaVc_zh{4p#2+K~2PJ*(y8obif*>#gEw{`0 z=Gi5WP1=p%HBM}T^6iiQu8Z?-s`DIsc1Yw~tFCuydrK!blh89#yW4O$20xKu{pqA@ z+*nJt`#8!**DcP~Gt@!)K~gu0{h{t&eO`P0hk-6ABgb;sVWeQv5hRj)!?KDjZkadOg=U->Z6yp7JVthKmr7jT*$hi7Dn z&AQw$nn?xe-Oyo~zIkbTXB=Q}2!vf32(#(LmX^ZE{QDyMw7h+NWi@KgMlcV3G2hfa zkzieBk6KC!k}3A`>D_0^)vk)d(|f5pdb{K=Pwz{4x%J=p5XK1a0G+)`XxiX6<#{d& zy6t21+|uUti7}6JLXEBd<-e(SgpcWQ&@Ad4O@A;cWG+COyO)8|EURD{2GuMSRiPWI zeWT3xO~#}3$eRc82K6)7tfOTCW`J6;CojXFX<5)p!@F3`3?|5iDcLz-DOEgu3rjlO zi&p4KIJRm!IgQx~ia*{BJE>If+8FdtYLFU;iM7KkK<36Es_z-RuCfTH951=NWZGnpjNKmBrrqY`sgx(|uklvBrLlpucfOG{SB_Ps!@4ZDjs1Q(yNDvT^ zrW7esqzH(V7jAj?KkwbS?`5)Qch8>1Y^C4XM=|&$@HTedA z^Z2Z|s_>9xBEID`?e)e2DlwL#S=J_&M$tV*NiqH66tNn-I9gzR3C2K_FknPplv*0> zmSLoCM{-xPyCHayPX4TEdvK>+_t%Tb$RJ^Opdr{C^;5}6x75Cb(&>b8R=&Ttfn&> z1JPQ7_eeTF?&mUnJ9R&5+*!D*`sb`0 z+~?_UKk2rK@%LjDS#sj-lU3?hMCIGbpH(03^0x3m85VrUH$OMqHE&59YInr_*9|#=zhD39BbL+|crByV}(j4PAuZZ}{#>cBAHm$gZR?u6K%| zCEZ(RBg-vVB;Ibc{iKT6;Z7;!zBM)`D41w)vWZ_ajoaG$Z28mrpYuP_`Q?;JNz&^uLbob4FbbZJApawigdH&!RsV)iKUZH# z8jrF%pF8$R*D^@fJs;#rK4?zUGH7#53JV_OX14$hDwR1V>6jAg62~OLeP``_^P#Da z`Fq7xcPrKQ@(tM46E_~73rr-AcgiM(8ToSOHV|qJUu@1R-2x<|;wN;(KdK&H`|+n0 zwQzRtMsxb-iAmVxZL{3$3Co!r_Kz7#8=RpuNnw>vsF$jA5R{R9e@Mg28$>F0!1dnb zWb9HN&9>&ZCDUvGC~Svo#!L6AiXiu1YsAB_S+XSVS+bvb=cjpZcFv9!@)owj<1>^e zKIRjQy}Q_PJ7VFP!teetonyZk0m)Sn5+_<+BhVJwT3fmsj-n@1tbz>}3kdOG#uH{$G+cr{M>V2#LPZ4r(g*Yb6U4j%%A?SFX&n*UqIms7)feO z7YNP};4pRpCi@8-WY_bd7lg}gbj#E_*6dKBrT7Pcvq{N7y+Rwo0}51V9#qM2tvomB zxDq1A^Ch!f0e?G&4B;9DRa!6l1CM4r5<=QA&dsPO4fMSO%oXDk=uUJ|p%wTr{sUrE z&w|QAkd$)p$b$v#f}I*&nuT5b>({x0=iTU@T#bvvz^da`(UZ^FlDoA}PQH7hkIUk> zTkuwJCae`Gr0@PkEOppcQGLYukII8r*>Q{RIwu=KlDiY&lObH=qLAbXLU=A_QjT_A zQ}SeyVg}1df6}|Z-_O`G%4p(}KSFWFV;mf+7^0~AC*9~I2a^8_&pv5^We0>fQJpnI z@=ewbmEx&e{YRDNl=M!=m`=u%iLvE~^<-Vsem*ihUrT?quIYM9t*W=hjXAfH1c%0V z^c^aN3DfJvjRy&U5NP2oA*7GeiC$QTK%U}t*;{_2WKTKPijEvk&xqGU?jMPi9|<1h zfltU#^R;+CRExmD5zSl$zH=Ll_7^zN5$_h)?Ieo*f>)Vz7&_tZpmecf>?ZDA5R0C# z!_Rom@y?gwEhaxQ!sk@sR)tnUR#k0&idNa>XStBcH*%l(57LA?SZE*dt)SM1GXk5n z;Gj>eqrOYv6N`cc!2{B@Z_l3ucB8D*HR_GmBmmv!ggl_gR7kzi8hv_s>?8hzJ)^)( z$RojnPnLm$ZYQiL>l!@$*}4i%w_YKRKJXc2WT$A=cN7M&>L}5+titnnU?hNgkGG1R zJsV$8u}B;9q5HClqYKHzRH7yQMxTqC*W>#EwT3Ppuk>q%OVM=Z$(r39>IZ`3clCAi zS^^8XO<30rBoGZ5)FJpyl zfjEDYxd+xP6sI@OgbF>z1gnNMcA-DMK{KDZnz@EG)}YhUO#45bW3U?Ke4~r5l8sY3 z<=U@e#~vfy63uelN>!X>OVK)cj)fm&n?)qF!}iKIa$U-3_YC{Vt-ze%eM8sz zVJ&z7p8mS<0^MpJyKAqLVFcStwJGKc?Co4;FesiDSD<`J*WF&`8YZ z2Kz=KGe=e({jfFo-g;rOE)tbtF->O|D)#e|`s=4NZQW0sU!T)c z-TF|9u`k=pYByh)oZ7HY-LVu6=iGZciKjuY(QPZG)$aQd2_4 z*hZ&`7?x$O&&~`==9gEgM$gVu*i|>lh0yjo;||BUwB+A8rn^7$F!Tne%Gv^3sTlx9>;&XgKtQlC#Kv?3mPK6q3IcI zP-IcWJQ}|C+(jMvLm-!`Hl{3(uvw(hj&|ve`>Z+WoVJXSd(Fxi`KOq(aetmEyZvHI z(JZ~v=-3;?GS?;=_UEodj)&UFkFV_B#za#8=Jw0{m6K8SMY_Nd`~vrkT#=7qgxb*8 z-VNq}B(xqNV0K8tn5DRsB%w%I0)arSOJR2@j4lQ2QkY%}OCXp~fIz=X;d&|lX7x|C z*MDm~FI$e60)8nR1nz0V0{r0!M<0Fxu6ue2xTCKFfwkt>^0hO9nTm*sib;zC2_t{= z#ue@fB_P;b{7_jCkU;TBfyBjtATf}b5D>%%1o9E=wBe5bmx&R=&dUqtNZ`5c9{UhT zZapIvQ&Cml$B!X)o}QO;8@agpa1p))A09#oUI=rUzz{x!$+`aS4#5Iu%BAJ%2je2R z0TvJ-0N|Gk4Yzj#fTX1V@^t-AvRqQ^pJ2I!xWr}iKX6deK+wMf%TqI~;4_kR&v*8P zo|wTOet5tUdYy@s8`nYaF|eD#=>*F3}RoP9>wvabplUWM$LFqyu(WXH&y%gV?f-|!pl<> z249No3N)Ge05=n+O;J-2%Je}Xse6qhdBiJdai1*-%vp>Hm*mVZ#P1J?ARn@ddK)QU z+V{0U$5d~Sk`_{g81J>M9Q9g5QL>icWlE~UNe9dZ`NpYPIE12>bCeB2Yp#$6@squa z+^C>DOiaPkkPf;Q3P^hyti8Ny7DWoaK96b9YvBg>aWN+}88o=#pDIHHzw~m632tXR zP>(zWKD+_D6%m=);Zyinr{Mqj7-PY(mW$64c57dZ8Sf3IRYy-=36tez``d>i;4dDHLYAflntfDmMK6r@KjbyJeNG#>bgyC@P52$ z&)ESUy)w4t)V6sv+4-xlVNUxj-{h!mZ?Au!Gc@e?FWAM=M*1zdN%q-#|KT?2-EEWB zptoaqG8 z&y%a!Go2i=IH?C=7ZiT9!wZYteV*o9>nC+&N_oJa5{~&+J?mr#D^nMTGrf#h#Z$4k zIjYFTqwF7B!+}0%WBALrfyc;`VpHpy!!z)?PhNQ(+~gtA(xDgI_G$xvPS0G=Oikg; z-HM(J@+8VcFYWm5))~$`s~$^*lALo*)&3q-)Uau?%_y=`KL3z^wRt$-f|@39ZlUg| zZ#(%3BDDFe*d}_VdQST_1xWzHG?*F@@}%v@P9KB%lfkXiu8ysvZ_bQ$QDjq?sYBw9 zNERiVg~+SgD}@3g5~U^OZ{@c*JzxKfV3j_dat+U}Q;~ag;ozYtx>Y{m)t4afJ_9hi zDv>~%1W=)`8xH(s*Jc5SfOIE2&wz^51D4h`-@Ew(b2>S$w1bj&S7fpbUyy0Oi&5G+ z2^&a%!&WXmQ!?ElBR9{h^ouiUci3RmKwYy1vuMqZaSQPFAGe<*3EKAFB z?-!;J$;DKZmUe34#xcwKnt#gDhA~Denw=%EEc3uR`etE_v}w4))Q^zSOp0} zXb7tE){O~rL);VJt{hO{*@h(tWqRvMY{M3byr{g65X3oo9XG zgL2y@wYPmQhbRA`{K0Z0>s_CHxsXboC>`+i%!dwp-sj&@jsX>o@n&ksgLy`A)m;yL3`o<2S>wk-DRN=`%mZr25>X zgt6emL1IOV5trMWT|$g^diLzE5HGsRl8!^L&FVMSNEBZV%lAfOo0sU`vPnGZr;}jp zzowwuCwy&Wbp{nZl3cli&+q){B{otJF(@6jU~}uo*7xeKpA0X2J-O_IA*!3-!OL}MmfkX} z2?`j~&g=}=H2hxo71Tpo|K{sSs6tB0N<=*eQKo3I^kE`3-Mh3j`d9~Ccy+_?gd|n( z$C=10uWDLVy=RNLVwZ-r@%%4@#6RJYFjbb&5Hqiv={>@2&owVxeA^BZOrmggvt~Eb zt<2iJ_EVdgMLR)vpX2I2ME7l3F}!$Z(Pf$2r?EG>#|y>DHr^a<(%Xb!X5|iVn}4Tr z)HIH+`b5ud>hywjK1sWNsXVSQ(A zqM>7Ep5y$H{JC1dY2VLd!HjL5l?3O6ne&yyGMk>6qxFUUN4L(t2YN1 z{NL?t{<^XKd+0SafH`s9Q}=X)J>jw-1ZI$oIF}XIJww+(LhL5A?>!=@#kj-?g`hTo z!;u8_C8}(2d6g5K(7W6&UG|^^OL+(cEF~cUl8{tZ6;lDKs;GigRUfE`iz`cugJr~J z#1#KOCWM*(Q{tp|i%(`xkufyVr&8@CbM%4UXJa5rB)UeZ(}*0Cm5rd# zYm9zF`0;~3L?*7O-_t5d&uoccc$C(Yxem`dt zBAf91%#=k0E0CvOf#`CtUEh56&a(PSYC&uV!o;^=pmsAveuMjcxM2Z94ylaZj)^g| z@6E3_T&_7Qv$`N~p|MF$_^ak`W$+|l<~CT1N{DmFa=%_(BMxYtZRTGmRVZMzyw+E+ zQ2Wb2<;GJ@TPjP^2exc+ZN5F|4k24QOA-=E8;`5#MPb-@Zw`iQ)2(OvQMr{&c=onFDp)6 zyY+@)id6T_-PY-Q`i$ZW-&AO8{N8e~giMo>xg30@Vk;a?cYru-i>M&di|y|C4i$j) zujx=rvs7QVB$4fn%@D93z!=?h)8oBsUnV?+fqphZgOsVICknNUe-#kHKHc$kgZ?q2 zR#?$XD(1z<(a66IB-Ap&cq-ALZFP@b;nG05hX zt*2J4>Gfmrj|e&bm6k*kCyAtyf6gW5jvHTs@`5=069UveZAnoF`0t#&1m|;n&35pv zy#XB%QpR*&FIpP29SEs@8QN&$enB*z_H_DxKbJm8I|T9x0_FsemXVeM0(f{-bX5WW E1(`*28~^|S literal 0 HcmV?d00001 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..a589e15c --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,20 @@ +# Scripts + +This folder contains some scripts used to generate the Waterproof tactics sheets. + +Running +```bash +node createmd.js +``` +Creates the [`tactics-sheet.md`](../docs/tactics-sheet.md) file in the [`../docs`](../docs) folder. + +### Pandoc +Running the following command in the [`../docs`](../docs) folder converts the Markdown file into a webpage +```bash +pandoc tactics-sheet.md -o tactics-sheet.html +``` + +Running the following command in the [`../docs`](../docs) folder converts the Markdown file into a pdf file (see [../docs/tactics-sheet.pdf](../docs/tactics-sheet.pdf)) +```bash +pandoc tactics-sheet.md -o tactics-sheet.pdf --pdf-engine=lualatex -V 'monofont:DejaVu Sans Mono' -V 'mainfont:DejaVu Sans' +``` \ No newline at end of file diff --git a/scripts/createmd.js b/scripts/createmd.js index 357fb585..44ef99dd 100644 --- a/scripts/createmd.js +++ b/scripts/createmd.js @@ -1,8 +1,10 @@ const { writeFileSync } = require("fs"); + +// Open the tactics file const tactics = require("../shared/completions/tactics.json"); -const pdfLocation = "tactics.md"; +const outputLocation = "../docs/tactics-sheet.md"; -let mdContent = `# Waterproof Tactics\n\n`; +let mdContent = `# Waterproof Tactics Sheet\n\n`; for (const tactic of tactics) { mdContent += `## \`${tactic.label}\`\n\n${tactic.description.replaceAll("*", "\\*")}\n\n`; @@ -11,4 +13,4 @@ for (const tactic of tactics) { mdContent += "```coq\n" + tactic.example + "\n```\n\n"; } -writeFileSync(pdfLocation, mdContent); +writeFileSync(outputLocation, mdContent); From 51fdd4f0e5565287897a432e4ca96ad1d7daee8f Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:41:23 +0200 Subject: [PATCH 63/93] Update eslint: ignore scripts folder --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index fd8ca8bd..fdd2522d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ import tseslint from 'typescript-eslint'; const config = tseslint.config( { - ignores: ["editor_output/", "out/", "test_out/", "lib", "*.config.js", "esbuild*.mjs"], + ignores: ["editor_output/", "out/", "test_out/", "lib", "*.config.js", "esbuild*.mjs", "scripts/"], }, { extends: [ From bfe5d22cdc051414956edef74a3fb9c78a5dc2c0 Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:15:54 +0200 Subject: [PATCH 64/93] Remove pdf --- README.md | 2 +- docs/tactics-sheet.pdf | Bin 90812 -> 0 bytes scripts/README.md | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 docs/tactics-sheet.pdf diff --git a/README.md b/README.md index b6b99a66..1f6078ae 100644 --- a/README.md +++ b/README.md @@ -146,4 +146,4 @@ To get started with Waterproof, we recommend going through the tutorial. The tut Waterproof makes use of 'tactics', information on the available tactics, together with explanations and examples can be accessed via the extension or through the repository: * From the Waterproof extension: Navigate to the Waterproof sidebar (accessible via the droplet icon on the left) and click on the `Tactics` button. The panel that now opens shows all available tactics. -* From the repository: The repository contains both a [Markdown](/docs/tactics-sheet.md) and [pdf](/docs//tactics-sheet.pdf) variant of the tactics sheet. +* From the repository: The repository contains a [Markdown](/docs/tactics-sheet.md) version of the tactics sheet. diff --git a/docs/tactics-sheet.pdf b/docs/tactics-sheet.pdf deleted file mode 100644 index 77782329648887163e883e97d8921eb6891c991e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90812 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

3Mw9*Dq*H{Y>Fh_-haNS&j*7pH>YS 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 E1(`*28~^|S diff --git a/scripts/README.md b/scripts/README.md index a589e15c..285f44d4 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -14,7 +14,7 @@ Running the following command in the [`../docs`](../docs) folder converts the Ma pandoc tactics-sheet.md -o tactics-sheet.html ``` -Running the following command in the [`../docs`](../docs) folder converts the Markdown file into a pdf file (see [../docs/tactics-sheet.pdf](../docs/tactics-sheet.pdf)) +Running the following command in the [`../docs`](../docs) folder converts the Markdown file into a pdf file ```bash pandoc tactics-sheet.md -o tactics-sheet.pdf --pdf-engine=lualatex -V 'monofont:DejaVu Sans Mono' -V 'mainfont:DejaVu Sans' -``` \ No newline at end of file +``` From d537bafb0b2d51af4772aab025fe543080c97b6f Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:50:45 +0200 Subject: [PATCH 65/93] Refactor Editor 2 (#168) * Begin (re)moving Waterproof specifics from editor * Fix unit tests and remove kroqed-editor folder * Rename CoqCode to Code * Rename QedStatus to InputAreaStatus * Fix cypress test --- .gitignore | 1 + __tests__/editor-content-construction.test.ts | 20 +++--- __tests__/mathinline.test.ts | 2 +- __tests__/mv-mapping.test.ts | 6 +- __tests__/mvFileToProsemirror.test.ts | 6 +- __tests__/parser.test.ts | 6 +- .../block-extractions.test.ts | 4 +- .../inner-blocks.test.ts | 10 +-- .../top-level-construction.test.ts | 20 +++--- __tests__/prosedoc-construction/utils.test.ts | 12 ++-- cypress/e2e/basic.cy.ts | 2 +- cypress/e2e/with-mock-messages.cy.ts | 4 +- cypress/support/commands.ts | 8 +-- .../block-extraction.ts | 50 +++++++------- .../construct-document.ts | 60 ++++------------ .../inner-blocks.ts | 14 ++-- editor/src/index.ts | 61 +++++++++------- .../codeview/codeblock-input-rule.ts | 27 -------- .../kroqed-editor/codeview/codeblockspec.ts | 12 ---- editor/src/kroqed-editor/codeview/index.ts | 6 -- editor/src/kroqed-editor/index.ts | 2 - .../src/kroqed-editor/mappingModel/index.ts | 1 - .../src/kroqed-editor/mappingModel/mapping.ts | 38 ---------- .../prosedoc-construction/blocks/index.ts | 3 - .../prosedoc-construction/index.ts | 1 - editor/src/kroqed-editor/utilities/types.ts | 18 ----- editor/src/mapping/index.ts | 2 + .../mvFile/helper-functions.ts | 0 .../mappingModel => mapping}/mvFile/index.ts | 0 .../mvFile/nodeUpdate.ts | 34 ++++----- .../mvFile/textUpdate.ts | 12 ++-- .../mappingModel => mapping}/mvFile/types.ts | 12 ++-- .../mvFile/vscodeMapping.ts | 69 ++++++++++--------- .../vFile/helper-functions.ts | 0 .../mappingModel => mapping}/vFile/index.ts | 0 .../vFile/nodeUpdate.ts | 34 ++++----- .../vFile/textUpdate.ts | 12 ++-- .../mappingModel => mapping}/vFile/types.ts | 12 ++-- .../vFile/vscodeMapping.ts | 69 ++++++++++--------- .../autocomplete/coqTerms.json | 0 .../autocomplete/coqTerms.ts | 0 .../autocomplete/emojis.json | 0 .../autocomplete/emojis.ts | 0 .../autocomplete/index.ts | 0 .../autocomplete/renderSymbol.ts | 0 .../autocomplete/symbols.ts | 0 .../autocomplete/tactics.ts | 0 .../codeview/code-plugin.ts} | 16 ++--- .../codeview/color-scheme.ts | 0 .../codeview/coqTheme.json | 0 .../codeview/debouncer.ts | 0 .../src/waterproof-editor/codeview/index.ts | 4 ++ .../codeview/lang-pack/index.ts | 0 .../codeview/lang-pack/syntax.grammar | 0 .../codeview/lang-pack/syntax.grammar.d.ts | 0 .../codeview/lang-pack/syntax.terms.ts | 0 .../codeview/lang-pack/syntax.ts | 0 .../codeview/nodeview.ts | 0 .../commands/command-helpers.ts | 0 .../commands/commands.ts | 0 .../commands/delete-command.ts | 0 .../commands/index.ts | 0 .../commands/insert-command.ts | 0 .../commands/types.ts | 0 .../context-menu/index.ts | 0 .../context-menu/menu.ts | 2 +- .../context-menu/types.ts | 0 .../document}/blocks/block.ts | 0 .../document}/blocks/blocktypes.ts | 18 ++--- .../document/blocks/index.ts | 3 + .../document}/blocks/schema.ts | 0 .../document}/blocks/typeguards.ts | 0 .../document/construct-document.ts | 9 +++ .../src/waterproof-editor/document/index.ts | 2 + .../document}/utils.ts | 17 +++-- .../editor.ts | 41 ++++++----- .../embedded-codemirror-keymap.ts | 0 .../embedded-codemirror/embeddedCodemirror.ts | 0 .../embedded-codemirror/index.ts | 0 .../embedded-codemirror/types.ts | 0 .../file-utils.ts | 0 .../hinting/hint-plugin.ts | 6 +- .../hinting/index.ts | 0 editor/src/waterproof-editor/index.ts | 4 ++ .../inputArea.ts | 0 .../markup-views/CoqdocPlugin.ts | 0 .../markup-views/CoqdocView.ts | 0 .../markup-views/MarkdownPlugin.ts | 0 .../markup-views/MarkdownView.ts | 0 .../markup-views/index.ts | 0 .../switchable-view/EditableView.ts | 0 .../switchable-view/EditorTheme.ts | 0 .../switchable-view/MarkdownSchema.ts | 0 .../switchable-view/RenderedView.ts | 0 .../switchable-view/SwitchableView.ts | 0 .../switchable-view/editorTheme.json | 0 .../markup-views/switchable-view/index.ts | 0 .../math-integration/index.ts | 0 .../math-integration/nodespecs.ts | 0 .../menubar/index.ts | 0 .../menubar/menubar.ts | 0 .../osType.ts | 0 .../progressBar.ts | 0 .../qedStatus.ts | 12 ++-- .../schema/index.ts | 0 .../schema/notes.md | 0 .../schema/schema-nodes.ts | 0 .../schema/schema.ts | 0 .../styles/autocomplete.css | 0 .../styles/context-menu.css | 0 .../styles/coqdoc.css | 0 .../styles/freeze.css | 0 .../styles/hints.css | 0 .../styles/index.ts | 0 .../styles/input-area.css | 0 .../styles/markdown.css | 0 .../styles/math.css | 0 .../styles/menubar.css | 0 .../styles/notifications.css | 0 .../styles/progressBar.css | 0 .../styles/spinner.css | 0 .../styles/style.css | 0 .../styles/waterproof.css | 0 .../translation/Translator.ts | 16 ++--- .../translation/index.ts | 0 .../translation/toProsemirror/index.ts | 0 .../toProsemirror/mvFileToProsemirror.ts | 0 .../translation/toProsemirror/parseAsMv.ts | 0 .../translation/toProsemirror/parser.ts | 0 .../translation/types.ts | 0 editor/src/waterproof-editor/types.ts | 35 ++++++++++ .../utilities/index.ts | 0 .../utilities/prosemirror.ts | 0 package.json | 2 +- shared/FileFormat.ts | 3 +- shared/{QedStatus.ts => InputAreaStatus.ts} | 4 +- shared/Messages.ts | 4 +- shared/index.ts | 2 +- src/lsp-client/client.ts | 4 +- src/lsp-client/qedStatus.ts | 10 +-- src/pm-editor/fileUtils.ts | 7 +- src/pm-editor/pmWebview.ts | 11 ++- 142 files changed, 415 insertions(+), 467 deletions(-) rename editor/src/{kroqed-editor/prosedoc-construction => document-construction}/block-extraction.ts (87%) rename editor/src/{kroqed-editor/prosedoc-construction => document-construction}/construct-document.ts (58%) rename editor/src/{kroqed-editor/prosedoc-construction/blocks => document-construction}/inner-blocks.ts (84%) delete mode 100644 editor/src/kroqed-editor/codeview/codeblock-input-rule.ts delete mode 100644 editor/src/kroqed-editor/codeview/codeblockspec.ts delete mode 100644 editor/src/kroqed-editor/codeview/index.ts delete mode 100644 editor/src/kroqed-editor/index.ts delete mode 100644 editor/src/kroqed-editor/mappingModel/index.ts delete mode 100644 editor/src/kroqed-editor/mappingModel/mapping.ts delete mode 100644 editor/src/kroqed-editor/prosedoc-construction/blocks/index.ts delete mode 100644 editor/src/kroqed-editor/prosedoc-construction/index.ts delete mode 100644 editor/src/kroqed-editor/utilities/types.ts create mode 100644 editor/src/mapping/index.ts rename editor/src/{kroqed-editor/mappingModel => mapping}/mvFile/helper-functions.ts (100%) rename editor/src/{kroqed-editor/mappingModel => mapping}/mvFile/index.ts (100%) rename editor/src/{kroqed-editor/mappingModel => mapping}/mvFile/nodeUpdate.ts (97%) rename editor/src/{kroqed-editor/mappingModel => mapping}/mvFile/textUpdate.ts (96%) rename editor/src/{kroqed-editor/mappingModel => mapping}/mvFile/types.ts (91%) rename editor/src/{kroqed-editor/mappingModel => mapping}/mvFile/vscodeMapping.ts (94%) rename editor/src/{kroqed-editor/mappingModel => mapping}/vFile/helper-functions.ts (100%) rename editor/src/{kroqed-editor/mappingModel => mapping}/vFile/index.ts (100%) rename editor/src/{kroqed-editor/mappingModel => mapping}/vFile/nodeUpdate.ts (97%) rename editor/src/{kroqed-editor/mappingModel => mapping}/vFile/textUpdate.ts (96%) rename editor/src/{kroqed-editor/mappingModel => mapping}/vFile/types.ts (90%) rename editor/src/{kroqed-editor/mappingModel => mapping}/vFile/vscodeMapping.ts (94%) rename editor/src/{kroqed-editor => waterproof-editor}/autocomplete/coqTerms.json (100%) rename editor/src/{kroqed-editor => waterproof-editor}/autocomplete/coqTerms.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/autocomplete/emojis.json (100%) rename editor/src/{kroqed-editor => waterproof-editor}/autocomplete/emojis.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/autocomplete/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/autocomplete/renderSymbol.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/autocomplete/symbols.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/autocomplete/tactics.ts (100%) rename editor/src/{kroqed-editor/codeview/coqcodeplugin.ts => waterproof-editor/codeview/code-plugin.ts} (87%) rename editor/src/{kroqed-editor => waterproof-editor}/codeview/color-scheme.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/codeview/coqTheme.json (100%) rename editor/src/{kroqed-editor => waterproof-editor}/codeview/debouncer.ts (100%) create mode 100644 editor/src/waterproof-editor/codeview/index.ts rename editor/src/{kroqed-editor => waterproof-editor}/codeview/lang-pack/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/codeview/lang-pack/syntax.grammar (100%) rename editor/src/{kroqed-editor => waterproof-editor}/codeview/lang-pack/syntax.grammar.d.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/codeview/lang-pack/syntax.terms.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/codeview/lang-pack/syntax.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/codeview/nodeview.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/commands/command-helpers.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/commands/commands.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/commands/delete-command.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/commands/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/commands/insert-command.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/commands/types.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/context-menu/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/context-menu/menu.ts (97%) rename editor/src/{kroqed-editor => waterproof-editor}/context-menu/types.ts (100%) rename editor/src/{kroqed-editor/prosedoc-construction => waterproof-editor/document}/blocks/block.ts (100%) rename editor/src/{kroqed-editor/prosedoc-construction => waterproof-editor/document}/blocks/blocktypes.ts (87%) create mode 100644 editor/src/waterproof-editor/document/blocks/index.ts rename editor/src/{kroqed-editor/prosedoc-construction => waterproof-editor/document}/blocks/schema.ts (100%) rename editor/src/{kroqed-editor/prosedoc-construction => waterproof-editor/document}/blocks/typeguards.ts (100%) create mode 100644 editor/src/waterproof-editor/document/construct-document.ts create mode 100644 editor/src/waterproof-editor/document/index.ts rename editor/src/{kroqed-editor/prosedoc-construction => waterproof-editor/document}/utils.ts (79%) rename editor/src/{kroqed-editor => waterproof-editor}/editor.ts (93%) rename editor/src/{kroqed-editor => waterproof-editor}/embedded-codemirror/embedded-codemirror-keymap.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/embedded-codemirror/embeddedCodemirror.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/embedded-codemirror/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/embedded-codemirror/types.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/file-utils.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/hinting/hint-plugin.ts (98%) rename editor/src/{kroqed-editor => waterproof-editor}/hinting/index.ts (100%) create mode 100644 editor/src/waterproof-editor/index.ts rename editor/src/{kroqed-editor => waterproof-editor}/inputArea.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/CoqdocPlugin.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/CoqdocView.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/MarkdownPlugin.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/MarkdownView.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/switchable-view/EditableView.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/switchable-view/EditorTheme.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/switchable-view/MarkdownSchema.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/switchable-view/RenderedView.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/switchable-view/SwitchableView.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/switchable-view/editorTheme.json (100%) rename editor/src/{kroqed-editor => waterproof-editor}/markup-views/switchable-view/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/math-integration/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/math-integration/nodespecs.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/menubar/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/menubar/menubar.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/osType.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/progressBar.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/qedStatus.ts (93%) rename editor/src/{kroqed-editor => waterproof-editor}/schema/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/schema/notes.md (100%) rename editor/src/{kroqed-editor => waterproof-editor}/schema/schema-nodes.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/schema/schema.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/autocomplete.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/context-menu.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/coqdoc.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/freeze.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/hints.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/input-area.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/markdown.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/math.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/menubar.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/notifications.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/progressBar.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/spinner.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/style.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/styles/waterproof.css (100%) rename editor/src/{kroqed-editor => waterproof-editor}/translation/Translator.ts (71%) rename editor/src/{kroqed-editor => waterproof-editor}/translation/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/translation/toProsemirror/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/translation/toProsemirror/mvFileToProsemirror.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/translation/toProsemirror/parseAsMv.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/translation/toProsemirror/parser.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/translation/types.ts (100%) create mode 100644 editor/src/waterproof-editor/types.ts rename editor/src/{kroqed-editor => waterproof-editor}/utilities/index.ts (100%) rename editor/src/{kroqed-editor => waterproof-editor}/utilities/prosemirror.ts (100%) rename shared/{QedStatus.ts => InputAreaStatus.ts} (83%) diff --git a/.gitignore b/.gitignore index cee7c65d..b8514427 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ test_out/ __tests_output__/ **/*.cache ExampleFiles/ +/cypress/screenshots/ \ No newline at end of file diff --git a/__tests__/editor-content-construction.test.ts b/__tests__/editor-content-construction.test.ts index c223af46..2d8d7d6c 100644 --- a/__tests__/editor-content-construction.test.ts +++ b/__tests__/editor-content-construction.test.ts @@ -1,4 +1,4 @@ -import { checkPrePost, fixLessThanBug } from "../editor/src/kroqed-editor/file-utils"; +import { checkPrePost, fixLessThanBug } from "../editor/src/waterproof-editor/file-utils"; import { Message, MessageType } from "../shared"; class PostManager { @@ -38,10 +38,10 @@ test("TEMP: Add space between '<' and characters following it #4", () => { const pm = new PostManager(); expect(fixLessThanBug("```coq\n10<10\n```", (msg) => pm.post(msg))).toBe("```coq\n10< 10\n```"); expect(pm.storage).toStrictEqual>([{ - type: MessageType.docChange, + type: MessageType.docChange, body: { startInFile: 10, - endInFile: 10, + endInFile: 10, finalText: " " } }]); @@ -51,10 +51,10 @@ test("TEMP: Add space between '<' and characters following it #5", () => { const pm = new PostManager(); expect(fixLessThanBug("\n10", (msg) => pm.post(msg))).toBe("\n10< z\n"); expect(pm.storage).toStrictEqual>([{ - type: MessageType.docChange, + type: MessageType.docChange, body: { - startInFile: 16, - endInFile: 16, + startInFile: 16, + endInFile: 16, finalText: " " } }]); @@ -84,8 +84,8 @@ test("TEMP: Leave
in markdown untouched #2", () => { expect(pm.storage).toStrictEqual>([{ type: MessageType.docChange, body: { - startInFile: 19, - endInFile: 19, + startInFile: 19, + endInFile: 19, finalText: " " } }]); @@ -103,8 +103,8 @@ test("TEMP: Leave


in markdown untouched #2", () => { expect(pm.storage).toStrictEqual>([{ type: MessageType.docChange, body: { - startInFile: 19, - endInFile: 19, + startInFile: 19, + endInFile: 19, finalText: " " } }]); diff --git a/__tests__/mathinline.test.ts b/__tests__/mathinline.test.ts index 6261bf65..67efcdec 100644 --- a/__tests__/mathinline.test.ts +++ b/__tests__/mathinline.test.ts @@ -1,4 +1,4 @@ -import { toMathInline } from "../editor/src/kroqed-editor/translation/toProsemirror/parser" +import { toMathInline } from "../editor/src/waterproof-editor/translation/toProsemirror/parser" test("Replace $ inside of markdown", () => { diff --git a/__tests__/mv-mapping.test.ts b/__tests__/mv-mapping.test.ts index 04c527b8..dad8d7ee 100644 --- a/__tests__/mv-mapping.test.ts +++ b/__tests__/mv-mapping.test.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // Disable because the @ts-expect-error clashes with the tests -import { TextDocMappingMV as TextDocMapping } from "../editor/src/kroqed-editor/mappingModel/mvFile"; +import { TextDocMappingMV as TextDocMapping } from "../editor/src/mapping/mvFile"; import { ReplaceStep } from "prosemirror-transform"; -import { WaterproofSchema } from "../editor/src/kroqed-editor/schema"; -import { translateMvToProsemirror } from "../editor/src/kroqed-editor/translation/toProsemirror"; +import { WaterproofSchema } from "../editor/src/waterproof-editor/schema"; +import { translateMvToProsemirror } from "../editor/src/waterproof-editor/translation/toProsemirror"; import { expect } from "@jest/globals"; test("Normal coqdown", () => { diff --git a/__tests__/mvFileToProsemirror.test.ts b/__tests__/mvFileToProsemirror.test.ts index 81f7e57d..2521a8ac 100644 --- a/__tests__/mvFileToProsemirror.test.ts +++ b/__tests__/mvFileToProsemirror.test.ts @@ -1,6 +1,6 @@ /* eslint-disable no-useless-escape */ // Disable due to test data including latex code -import { translateMvToProsemirror } from "../editor/src/kroqed-editor/translation/toProsemirror/mvFileToProsemirror"; +import { translateMvToProsemirror } from "../editor/src/waterproof-editor/translation/toProsemirror/mvFileToProsemirror"; import { expect } from "@jest/globals"; test("Expect empty input to return empty output", () => { @@ -72,7 +72,7 @@ test("entire document", () => { }) test("entire document2", () => { - const docString = `# This is a header. + const docString = `# This is a header. This is a paragraph. Paragraphs support inline LaTeX like $5+3=22$. Underneath you'll find a math display block. $$ @@ -102,7 +102,7 @@ Proof. - simpl. rewrite IHl. simpl. reflexivity. Qed. \`\`\`` - const predict = `# This is a header. + const predict = `# This is a header. This is a paragraph. Paragraphs support inline LaTeX like $5+3=22$. Underneath you'll find a math display block. diff --git a/__tests__/parser.test.ts b/__tests__/parser.test.ts index fddea38d..3b83a0b8 100644 --- a/__tests__/parser.test.ts +++ b/__tests__/parser.test.ts @@ -1,4 +1,4 @@ -import { translateCoqDoc } from "../editor/src/kroqed-editor/translation/toProsemirror/parser"; +import { translateCoqDoc } from "../editor/src/waterproof-editor/translation/toProsemirror/parser"; /* @@ -82,12 +82,12 @@ test("Preserves whitespace inside coq code cell.", () => { test("From indented list in Coqdoc comments, make markdown list", () => { - + expect(translateCoqDoc("- First item\n- Second item\n - Indented item\n - Second indented item\n- Third item")) .toBe(`- First item\n- Second item\n - Indented item\n - Second indented item\n- Third item`); }); -/* TEMPLATE +/* TEMPLATE test("name", () => { expect(translateCoqDoc("input")).toBe("expected output"); }); diff --git a/__tests__/prosedoc-construction/block-extractions.test.ts b/__tests__/prosedoc-construction/block-extractions.test.ts index 0c8e90d2..936a9a2c 100644 --- a/__tests__/prosedoc-construction/block-extractions.test.ts +++ b/__tests__/prosedoc-construction/block-extractions.test.ts @@ -1,5 +1,5 @@ -import { extractCoqBlocks, extractCoqDoc, extractHintBlocks, extractInputBlocks, extractMathDisplayBlocks, extractMathDisplayBlocksCoqDoc } from "../../editor/src/kroqed-editor/prosedoc-construction/block-extraction"; -import { isCoqBlock, isCoqDocBlock, isHintBlock, isInputAreaBlock, isMathDisplayBlock } from "../../editor/src/kroqed-editor/prosedoc-construction/blocks/typeguards"; +import { extractCoqBlocks, extractCoqDoc, extractHintBlocks, extractInputBlocks, extractMathDisplayBlocks, extractMathDisplayBlocksCoqDoc } from "../../editor/src/document-construction/block-extraction"; +import { isCoqBlock, isCoqDocBlock, isHintBlock, isInputAreaBlock, isMathDisplayBlock } from "../../editor/src/waterproof-editor/document/blocks/typeguards"; test("Identify input blocks", () => { const document = "# Example\n\n# Test input area\n\n"; diff --git a/__tests__/prosedoc-construction/inner-blocks.test.ts b/__tests__/prosedoc-construction/inner-blocks.test.ts index 315df8a6..2712b4ad 100644 --- a/__tests__/prosedoc-construction/inner-blocks.test.ts +++ b/__tests__/prosedoc-construction/inner-blocks.test.ts @@ -1,10 +1,10 @@ -import { createCoqDocInnerBlocks, createCoqInnerBlocks, createInputAndHintInnerBlocks } from "../../editor/src/kroqed-editor/prosedoc-construction/blocks/inner-blocks"; -import { isCoqCodeBlock, isCoqDocBlock, isCoqMarkdownBlock, isMathDisplayBlock } from "../../editor/src/kroqed-editor/prosedoc-construction/blocks/typeguards"; +import { createCoqDocInnerBlocks, createCoqInnerBlocks, createInputAndHintInnerBlocks } from "../../editor/src/document-construction/inner-blocks"; +import { isCoqCodeBlock, isCoqDocBlock, isCoqMarkdownBlock, isMathDisplayBlock } from "../../editor/src/waterproof-editor/document/blocks/typeguards"; import { expect } from "@jest/globals"; test("Inner input area (and hint) blocks", () => { const inputAreaContent = "$$1028 + 23 = ?$$\n```coq\nCompute 1028 + 23.\n```"; - + const blocks = createInputAndHintInnerBlocks(inputAreaContent); expect(blocks.length).toBe(2); @@ -28,7 +28,7 @@ test("Inner coq blocks", () => { // One block for the coq content and one block for the comment. expect(blocks.length).toBe(2); expect(isCoqCodeBlock(blocks[0])).toBe(true); - + expect(blocks[0].stringContent).toBe("Compute 1 + 1."); expect(blocks[0].range.from).toBe(0); expect(blocks[0].range.to).toBe(14); @@ -37,7 +37,7 @@ test("Inner coq blocks", () => { expect(blocks[1].stringContent).toBe("* Header "); expect(blocks[1].range.from).toBe(14); expect(blocks[1].range.to).toBe(coqContent.length); - + expect(blocks[1].innerBlocks?.length).toBe(1); expect(isCoqMarkdownBlock(blocks[1].innerBlocks![0])).toBe(true); }); diff --git a/__tests__/prosedoc-construction/top-level-construction.test.ts b/__tests__/prosedoc-construction/top-level-construction.test.ts index a961b354..6e7103e3 100644 --- a/__tests__/prosedoc-construction/top-level-construction.test.ts +++ b/__tests__/prosedoc-construction/top-level-construction.test.ts @@ -1,9 +1,9 @@ /* eslint-disable no-useless-escape */ // Disable due to latex code in sample data -import { topLevelBlocksMV, topLevelBlocksV } from "../../editor/src/kroqed-editor/prosedoc-construction"; +import { blocksFromMV, blocksFromV } from "../../editor/src/document-construction/construct-document"; import { expect } from '@jest/globals'; -import { MarkdownBlock } from "../../editor/src/kroqed-editor/prosedoc-construction/blocks/blocktypes"; -import { isHintBlock, isInputAreaBlock, isMarkdownBlock, isMathDisplayBlock, isCoqBlock } from "../../editor/src/kroqed-editor/prosedoc-construction/blocks/typeguards"; +import { MarkdownBlock } from "../../editor/src/waterproof-editor/document/blocks"; +import { isHintBlock, isInputAreaBlock, isMarkdownBlock, isMathDisplayBlock, isCoqBlock } from "../../editor/src/waterproof-editor/document/blocks/typeguards"; const inputDocumentMV = `# Example document @@ -31,27 +31,27 @@ Random Markdown list: // FIXME: Add checks for prewhite and postwhite here. test("Parse top level blocks (MV)", () => { - const blocks = topLevelBlocksMV(inputDocumentMV); + const blocks = blocksFromMV(inputDocumentMV); expect(blocks.length).toBe(8); expect(isMarkdownBlock(blocks[0])).toBe(true); expect(blocks[0].stringContent).toBe("# Example document\n"); - + expect(isHintBlock(blocks[1])).toBe(true); expect(blocks[1].stringContent).toBe("\n```coq\nRequire Import ZArith.\n```\n"); - + expect(isMarkdownBlock(blocks[2])).toBe(true); expect((blocks[2] as MarkdownBlock).isNewLineOnly).toBe(true); expect(isInputAreaBlock(blocks[3])).toBe(true); expect(blocks[3].stringContent).toBe("\n$$1028 + 23 = ?$$\n```coq\nCompute 1028 + 23.\n```\n"); - + expect(isMarkdownBlock(blocks[4])).toBe(true); expect(blocks[4].stringContent).toBe("\n#### Markdown content\n"); - + expect(isMathDisplayBlock(blocks[5])).toBe(true); expect(blocks[5].stringContent).toBe(" \int_0^2 x dx "); - + expect(isCoqBlock(blocks[6])).toBe(true); expect(blocks[6].stringContent).toBe("Compute 1 + 1."); @@ -74,7 +74,7 @@ Qed. `; test("Parse top level blocks (V)", () => { - const blocks = topLevelBlocksV(inputDocumentV); + const blocks = blocksFromV(inputDocumentV); // coqblock, hint, input, coqblock expect(blocks.length).toBe(4); diff --git a/__tests__/prosedoc-construction/utils.test.ts b/__tests__/prosedoc-construction/utils.test.ts index 5997f628..fe4c577d 100644 --- a/__tests__/prosedoc-construction/utils.test.ts +++ b/__tests__/prosedoc-construction/utils.test.ts @@ -1,6 +1,6 @@ -import { Block } from "../../editor/src/kroqed-editor/prosedoc-construction/blocks"; -import { text } from "../../editor/src/kroqed-editor/prosedoc-construction/blocks/schema"; -import { extractInterBlockRanges, iteratePairs, maskInputAndHints, sortBlocks } from "../../editor/src/kroqed-editor/prosedoc-construction/utils"; +import { Block } from "../../editor/src/waterproof-editor/document/blocks"; +import { text } from "../../editor/src/waterproof-editor/document/blocks/schema"; +import { extractInterBlockRanges, iteratePairs, maskInputAndHints, sortBlocks } from "../../editor/src/waterproof-editor/document/utils"; const toProseMirror = () => text("null"); const debugPrint = () => null; @@ -9,7 +9,7 @@ test("Sort blocks #1", () => { const stringContent = ""; const testBlocks = [ - {type: "second", range: {from: 1, to: 2}, stringContent, toProseMirror, debugPrint}, + {type: "second", range: {from: 1, to: 2}, stringContent, toProseMirror, debugPrint}, {type: "first", range: {from: 0, to: 1}, stringContent, toProseMirror, debugPrint} ]; @@ -23,7 +23,7 @@ test("Sort blocks #2", () => { const stringContent = ""; const testBlocks = [ - {type: "second", range: {from: 1, to: 2}, stringContent, toProseMirror, debugPrint}, + {type: "second", range: {from: 1, to: 2}, stringContent, toProseMirror, debugPrint}, {type: "first", range: {from: 0, to: 1}, stringContent, toProseMirror, debugPrint}, {type: "third", range: {from: 2, to: 3}, stringContent, toProseMirror, debugPrint} ]; @@ -40,7 +40,7 @@ test("Sort blocks #2", () => { // const stringContent = ""; // const toProseMirror = () => null; // const testBlocks = [ -// {type: "second", range: {from: 0, to: 1}, stringContent, toProseMirror}, +// {type: "second", range: {from: 0, to: 1}, stringContent, toProseMirror}, // {type: "first", range: {from: 0, to: 1}, stringContent, toProseMirror} // ]; diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts index af6c8829..91071422 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.ts @@ -44,7 +44,7 @@ describe('Basic tests', () => { cy.window().then((win) => { win.postMessage({type: MessageType.teacher, body: true}) }); cy.get(".markdown-view").type("\n## Hello World"); cy.get(".markdown-view").should("contain.text", "Hello World"); - cy.nthCoqCode(0).click(); // to reset h1 + cy.nthCode(0).click(); // to reset h1 cy.get("H2").should("exist"); // We record edits in the 'edits' global variable diff --git a/cypress/e2e/with-mock-messages.cy.ts b/cypress/e2e/with-mock-messages.cy.ts index 48d96e9a..963409ba 100644 --- a/cypress/e2e/with-mock-messages.cy.ts +++ b/cypress/e2e/with-mock-messages.cy.ts @@ -1,5 +1,5 @@ /// -import { QedStatus } from "../../shared"; +import { InputAreaStatus } from "../../shared"; import { Message, MessageType } from "../../shared/Messages"; const edits = []; @@ -31,7 +31,7 @@ const messages: Array = [ { "type": MessageType.qedStatus, "body": [ - QedStatus.InvalidInputArea + InputAreaStatus.Invalid ] }, { diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index ccf03da3..f29b0d74 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -8,11 +8,11 @@ Cypress.Commands.add("nthInputArea", (n) => { cy.get("waterproofinput").eq(n) }); -Cypress.Commands.add("coqCode", () => { +Cypress.Commands.add("code", () => { cy.get('coqblock > .cm-editor > .cm-scroller > .cm-content') }); -Cypress.Commands.add("nthCoqCode", (n) => { +Cypress.Commands.add("nthCode", (n) => { cy.get('coqblock > .cm-editor > .cm-scroller > .cm-content').eq(n) }); @@ -25,8 +25,8 @@ declare namespace Cypress { /** Command to find the nth Waterproof input-area */ nthInputArea : (n : number) => Chainable /** Command to find all Waterproof coq code blocks (CodeMirror instances). */ - coqCode : () => Chainable + code : () => Chainable /** Command to find the nth Waterproof coq code block (CodeMirror instance). */ - nthCoqCode : (n: number) => Chainable + nthCode : (n: number) => Chainable } } diff --git a/editor/src/kroqed-editor/prosedoc-construction/block-extraction.ts b/editor/src/document-construction/block-extraction.ts similarity index 87% rename from editor/src/kroqed-editor/prosedoc-construction/block-extraction.ts rename to editor/src/document-construction/block-extraction.ts index 0bc2bf8b..cdd090d7 100644 --- a/editor/src/kroqed-editor/prosedoc-construction/block-extraction.ts +++ b/editor/src/document-construction/block-extraction.ts @@ -1,6 +1,6 @@ -import { FileFormat } from "../../../../shared"; -import { Block, CoqBlock, HintBlock, InputAreaBlock, MathDisplayBlock } from "./blocks"; -import { CoqDocBlock } from "./blocks/blocktypes"; +import { FileFormat } from "../../../shared"; +import { Block, CoqBlock, HintBlock, InputAreaBlock, MathDisplayBlock, CoqDocBlock } from "../waterproof-editor/document"; +import { createCoqDocInnerBlocks, createCoqInnerBlocks, createInputAndHintInnerBlocks } from "./inner-blocks"; const regexes = { // coq: /```coq\n([\s\S]*?)\n```/g, @@ -13,9 +13,9 @@ const regexes = { } /** - * Create input blocks from document string. - * - * Uses regexes to search for + * Create input blocks from document string. + * + * Uses regexes to search for * - `` and `` tags (mv files) * - `(* begin input *)` and `(* end input *)` (v files) */ @@ -32,7 +32,7 @@ function extractInputBlocksMV(document: string) { const inputAreaBlocks = Array.from(input_areas).map((input_area) => { if (input_area.index === undefined) throw new Error("Index of input_area is undefined"); const range = { from: input_area.index, to: input_area.index + input_area[0].length }; - return new InputAreaBlock(input_area[1], range); + return new InputAreaBlock(input_area[1], range, createInputAndHintInnerBlocks); }); return inputAreaBlocks; @@ -44,17 +44,17 @@ function extractInputBlocksV(document: string) { const inputAreaBlocks = Array.from(input_areas).map((input_area) => { if (input_area.index === undefined) throw new Error("Index of input_area is undefined"); const range = { from: input_area.index, to: input_area.index + input_area[0].length }; - return new InputAreaBlock("```coq\n" + input_area[1] + "\n```", range); + return new InputAreaBlock("```coq\n" + input_area[1] + "\n```", range, createInputAndHintInnerBlocks); }); return inputAreaBlocks; } /** - * Create hint blocks from document string. - * - * Uses regexes to search for - * - `` and `` tags (mv files) + * Create hint blocks from document string. + * + * Uses regexes to search for + * - `` and `` tags (mv files) * - `(* begin hint : [hint title] *)` and `(* end hint *)` (v files) */ export function extractHintBlocks(document: string, fileFormat: (FileFormat.MarkdownV | FileFormat.RegularV) = FileFormat.MarkdownV): HintBlock[] { @@ -71,7 +71,7 @@ function extractHintBlocksMV(document: string) { const hintBlocks = Array.from(hints).map((hint) => { if (hint.index === undefined) throw new Error("Index of hint is undefined"); const range = { from: hint.index, to: hint.index + hint[0].length }; - return new HintBlock(hint[2], hint[1], range); + return new HintBlock(hint[2], hint[1], range, createInputAndHintInnerBlocks); }); return hintBlocks; @@ -84,7 +84,7 @@ function extractHintBlocksV(document: string) { if (hint.index === undefined) throw new Error("Index of hint is undefined"); const range = { from: hint.index, to: hint.index + hint[0].length }; // This is incorrect as we should wrap the content part in a coqblock. - return new HintBlock(`\`\`\`coq\n${hint[2]}\n\`\`\``, hint[1], range); + return new HintBlock(`\`\`\`coq\n${hint[2]}\n\`\`\``, hint[1], range, createInputAndHintInnerBlocks); }); return hintBlocks; @@ -92,7 +92,7 @@ function extractHintBlocksV(document: string) { /** * Create math display blocks from document string. - * + * * Uses regexes to search for `$$`. */ export function extractMathDisplayBlocks(inputDocument: string) { @@ -107,8 +107,8 @@ export function extractMathDisplayBlocks(inputDocument: string) { /** * Create coq blocks from document string. - * - * Uses regexes to search for ```coq and ``` markers. + * + * Uses regexes to search for ```coq and ``` markers. */ export function extractCoqBlocks(inputDocument: string) { const coq_code = Array.from(inputDocument.matchAll(regexes.coq)); @@ -116,7 +116,7 @@ export function extractCoqBlocks(inputDocument: string) { const coqBlocks = coq_code.map((coq) => { if (coq.index === undefined) throw new Error("Index of coq is undefined"); const range = { from: coq.index, to: coq.index + coq[0].length }; - // TODO: Documentation for this: + // TODO: Documentation for this: // - coq[0] the match; // - coq[1] capture group 1, prePreWhite; // - coq[2] ..., prePostWhite; @@ -130,20 +130,20 @@ export function extractCoqBlocks(inputDocument: string) { const postPreWhite = coq[4] == "\n" ? "newLine" : ""; const postPostWhite = coq[5] == "\n" ? "newLine" : ""; - return new CoqBlock(content, prePreWhite, prePostWhite, postPreWhite, postPostWhite, range); + return new CoqBlock(content, prePreWhite, prePostWhite, postPreWhite, postPostWhite, range, createCoqInnerBlocks); }); return coqBlocks; } /** * Create blocks based on ranges. - * + * * Extracts the text content of the ranges and creates blocks from them. */ export function extractBlocksUsingRanges( - inputDocument: string, - ranges: {from: number, to: number}[], - BlockConstructor: new (content: string, range: { from: number, to: number }) => BlockType ): BlockType[] + inputDocument: string, + ranges: {from: number, to: number}[], + BlockConstructor: new (content: string, range: { from: number, to: number }) => BlockType ): BlockType[] { const blocks = ranges.map((range) => { const content = inputDocument.slice(range.from, range.to); @@ -170,13 +170,13 @@ export function extractCoqDoc(input: string): CoqDocBlock[] { const preWhite = comment[1] == "\n" ? "newLine" : ""; const postWhite = comment[3] == "\n" ? "newLine" : ""; - return new CoqDocBlock(content, preWhite, postWhite, range); + return new CoqDocBlock(content, preWhite, postWhite, range, createCoqDocInnerBlocks); }); // TODO: Is there a better location for this? const pruned = coqDocBlocks.filter(block => { - return block.range.from !== block.range.to; + return block.range.from !== block.range.to; }); return pruned; diff --git a/editor/src/kroqed-editor/prosedoc-construction/construct-document.ts b/editor/src/document-construction/construct-document.ts similarity index 58% rename from editor/src/kroqed-editor/prosedoc-construction/construct-document.ts rename to editor/src/document-construction/construct-document.ts index a4dfbc18..c54acd8b 100644 --- a/editor/src/kroqed-editor/prosedoc-construction/construct-document.ts +++ b/editor/src/document-construction/construct-document.ts @@ -1,15 +1,13 @@ -import { FileFormat } from "../../../../shared"; -import { extractCoqBlocks, extractHintBlocks, extractInputBlocks, extractBlocksUsingRanges, extractMathDisplayBlocks } from "./block-extraction"; -import { Block } from "./blocks"; -import { CoqBlock, HintBlock, InputAreaBlock, MarkdownBlock } from "./blocks/blocktypes"; -import { root } from "./blocks/schema"; -import { isCoqBlock } from "./blocks/typeguards"; -import { extractInterBlockRanges, maskInputAndHints, sortBlocks } from "./utils"; -import { Node as ProseNode } from "prosemirror-model"; +import { FileFormat } from "../../../shared"; +import { extractHintBlocks, extractInputBlocks, extractMathDisplayBlocks, extractCoqBlocks, extractBlocksUsingRanges } from "./block-extraction"; +import { Block, CoqBlock, HintBlock, InputAreaBlock, MarkdownBlock } from "../waterproof-editor/document"; +import { isCoqBlock } from "../waterproof-editor/document/blocks/typeguards"; +import { maskInputAndHints, sortBlocks, extractInterBlockRanges } from "../waterproof-editor/document/utils"; +import { createCoqInnerBlocks } from "./inner-blocks"; // 0A. Extract the top level blocks from the input document. -export function topLevelBlocksMV(inputDocument: string): Block[] { - // There are five different 'top level' blocks, +export function blocksFromMV(inputDocument: string): Block[] { + // There are five different 'top level' blocks, // - hint // - input_area // - math_display @@ -21,7 +19,7 @@ export function topLevelBlocksMV(inputDocument: string): Block[] { const inputAreaBlocks: InputAreaBlock[] = extractInputBlocks(inputDocument); // 0A.2 Mask the hint and input area blocks in the input document. - // Done to make extraction of coq and math display easier, since + // Done to make extraction of coq and math display easier, since // we don't have to worry about the hint and input area blocks. inputDocument = maskInputAndHints(inputDocument, [...hintBlocks, ...inputAreaBlocks]); @@ -35,7 +33,7 @@ export function topLevelBlocksMV(inputDocument: string): Block[] { // 0A.5 Extract the markdown blocks based on the ranges. const markdownBlocks = extractBlocksUsingRanges(inputDocument, markdownRanges, MarkdownBlock); - + // Note: Blocks parse their own inner blocks. // 0A.6 Prune empty blocks. @@ -46,14 +44,14 @@ export function topLevelBlocksMV(inputDocument: string): Block[] { } // 0B. Extract the top level blocks from the input document. -export function topLevelBlocksV(inputDocument: string): Block[] { - // There are three different 'top level' blocks, +export function blocksFromV(inputDocument: string): Block[] { + // There are three different 'top level' blocks, // - Hint // - InputArea // - Coq // We should also allow input and hints as we do with v files. - // The syntax is + // The syntax is // (* begin hint : [hint description] *) and (* end hint *) // (* begin input *) and (* end input *) @@ -61,11 +59,11 @@ export function topLevelBlocksV(inputDocument: string): Block[] { const inputAreaBlocks = extractInputBlocks(inputDocument, FileFormat.RegularV); const blocks = [...hintBlocks, ...inputAreaBlocks]; const coqBlockRanges = extractInterBlockRanges(blocks, inputDocument); - + // Extract the coq blocks based on the ranges. const coqBlocks = coqBlockRanges.map(range => { const content = inputDocument.slice(range.from, range.to); - return new CoqBlock(content, "", "", "", "", range); + return new CoqBlock(content, "", "", "", "", range, createCoqInnerBlocks); }); const sortedBlocks = sortBlocks([...hintBlocks, ...inputAreaBlocks, ...coqBlocks]); @@ -76,31 +74,3 @@ export function topLevelBlocksV(inputDocument: string): Block[] { }); return prunedBlocks; } - - - -// 1. Construct the prosemirror document from the top level blocks. -export function constructDocument(blocks: Block[]): ProseNode { - const documentContent: ProseNode[] = blocks.map(block => block.toProseMirror()); - return root(documentContent); -} - -// 2. Construct the prosemirror document from the input document. -export function createProseMirrorDocument(input: string, fileFormat: FileFormat): ProseNode { - let blocks: Block[]; - - // 2.1 We differentiate between the two supported file formats - switch (fileFormat) { - case FileFormat.MarkdownV: - blocks = topLevelBlocksMV(input); - break; - case FileFormat.RegularV: - blocks = topLevelBlocksV(input); - break; - case FileFormat.Unknown: - throw new Error("Unknown file format in prosemirror document constructor"); - } - - // 2.2 Construct the document and return - return constructDocument(blocks); -} \ No newline at end of file diff --git a/editor/src/kroqed-editor/prosedoc-construction/blocks/inner-blocks.ts b/editor/src/document-construction/inner-blocks.ts similarity index 84% rename from editor/src/kroqed-editor/prosedoc-construction/blocks/inner-blocks.ts rename to editor/src/document-construction/inner-blocks.ts index 9871ec03..3a8622b4 100644 --- a/editor/src/kroqed-editor/prosedoc-construction/blocks/inner-blocks.ts +++ b/editor/src/document-construction/inner-blocks.ts @@ -1,14 +1,14 @@ -import { extractMathDisplayBlocks, extractCoqBlocks, extractBlocksUsingRanges, extractCoqDoc, extractMathDisplayBlocksCoqDoc } from "../block-extraction"; -import { extractInterBlockRanges, sortBlocks } from "../utils"; -import { Block } from "./block"; -import { CoqCodeBlock, CoqMarkdownBlock, MarkdownBlock } from "./blocktypes"; +import { Block, MarkdownBlock } from "../waterproof-editor/document"; +import { CoqCodeBlock, CoqMarkdownBlock } from "../waterproof-editor/document/blocks/blocktypes"; +import { extractInterBlockRanges, sortBlocks } from "../waterproof-editor/document/utils"; +import { extractMathDisplayBlocks, extractCoqBlocks, extractBlocksUsingRanges, extractCoqDoc, extractMathDisplayBlocksCoqDoc } from "./block-extraction"; export function createInputAndHintInnerBlocks(input: string): Block[] { // Since input areas and hints can both contain: // - coq // - math_display - // - markdown + // - markdown // This amounts to the same as steps 0.3 - 0.5 in topLevelBlocks. const mathDisplayBlocks = extractMathDisplayBlocks(input); const coqBlocks = extractCoqBlocks(input); @@ -24,7 +24,7 @@ export function createCoqInnerBlocks(input: string): Block[] { // - Coq code // - Coqdoc comments - // Extract all the coqdoc comments: + // Extract all the coqdoc comments: const coqdocBlocks = extractCoqDoc(input); // Everything in between is coq code (including regular coq comments). @@ -46,7 +46,7 @@ export function createCoqDocInnerBlocks(input: string): Block[] { // - Coq Markdown // - Math display (with single dollar signs) - // Extract all the math display blocks: + // Extract all the math display blocks: const mathDisplayBlocks = extractMathDisplayBlocksCoqDoc(input); // Everything in between is coq markdown. diff --git a/editor/src/index.ts b/editor/src/index.ts index 513f6114..ef389632 100644 --- a/editor/src/index.ts +++ b/editor/src/index.ts @@ -1,9 +1,11 @@ import { Completion } from "@codemirror/autocomplete"; -import { Message, MessageType } from "../../shared"; -import { Editor } from "./kroqed-editor"; -import { COQ_CODE_PLUGIN_KEY } from "./kroqed-editor/codeview/coqcodeplugin"; -import { WaterproofEditorConfig } from "./kroqed-editor/utilities/types"; +import { FileFormat, Message, MessageType } from "../../shared"; +import { WaterproofEditor, WaterproofEditorConfig } from "./waterproof-editor"; +import { CODE_PLUGIN_KEY } from "./waterproof-editor/codeview"; +// TODO: Move this to a types location. +import { TextDocMappingMV, TextDocMappingV } from "./mapping"; +import { blocksFromMV, blocksFromV } from "./document-construction/construct-document"; /** * Very basic representation of the acquirable VSCodeApi. @@ -13,25 +15,15 @@ interface VSCodeAPI { postMessage: (message: Message) => void; } -window.onload = () => { - // Get HTML DOM elements - const editorElement = document.querySelector("#editor"); - - if (editorElement == null) { - throw Error("Editor element cannot be null (no element with id 'editor' found)"); - } - - const codeAPI = acquireVsCodeApi() as VSCodeAPI; - if (codeAPI == null) { - throw Error("Could not acquire the vscode api."); - // TODO: maybe sent some sort of test message? - } - +function createConfiguration(format: FileFormat, codeAPI: VSCodeAPI) { // Create the WaterproofEditorConfig object const cfg: WaterproofEditorConfig = { api: { + executeHelp() { + codeAPI.postMessage({ type: MessageType.command, body: { command: "Help.", time: (new Date()).getTime()} }); + }, executeCommand(command: string, time: number) { - codeAPI.postMessage({ type: MessageType.command, body: {command, time} }); + codeAPI.postMessage({ type: MessageType.command, body: { command, time } }); }, editorReady() { codeAPI.postMessage({ type: MessageType.editorReady }); @@ -46,12 +38,35 @@ window.onload = () => { codeAPI.postMessage({ type: MessageType.cursorChange, body: cursorPosition }); }, lineNumbers(linenumbers, version) { - codeAPI.postMessage({ type: MessageType.lineNumbers, body: {linenumbers, version} }); + codeAPI.postMessage({ type: MessageType.lineNumbers, body: { linenumbers, version } }); }, - } + }, + documentConstructor: format === FileFormat.MarkdownV ? blocksFromMV : blocksFromV, + // TODO: For now assuming we are constructing an mv file editor. + mapping: format === FileFormat.MarkdownV ? TextDocMappingMV : TextDocMappingV + } + + return cfg; +} + +window.onload = () => { + // Get HTML DOM elements + const editorElement = document.querySelector("#editor"); + + if (editorElement == null) { + throw Error("Editor element cannot be null (no element with id 'editor' found)"); } + + const codeAPI = acquireVsCodeApi() as VSCodeAPI; + if (codeAPI == null) { + throw Error("Could not acquire the vscode api."); + // TODO: maybe sent some sort of test message? + } + + // Create the editor, passing it the vscode api and the editor and content HTML elements. - const editor = new Editor(editorElement, cfg); + const cfg = createConfiguration(FileFormat.MarkdownV, codeAPI); + const editor = new WaterproofEditor(editorElement, cfg); // Register event listener for communication between extension and editor window.addEventListener("message", (event: MessageEvent) => { @@ -80,7 +95,7 @@ window.onload = () => { if (!state) break; const completions: Completion[] = msg.body; // Apply autocomplete to all coq cells - COQ_CODE_PLUGIN_KEY + CODE_PLUGIN_KEY .getState(state) ?.activeNodeViews ?.forEach(codeBlock => codeBlock.handleNewComplete(completions)); diff --git a/editor/src/kroqed-editor/codeview/codeblock-input-rule.ts b/editor/src/kroqed-editor/codeview/codeblock-input-rule.ts deleted file mode 100644 index 4b3cbd67..00000000 --- a/editor/src/kroqed-editor/codeview/codeblock-input-rule.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { InputRule } from "prosemirror-inputrules"; -import { NodeType } from "prosemirror-model"; - -// user has to type '!coq' followed by a whitespace character. -export const codeblockInputRuleRegExp: RegExp = /!coq\s+$/i; - - -// adepted from prosemirror math. -export function makeCodeBlockInputRule(nodeType: NodeType) { - return new InputRule(codeblockInputRuleRegExp, (state, match, start, end) => { - const _start = state.doc.resolve(start); - const index = _start.index(); - const _end = state.doc.resolve(end); - - // check if the replacement is valid - if (!_start.parent.canReplaceWith(index, _end.index(), nodeType)) { - return null; - } - - // perform replacement - return state.tr.replaceRangeWith( - start, - end, - nodeType.create({}, nodeType.schema.text("This is a new coq cell")) - ); - }); -} diff --git a/editor/src/kroqed-editor/codeview/codeblockspec.ts b/editor/src/kroqed-editor/codeview/codeblockspec.ts deleted file mode 100644 index 1669632c..00000000 --- a/editor/src/kroqed-editor/codeview/codeblockspec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NodeSpec } from "prosemirror-model"; - -// Custom NodeSpec for a codeblock node. -export const codeblockSpec: NodeSpec = { - group: "inline", // Considered as inline element - content: "text*",// content is of type text - inline: true, - code: true, - atom: true, // doesn't have directly editable content (content is edited through codemirror) - toDOM: () => ["codeblock", 0], // cells - parseDOM: [{tag: "codeblock"}] -}; \ No newline at end of file diff --git a/editor/src/kroqed-editor/codeview/index.ts b/editor/src/kroqed-editor/codeview/index.ts deleted file mode 100644 index fb0697d8..00000000 --- a/editor/src/kroqed-editor/codeview/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Export the CodeBlockView class. -export { CodeBlockView } from "./nodeview"; -export { codeblockSpec } from "./codeblockspec"; -export { codeblockInputRuleRegExp, makeCodeBlockInputRule } from "./codeblock-input-rule"; -// Export the coq plugin and plugin key for use in the prosemirror editor. -export { COQ_CODE_PLUGIN_KEY, coqCodePlugin } from "./coqcodeplugin"; \ No newline at end of file diff --git a/editor/src/kroqed-editor/index.ts b/editor/src/kroqed-editor/index.ts deleted file mode 100644 index 5a66c392..00000000 --- a/editor/src/kroqed-editor/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Export the Editor class -export { WaterproofEditor as Editor } from "./editor"; \ No newline at end of file diff --git a/editor/src/kroqed-editor/mappingModel/index.ts b/editor/src/kroqed-editor/mappingModel/index.ts deleted file mode 100644 index c806dc29..00000000 --- a/editor/src/kroqed-editor/mappingModel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TextDocMapping } from "./mapping"; \ No newline at end of file diff --git a/editor/src/kroqed-editor/mappingModel/mapping.ts b/editor/src/kroqed-editor/mappingModel/mapping.ts deleted file mode 100644 index dedb2fd6..00000000 --- a/editor/src/kroqed-editor/mappingModel/mapping.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Step } from "prosemirror-transform"; -import { DocChange, FileFormat, WrappingDocChange } from "../../../../shared"; -import { TextDocMappingMV } from "./mvFile"; -import { TextDocMappingV } from "./vFile"; - -export class TextDocMapping { - private _theMapping: TextDocMappingMV | TextDocMappingV; - - constructor (fileFormat: FileFormat, inputString: string, version: number) { - // if (fileFormat === FileFormat.RegularV) { - // this._theMapping = new TextDocMappingV(inputString, version); - // } else if (fileFormat === FileFormat.MarkdownV) { - this._theMapping = new TextDocMappingMV(inputString, version); - // } else { - // throw new Error("Unsupported file format passed to TextDocMapping."); - // } - } - - public getMapping() { - return this._theMapping.getMapping(); - } - - public get version() { - return this._theMapping.version; - } - - public findPosition(index: number) { - return this._theMapping.findPosition(index) - } - - public findInvPosition(index: number) { - return this._theMapping.findInvPosition(index); - } - - public stepUpdate(step: Step): DocChange | WrappingDocChange { - return this._theMapping.stepUpdate(step); - } -} \ No newline at end of file diff --git a/editor/src/kroqed-editor/prosedoc-construction/blocks/index.ts b/editor/src/kroqed-editor/prosedoc-construction/blocks/index.ts deleted file mode 100644 index bdbf933e..00000000 --- a/editor/src/kroqed-editor/prosedoc-construction/blocks/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { BlockRange, Block } from "./block"; - -export { InputAreaBlock, HintBlock, CoqBlock, MathDisplayBlock } from "./blocktypes"; \ No newline at end of file diff --git a/editor/src/kroqed-editor/prosedoc-construction/index.ts b/editor/src/kroqed-editor/prosedoc-construction/index.ts deleted file mode 100644 index 69e5f469..00000000 --- a/editor/src/kroqed-editor/prosedoc-construction/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { topLevelBlocksMV, topLevelBlocksV, constructDocument, createProseMirrorDocument } from "./construct-document"; \ No newline at end of file diff --git a/editor/src/kroqed-editor/utilities/types.ts b/editor/src/kroqed-editor/utilities/types.ts deleted file mode 100644 index 4e1d12a7..00000000 --- a/editor/src/kroqed-editor/utilities/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { DocChange, WrappingDocChange } from "../../../../shared"; - - -export type Positioned = { - obj: A; - pos: number | undefined; -}; - -export type WaterproofEditorConfig = { - api: { - executeCommand: (command: string, time: number) => void, - editorReady: () => void, - documentChange: (change: DocChange | WrappingDocChange) => void, - applyStepError: (errorMessage: string) => void, - cursorChange: (cursorPosition: number) => void - lineNumbers: (linenumbers: Array, version: number) => void, - } -} \ No newline at end of file diff --git a/editor/src/mapping/index.ts b/editor/src/mapping/index.ts new file mode 100644 index 00000000..9c7c1ef0 --- /dev/null +++ b/editor/src/mapping/index.ts @@ -0,0 +1,2 @@ +export { TextDocMappingMV } from "./mvFile"; +export { TextDocMappingV } from "./vFile"; \ No newline at end of file diff --git a/editor/src/kroqed-editor/mappingModel/mvFile/helper-functions.ts b/editor/src/mapping/mvFile/helper-functions.ts similarity index 100% rename from editor/src/kroqed-editor/mappingModel/mvFile/helper-functions.ts rename to editor/src/mapping/mvFile/helper-functions.ts diff --git a/editor/src/kroqed-editor/mappingModel/mvFile/index.ts b/editor/src/mapping/mvFile/index.ts similarity index 100% rename from editor/src/kroqed-editor/mappingModel/mvFile/index.ts rename to editor/src/mapping/mvFile/index.ts diff --git a/editor/src/kroqed-editor/mappingModel/mvFile/nodeUpdate.ts b/editor/src/mapping/mvFile/nodeUpdate.ts similarity index 97% rename from editor/src/kroqed-editor/mappingModel/mvFile/nodeUpdate.ts rename to editor/src/mapping/mvFile/nodeUpdate.ts index b6de86f0..b07e666b 100644 --- a/editor/src/kroqed-editor/mappingModel/mvFile/nodeUpdate.ts +++ b/editor/src/mapping/mvFile/nodeUpdate.ts @@ -1,7 +1,7 @@ // Disabled because the ts-ignores later can't be made into ts-expect-error /* eslint-disable @typescript-eslint/ban-ts-comment */ import { ReplaceAroundStep, ReplaceStep } from "prosemirror-transform"; -import { DocChange, WrappingDocChange } from "../../../../../shared"; +import { DocChange, WrappingDocChange } from "../../../../shared"; import { HtmlTagInfo, OperationType, ParsedStep, StringCell } from "./types"; import { getEndHtmlTagText, getStartHtmlTagText, getTextOffset, parseFragment } from "./helper-functions"; @@ -54,10 +54,10 @@ export class NodeUpdate { } else { throw new Error(" We do not support this type of ReplaceAroundStep"); } - + } - return result; + return result; } private static replaceStepDelete(step: ReplaceStep, stringBlocks: Map, endHtmlMap: Map, startHtmlMap: Map) : ParsedStep { @@ -99,7 +99,7 @@ export class NodeUpdate { // Adjust textOffset based on what has been deleted textOffset += 1 - value.textCost; continue; - } + } newkey += proseOffset; newvalue.offsetText += textOffset; newvalue.offsetProse += proseOffset; } @@ -117,7 +117,7 @@ export class NodeUpdate { newHtmlMapS.set(newkey,newvalue); } - + for(const [key,value] of stringBlocks) { let newkey = key; const newvalue = structuredClone(value); if (key >= step.from) { @@ -159,7 +159,7 @@ export class NodeUpdate { result.endInFile = 0; result.finalText = final.starttext + inBetweenText + final.endtext; } else { - // Check whether we know of the insertion location + // Check whether we know of the insertion location if (!(startHtmlMap.has(step.from) || endHtmlMap.has(step.from))) throw new Error(" The insertion spot was not recognized, either mapping is off or a bug in code"); const location = startHtmlMap.has(step.from) ? startHtmlMap.get(step.from) : endHtmlMap.get(step.from); @@ -220,7 +220,7 @@ export class NodeUpdate { endText: textIndex + final.starttext.length + inBetweenText.length, }); } - + let proseIndex = step.from; // Add new tags for (let i = 0; i < final.tags.length; i++) { @@ -247,7 +247,7 @@ export class NodeUpdate { startInFile: 0, endInFile: 0, finalText: "" - }; + }; const edit2: DocChange = { startInFile: 0, endInFile: 0, @@ -258,7 +258,7 @@ export class NodeUpdate { const result: WrappingDocChange = { firstEdit: edit1, secondEdit: edit2 - }; + }; return result; } @@ -274,7 +274,7 @@ export class NodeUpdate { result.firstEdit.startInFile = startHtmlMap.get(step.from)?.offsetText; // @ts-ignore TODO: Fix this result.firstEdit.endInFile = endHtmlMap.get(step.gapFrom)?.offsetText; - + // @ts-ignore TODO: Fix this result.secondEdit.startInFile = startHtmlMap.get(step.gapTo)?.offsetText; // @ts-ignore TODO: Fix this @@ -283,7 +283,7 @@ export class NodeUpdate { for (const [key, value] of endHtmlMap) { let newkey = key; const newvalue = structuredClone(value); if (key == step.gapFrom || key == step.to) continue; - if (key >= step.gapFrom && key >= step.to) { + if (key >= step.gapFrom && key >= step.to) { // @ts-ignore TODO: Fix this newkey -= 2; newvalue.offsetText -= (endHtmlMap.get(step.to)?.textCost + endHtmlMap.get(step.gapFrom)?.textCost); newvalue.offsetProse -= 2; @@ -314,13 +314,13 @@ export class NodeUpdate { if (key >= step.gapFrom && key >= step.to) { newvalue.startProse -= 2; newkey -= 2; newvalue.endProse -= 2; // @ts-ignore TODO: Fix this - newvalue.startText -= (endHtmlMap.get(step.to)?.textCost + endHtmlMap.get(step.gapFrom)?.textCost); + newvalue.startText -= (endHtmlMap.get(step.to)?.textCost + endHtmlMap.get(step.gapFrom)?.textCost); // @ts-ignore TODO: Fix this newvalue.endText -= (endHtmlMap.get(step.to)?.textCost + endHtmlMap.get(step.gapFrom)?.textCost); } else if (key >= step.gapFrom) { newvalue.startProse -= 1; newkey -= 1; newvalue.endProse -= 1; // @ts-ignore TODO: Fix this - newvalue.startText -= endHtmlMap.get(step.gapFrom)?.textCost; + newvalue.startText -= endHtmlMap.get(step.gapFrom)?.textCost; // @ts-ignore TODO: Fix this newvalue.endText -= endHtmlMap.get(step.gapFrom)?.textCost; } @@ -344,7 +344,7 @@ export class NodeUpdate { result.secondEdit.startInFile = endHtmlMap.get(step.to)!.offsetText; result.secondEdit.endInFile = result.secondEdit.startInFile; - + if (step.slice.content.firstChild!.type.name == 'hint') { result.firstEdit.finalText = ''; result.secondEdit.finalText = '' @@ -357,7 +357,7 @@ export class NodeUpdate { for (const [key, value] of endHtmlMap) { let newkey = key; const newvalue = structuredClone(value); - if (key - 1 >= step.from && key - 1 >= step.to) { + if (key - 1 >= step.from && key - 1 >= step.to) { newkey += 2; newvalue.offsetText += result.firstEdit.finalText.length + result.secondEdit.finalText.length; newvalue.offsetProse += 2; } else if(key - 1 >= step.from) { @@ -403,5 +403,5 @@ export class NodeUpdate { return {result, newHtmlMapS, newHtmlMap, newMap}; } -} - +} + diff --git a/editor/src/kroqed-editor/mappingModel/mvFile/textUpdate.ts b/editor/src/mapping/mvFile/textUpdate.ts similarity index 96% rename from editor/src/kroqed-editor/mappingModel/mvFile/textUpdate.ts rename to editor/src/mapping/mvFile/textUpdate.ts index 9d154007..609cc9aa 100644 --- a/editor/src/kroqed-editor/mappingModel/mvFile/textUpdate.ts +++ b/editor/src/mapping/mvFile/textUpdate.ts @@ -1,5 +1,5 @@ import { ReplaceStep } from "prosemirror-transform"; -import { DocChange } from "../../../../../shared"; +import { DocChange } from "../../../../shared"; import { HtmlTagInfo, OperationType, ParsedStep, StringCell } from "./types"; import { getTextOffset } from "./helper-functions"; @@ -37,11 +37,11 @@ export class TextUpdate { /** Check that the change is, indeed, happening within a stringcell */ if (targetCell.endProse < step.from) throw new Error(" Step does not happen within cell "); - /** The offset within the correct stringCell for the step action */ + /** The offset within the correct stringCell for the step action */ const offsetBegin = step.from - targetCell.startProse; - /** The offset within the correct stringCell for the step action */ - const offsetEnd = step.to - targetCell.startProse; + /** The offset within the correct stringCell for the step action */ + const offsetEnd = step.to - targetCell.startProse; const text = step.slice.content.firstChild && step.slice.content.firstChild.text ? step.slice.content.firstChild.text : ""; @@ -71,7 +71,7 @@ export class TextUpdate { } newMap.set(newkey,newvalue); } - + const newHtmlMap = new Map(); const newHtmlMapS = new Map(); @@ -84,7 +84,7 @@ export class TextUpdate { } newHtmlMap.set(newkey,newvalue); } - + for (const [key, value] of startHtmlMap) { let newkey = key; const newvalue = structuredClone(value); if (key >= step.to) { diff --git a/editor/src/kroqed-editor/mappingModel/mvFile/types.ts b/editor/src/mapping/mvFile/types.ts similarity index 91% rename from editor/src/kroqed-editor/mappingModel/mvFile/types.ts rename to editor/src/mapping/mvFile/types.ts index 3d115a36..bd38e7c6 100644 --- a/editor/src/kroqed-editor/mappingModel/mvFile/types.ts +++ b/editor/src/mapping/mvFile/types.ts @@ -1,9 +1,9 @@ -import { DocChange, WrappingDocChange } from "../../../../../shared"; +import { DocChange, WrappingDocChange } from "../../../../shared"; //// The types used by this module /** - * In prosemirror, every step is a replace step. This enum is used to classify + * In prosemirror, every step is a replace step. This enum is used to classify * the steps into the given 'pure' operations */ export enum OperationType { @@ -33,7 +33,7 @@ export type ParsedStep = { * Represents an area of text, that is editable in the prosemirror view and its * mapping to the vscode document */ -export type StringCell = { +export type StringCell = { /** The prosemirror starting index of this cell */ startProse: number, /** The prosemirror ending index of this cell */ @@ -54,7 +54,7 @@ export type HtmlTagInfo = { offsetText: number, /** The prosemirror index */ offsetProse: number, - /** The length of text this tag represents in vscode */ + /** The length of text this tag represents in vscode */ textCost: number, }; @@ -62,9 +62,9 @@ export type HtmlTagInfo = { -/** This stores the characters that each 'starting' HTML tag represents in the orginal document */ +/** This stores the characters that each 'starting' HTML tag represents in the orginal document */ export const textStartHTML: Map = new Map([["coqblock", "```coq"],["coqcode", ""], ["coqdoc", "(** "], ["coqdown", ""], ["math-display", "$"], ["input-area",""], ["markdown",""], ["math_display", "$"], ["input",""],["text",""]]); - + /** This stores the characters that each 'ending' HTML tag represents the orginal document */ export const textEndHTML: Map = new Map([["coqblock", "```"],["coqcode", ""], ["coqdoc", "*)"], ["coqdown", ""], ["math-display", "$"], ["input-area",""], ["markdown",""], ["hint", ""], ["math_display", "$"], ["input",""],["text",""]]); diff --git a/editor/src/kroqed-editor/mappingModel/mvFile/vscodeMapping.ts b/editor/src/mapping/mvFile/vscodeMapping.ts similarity index 94% rename from editor/src/kroqed-editor/mappingModel/mvFile/vscodeMapping.ts rename to editor/src/mapping/mvFile/vscodeMapping.ts index 6dd8f4a9..93b73082 100644 --- a/editor/src/kroqed-editor/mappingModel/mvFile/vscodeMapping.ts +++ b/editor/src/mapping/mvFile/vscodeMapping.ts @@ -1,16 +1,17 @@ import { Step, ReplaceStep, ReplaceAroundStep } from "prosemirror-transform"; -import { DocChange, WrappingDocChange } from "../../../../../shared"; +import { DocChange, WrappingDocChange } from "../../../../shared"; import { getEndHtmlTagText, getStartHtmlTagText} from "./helper-functions"; import { StringCell, HtmlTagInfo, ParsedStep} from "./types"; import { TextUpdate } from "./textUpdate"; import { NodeUpdate } from "./nodeUpdate"; +import { WaterproofMapping } from "../../waterproof-editor"; /** * A type to specify an HTML tag in the prosemirror content elem. */ interface TagInformation { /** The starting index of the tag in the input string */ - start: number; + start: number; /** The ending index of the tag in the input string */ end: number; /** The number of 'hidden' newlines the starting tag encodes in the original vscode document */ @@ -27,11 +28,11 @@ interface TagInformation { * it could possibly be simpler to do all this with the EditorState document node (textContent attribute). * However, we had thought this approach would be somewhat viable and nice for large documents, as you are not * sending the entire doc back and forth, but it comes at the cost of complexity. - * + * * This class is responsible for keeping track of the mapping between the prosemirror state and the vscode Text * Document model */ -export class TextDocMappingMV { +export class TextDocMappingMV implements WaterproofMapping { /** This stores the String cells of the entire document */ private stringBlocks: Map; /** This stores the inverted mapping of stringBlocks */ @@ -55,9 +56,9 @@ export class TextDocMappingMV { "coqdown", ]); - /** + /** * Constructs a prosemirror view vsode mapping for the inputted prosemirror html elem - * + * * @param inputString a string contatining the prosemirror content html elem */ constructor(inputString: string, versionNum: number) { @@ -113,7 +114,7 @@ export class TextDocMappingMV { /** * Initializes the mapping according to the inputed html content elem - * + * * @param inputString the string of the html elem */ private initialize(inputString: string) : void { @@ -121,19 +122,19 @@ export class TextDocMappingMV { const stack = new Array<{ tag: string, offsetPost: number}>; /** The current index we are at within the prosemirror content elem */ - let offsetProse: number = 0; + let offsetProse: number = 0; /** The current index we are at within the raw vscode text document */ let offsetText: number = 0; /** This represents whether we are currently within a coqdoc block */ - let inCoqdoc: boolean = false; + let inCoqdoc: boolean = false; // Continue until the entire string has been parsed - while(inputString.length > 0) { + while(inputString.length > 0) { const next: TagInformation = TextDocMappingMV.getNextHTMLtag(inputString); let nextCell: StringCell | undefined = undefined; - + /** The number of characters the tag `next` takes up in the raw vscode doc. */ let textCost = 0; @@ -141,14 +142,14 @@ export class TextDocMappingMV { if (stack.length && stack[stack.length - 1].tag === next.content) { const stackTag = stack.pop(); if (stackTag === undefined) throw new Error(" Stack returned undefined in initialization of vscode mapping"); - nextCell = { - startProse: offsetProse, endProse: offsetProse + next.start, + nextCell = { + startProse: offsetProse, endProse: offsetProse + next.start, startText: offsetText, endText: offsetText + next.start, }; if (next.content == "coqdoc") inCoqdoc = false; if (next.content == "math-display" && !inCoqdoc) textCost += 1; // Two dollar signs outside coqdoc cells textCost += stackTag.offsetPost; // Increment for the post newlines - textCost += getEndHtmlTagText(next.content).length; + textCost += getEndHtmlTagText(next.content).length; } else { // Otherwise `next` opens a block stack.push({ tag: next.content, offsetPost: next.postNumber}); // Push new tag to stack // Add text offset based on tag @@ -176,16 +177,16 @@ export class TextDocMappingMV { // Check if the nextCell should be pushed switch(next.content) { - case "markdown": case "coqcode": + case "markdown": case "coqcode": case "math-display": case "coqdown": // If the nextcell is set, push it to mapping if(!(nextCell === undefined)) this.stringBlocks.set(nextCell.startProse, nextCell); break; } - + // Update the input string and cut off the processed text inputString = inputString.slice(next.end); - + } this.updateInvMapping(); } @@ -203,11 +204,11 @@ export class TextDocMappingMV { } /** Called whenever a step is made in the editor */ - public stepUpdate(step: Step) : DocChange | WrappingDocChange { - if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep)) + public update(step: Step) : DocChange | WrappingDocChange { + if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep)) throw new Error("Step update (in textDocMapping) should not be called with a non document changing step"); - /** Check whether the edit is a text edit and occurs within a stringcell */ + /** Check whether the edit is a text edit and occurs within a stringcell */ const isText: boolean = (step.slice.content.firstChild === null) ? this.inStringCell(step) : step.slice.content.firstChild.type.name === "text"; let result : ParsedStep; @@ -228,7 +229,7 @@ export class TextDocMappingMV { if(this.checkDocChange(result.result)) this._version++; } else { if(this.checkDocChange(result.result.firstEdit) || this.checkDocChange(result.result.secondEdit)) this._version++; - } + } return result.result; } @@ -243,7 +244,7 @@ export class TextDocMappingMV { /** * This checks if the doc change actually changed the document, since vscode - * does not register empty changes + * does not register empty changes */ private checkDocChange(change: DocChange) : boolean { if (change.endInFile === change.startInFile && change.finalText.length == 0) return false; @@ -251,9 +252,9 @@ export class TextDocMappingMV { } /** - * Function that returns the next valid html tag in a string. + * Function that returns the next valid html tag in a string. * Throws an error if no valid html tag is found. - * @param The string that contains html tags + * @param The string that contains html tags * @returns The first valid html tag in the string and the position of this tag in the string */ public static getNextHTMLtag(input: string): TagInformation { @@ -261,7 +262,7 @@ export class TextDocMappingMV { // Find all html tags (this is necessary for the position and for invalid matches) const matches = Array.from(input.matchAll(/<(\/)?(input-area|coqblock|coqcode|markdown|math-display|hint|coqdoc|coqdown)( [^>]*)?>/g)); - // Loop through all matches + // Loop through all matches for (const match of matches) { // Check if there are no weird matches that we should throw an error on @@ -310,7 +311,7 @@ export class TextDocMappingMV { if (postPreWhiteMatch[1] === "newLine") { preNum++; } - } + } // We check for the pre whiteline in front of the closing coqblock tag const prePostWhiteMatch = Array.from(whiteSpaceMatch.matchAll(/postPreWhite="(\w*?)"/g))[0] @@ -333,8 +334,8 @@ export class TextDocMappingMV { } //return the resulting object - return {start: start, end: end, content: match[2], preNumber: preNum, postNumber: postNum} - + return {start: start, end: end, content: match[2], preNumber: preNum, postNumber: postNum} + // For coqdoc we repeat the same process } else if ((match[2] === "coqdoc") && match[1] == undefined){ @@ -359,16 +360,16 @@ export class TextDocMappingMV { if (postWhiteMatch[1] === "newLine") { postNum++; } - } + } // Return the result - return {start: start, end: end, content: match[2], preNumber: preNum, postNumber: postNum} + return {start: start, end: end, content: match[2], preNumber: preNum, postNumber: postNum} } else { - + return {start: start, end: end, content: match[2], preNumber: 0, postNumber: 0} } - - } + + } } else { // WE have incountered an invalid match @@ -378,7 +379,7 @@ export class TextDocMappingMV { // We have found no valid HTML tags which means the string on input was invalid. throw new Error(" No tag found "); - + } diff --git a/editor/src/kroqed-editor/mappingModel/vFile/helper-functions.ts b/editor/src/mapping/vFile/helper-functions.ts similarity index 100% rename from editor/src/kroqed-editor/mappingModel/vFile/helper-functions.ts rename to editor/src/mapping/vFile/helper-functions.ts diff --git a/editor/src/kroqed-editor/mappingModel/vFile/index.ts b/editor/src/mapping/vFile/index.ts similarity index 100% rename from editor/src/kroqed-editor/mappingModel/vFile/index.ts rename to editor/src/mapping/vFile/index.ts diff --git a/editor/src/kroqed-editor/mappingModel/vFile/nodeUpdate.ts b/editor/src/mapping/vFile/nodeUpdate.ts similarity index 97% rename from editor/src/kroqed-editor/mappingModel/vFile/nodeUpdate.ts rename to editor/src/mapping/vFile/nodeUpdate.ts index 44508dce..0b406cc1 100644 --- a/editor/src/kroqed-editor/mappingModel/vFile/nodeUpdate.ts +++ b/editor/src/mapping/vFile/nodeUpdate.ts @@ -1,5 +1,5 @@ import { ReplaceAroundStep, ReplaceStep } from "prosemirror-transform"; -import { DocChange, WrappingDocChange } from "../../../../../shared"; +import { DocChange, WrappingDocChange } from "../../../../shared"; import { HtmlTagInfo, OperationType, ParsedStep, StringCell } from "./types"; import { getEndHtmlTagText, getStartHtmlTagText, getTextOffset, parseFragment } from "./helper-functions"; @@ -52,10 +52,10 @@ export class NodeUpdate { } else { throw new Error(" We do not support this type of ReplaceAroundStep"); } - + } - return result; + return result; } private static replaceStepDelete(step: ReplaceStep, stringBlocks: Map, endHtmlMap: Map, startHtmlMap: Map) : ParsedStep { @@ -97,7 +97,7 @@ export class NodeUpdate { // Adjust textOffset based on what has been deleted textOffset += 1 - value.textCost; continue; - } + } newkey += proseOffset; newvalue.offsetText += textOffset; newvalue.offsetProse += proseOffset; } @@ -115,7 +115,7 @@ export class NodeUpdate { newHtmlMapS.set(newkey,newvalue); } - + for(const [key,value] of stringBlocks) { let newkey = key; const newvalue = structuredClone(value); if (key >= step.from) { @@ -157,7 +157,7 @@ export class NodeUpdate { result.endInFile = 0; result.finalText = final.starttext + inBetweenText + final.endtext; } else { - // Check whether we know of the insertion location + // Check whether we know of the insertion location if (!(startHtmlMap.has(step.from) || endHtmlMap.has(step.from))) throw new Error(" The insertion spot was not recognized, either mapping is off or a bug in code"); const location = startHtmlMap.has(step.from) ? startHtmlMap.get(step.from) : endHtmlMap.get(step.from); @@ -218,7 +218,7 @@ export class NodeUpdate { endText: textIndex + final.starttext.length + inBetweenText.length, }); } - + let proseIndex = step.from; // Add new tags for (let i = 0; i < final.tags.length; i++) { @@ -245,7 +245,7 @@ export class NodeUpdate { startInFile: 0, endInFile: 0, finalText: "" - }; + }; const edit2: DocChange = { startInFile: 0, endInFile: 0, @@ -256,7 +256,7 @@ export class NodeUpdate { const result: WrappingDocChange = { firstEdit: edit1, secondEdit: edit2 - }; + }; return result; } @@ -272,7 +272,7 @@ export class NodeUpdate { result.firstEdit.startInFile = startHtmlMap.get(step.from)?.offsetText; // @ts-expect-error TODO: Fix this result.firstEdit.endInFile = endHtmlMap.get(step.gapFrom)?.offsetText; - + // @ts-expect-error TODO: Fix this result.secondEdit.startInFile = startHtmlMap.get(step.gapTo)?.offsetText; // @ts-expect-error TODO: Fix this @@ -281,7 +281,7 @@ export class NodeUpdate { for (const [key, value] of endHtmlMap) { let newkey = key; const newvalue = structuredClone(value); if (key == step.gapFrom || key == step.to) continue; - if (key >= step.gapFrom && key >= step.to) { + if (key >= step.gapFrom && key >= step.to) { // @ts-expect-error TODO: Fix this newkey -= 2; newvalue.offsetText -= (endHtmlMap.get(step.to)?.textCost + endHtmlMap.get(step.gapFrom)?.textCost); newvalue.offsetProse -= 2; @@ -312,13 +312,13 @@ export class NodeUpdate { if (key >= step.gapFrom && key >= step.to) { newvalue.startProse -= 2; newkey -= 2; newvalue.endProse -= 2; // @ts-expect-error TODO: Fix this - newvalue.startText -= (endHtmlMap.get(step.to)?.textCost + endHtmlMap.get(step.gapFrom)?.textCost); + newvalue.startText -= (endHtmlMap.get(step.to)?.textCost + endHtmlMap.get(step.gapFrom)?.textCost); // @ts-expect-error TODO: Fix this newvalue.endText -= (endHtmlMap.get(step.to)?.textCost + endHtmlMap.get(step.gapFrom)?.textCost); } else if (key >= step.gapFrom) { newvalue.startProse -= 1; newkey -= 1; newvalue.endProse -= 1; // @ts-expect-error TODO: Fix this - newvalue.startText -= endHtmlMap.get(step.gapFrom)?.textCost; + newvalue.startText -= endHtmlMap.get(step.gapFrom)?.textCost; // @ts-expect-error TODO: Fix this newvalue.endText -= endHtmlMap.get(step.gapFrom)?.textCost; } @@ -342,7 +342,7 @@ export class NodeUpdate { result.secondEdit.startInFile = endHtmlMap.get(step.to)!.offsetText; result.secondEdit.endInFile = result.secondEdit.startInFile; - + if (step.slice.content.firstChild!.type.name == 'hint') { result.firstEdit.finalText = ''; result.secondEdit.finalText = '' @@ -355,7 +355,7 @@ export class NodeUpdate { for (const [key, value] of endHtmlMap) { let newkey = key; const newvalue = structuredClone(value); - if (key - 1 >= step.from && key - 1 >= step.to) { + if (key - 1 >= step.from && key - 1 >= step.to) { newkey += 2; newvalue.offsetText += result.firstEdit.finalText.length + result.secondEdit.finalText.length; newvalue.offsetProse += 2; } else if(key - 1 >= step.from) { @@ -401,5 +401,5 @@ export class NodeUpdate { return {result, newHtmlMapS, newHtmlMap, newMap}; } -} - +} + diff --git a/editor/src/kroqed-editor/mappingModel/vFile/textUpdate.ts b/editor/src/mapping/vFile/textUpdate.ts similarity index 96% rename from editor/src/kroqed-editor/mappingModel/vFile/textUpdate.ts rename to editor/src/mapping/vFile/textUpdate.ts index 39d12a46..7eddc4de 100644 --- a/editor/src/kroqed-editor/mappingModel/vFile/textUpdate.ts +++ b/editor/src/mapping/vFile/textUpdate.ts @@ -1,5 +1,5 @@ import { ReplaceStep } from "prosemirror-transform"; -import { DocChange } from "../../../../../shared"; +import { DocChange } from "../../../../shared"; import { HtmlTagInfo, OperationType, ParsedStep, StringCell } from "./types"; import { getTextOffset } from "./helper-functions"; @@ -37,11 +37,11 @@ export class TextUpdate { /** Check that the change is, indeed, happening within a stringcell */ if (targetCell.endProse < step.from) throw new Error(" Step does not happen within cell "); - /** The offset within the correct stringCell for the step action */ + /** The offset within the correct stringCell for the step action */ const offsetBegin = step.from - targetCell.startProse; - /** The offset within the correct stringCell for the step action */ - const offsetEnd = step.to - targetCell.startProse; + /** The offset within the correct stringCell for the step action */ + const offsetEnd = step.to - targetCell.startProse; const text = step.slice.content.firstChild && step.slice.content.firstChild.text ? step.slice.content.firstChild.text : ""; @@ -71,7 +71,7 @@ export class TextUpdate { } newMap.set(newkey,newvalue); } - + const newHtmlMap = new Map(); const newHtmlMapS = new Map(); @@ -84,7 +84,7 @@ export class TextUpdate { } newHtmlMap.set(newkey,newvalue); } - + for (const [key, value] of startHtmlMap) { let newkey = key; const newvalue = structuredClone(value); if (key >= step.to) { diff --git a/editor/src/kroqed-editor/mappingModel/vFile/types.ts b/editor/src/mapping/vFile/types.ts similarity index 90% rename from editor/src/kroqed-editor/mappingModel/vFile/types.ts rename to editor/src/mapping/vFile/types.ts index e1800747..5f353f31 100644 --- a/editor/src/kroqed-editor/mappingModel/vFile/types.ts +++ b/editor/src/mapping/vFile/types.ts @@ -1,9 +1,9 @@ -import { DocChange, WrappingDocChange } from "../../../../../shared"; +import { DocChange, WrappingDocChange } from "../../../../shared"; //// The types used by this module /** - * In prosemirror, every step is a replace step. This enum is used to classify + * In prosemirror, every step is a replace step. This enum is used to classify * the steps into the given 'pure' operations */ export enum OperationType { @@ -33,7 +33,7 @@ export type ParsedStep = { * Represents an area of text, that is editable in the prosemirror view and its * mapping to the vscode document */ -export type StringCell = { +export type StringCell = { /** The prosemirror starting index of this cell */ startProse: number, /** The prosemirror ending index of this cell */ @@ -54,7 +54,7 @@ export type HtmlTagInfo = { offsetText: number, /** The prosemirror index */ offsetProse: number, - /** The length of text this tag represents in vscode */ + /** The length of text this tag represents in vscode */ textCost: number, }; @@ -62,9 +62,9 @@ export type HtmlTagInfo = { -/** This stores the characters that each 'starting' HTML tag represents in the orginal document */ +/** This stores the characters that each 'starting' HTML tag represents in the orginal document */ export const textStartHTML: Map = new Map([["coqcode", ""], ["coqdoc", "(** "], ["coqdown", ""], ["math-display", "$"], ["input-area","(* input *)"],["text",""]]); - + /** This stores the characters that each 'ending' HTML tag represents the orginal document */ export const textEndHTML: Map = new Map([["coqcode", ""], ["coqdoc", "*)"], ["coqdown", ""], ["math-display", "$"], ["input-area","(* /input-area *)"], ["hint", "(* /hint *)"], ["text",""]]); diff --git a/editor/src/kroqed-editor/mappingModel/vFile/vscodeMapping.ts b/editor/src/mapping/vFile/vscodeMapping.ts similarity index 94% rename from editor/src/kroqed-editor/mappingModel/vFile/vscodeMapping.ts rename to editor/src/mapping/vFile/vscodeMapping.ts index cc2da7da..1ecab55f 100644 --- a/editor/src/kroqed-editor/mappingModel/vFile/vscodeMapping.ts +++ b/editor/src/mapping/vFile/vscodeMapping.ts @@ -1,9 +1,10 @@ import { Step, ReplaceStep, ReplaceAroundStep } from "prosemirror-transform"; -import { DocChange, WrappingDocChange } from "../../../../../shared"; +import { DocChange, WrappingDocChange } from "../../../../shared"; import { getEndHtmlTagText, getStartHtmlTagText} from "./helper-functions"; import { StringCell, HtmlTagInfo, ParsedStep} from "./types"; import { TextUpdate } from "./textUpdate"; import { NodeUpdate } from "./nodeUpdate"; +import { WaterproofMapping } from "../../waterproof-editor"; /** @@ -11,7 +12,7 @@ import { NodeUpdate } from "./nodeUpdate"; */ interface TagInformation { /** The starting index of the tag in the input string */ - start: number; + start: number; /** The ending index of the tag in the input string */ end: number; /** The number of 'hidden' newlines the starting tag encodes in the original vscode document */ @@ -28,11 +29,11 @@ interface TagInformation { * it could possibly be simpler to do all this with the EditorState document node (textContent attribute). * However, we had thought this approach would be somewhat viable and nice for large documents, as you are not * sending the entire doc back and forth, but it comes at the cost of complexity. - * + * * This class is responsible for keeping track of the mapping between the prosemirror state and the vscode Text * Document model */ -export class TextDocMappingV { +export class TextDocMappingV implements WaterproofMapping { /** This stores the String cells of the entire document */ private stringBlocks: Map; /** This stores the inverted mapping of stringBlocks */ @@ -56,9 +57,9 @@ export class TextDocMappingV { "coqdown", ]); - /** + /** * Constructs a prosemirror view vsode mapping for the inputted prosemirror html elem - * + * * @param inputString a string contatining the prosemirror content html elem */ constructor(inputString: string, versionNum: number) { @@ -114,7 +115,7 @@ export class TextDocMappingV { /** * Initializes the mapping according to the inputed html content elem - * + * * @param inputString the string of the html elem */ private initialize(inputString: string) : void { @@ -122,16 +123,16 @@ export class TextDocMappingV { const stack = new Array<{ tag: string, offsetPost: number}>; /** The current index we are at within the prosemirror content elem */ - let offsetProse: number = 0; + let offsetProse: number = 0; /** The current index we are at within the raw vscode text document */ let offsetText: number = 0; // Continue until the entire string has been parsed - while(inputString.length > 0) { + while(inputString.length > 0) { const next: TagInformation = TextDocMappingV.getNextHTMLtag(inputString); let nextCell: StringCell | undefined = undefined; - + /** The number of characters the tag `next` takes up in the raw vscode doc. */ let textCost = 0; @@ -139,12 +140,12 @@ export class TextDocMappingV { if (stack.length && stack[stack.length - 1].tag === next.content) { const stackTag = stack.pop(); if (stackTag === undefined) throw new Error(" Stack returned undefined in initialization of vscode mapping"); - nextCell = { - startProse: offsetProse, endProse: offsetProse + next.start, + nextCell = { + startProse: offsetProse, endProse: offsetProse + next.start, startText: offsetText, endText: offsetText + next.start, }; textCost += stackTag.offsetPost; // Increment for the post newlines - textCost += getEndHtmlTagText(next.content).length; + textCost += getEndHtmlTagText(next.content).length; } else { // Otherwise `next` opens a block stack.push({ tag: next.content, offsetPost: next.postNumber}); // Push new tag to stack // Add text offset based on tag @@ -170,16 +171,16 @@ export class TextDocMappingV { // Check if the nextCell should be pushed switch(next.content) { - case "coqcode": + case "coqcode": case "math-display": case "coqdown": // If the nextcell is set, push it to mapping if(!(nextCell === undefined)) this.stringBlocks.set(nextCell.startProse, nextCell); break; } - + // Update the input string and cut off the processed text inputString = inputString.slice(next.end); - + } this.updateInvMapping(); } @@ -197,11 +198,11 @@ export class TextDocMappingV { } /** Called whenever a step is made in the editor */ - public stepUpdate(step: Step) : DocChange | WrappingDocChange { - if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep)) + public update(step: Step) : DocChange | WrappingDocChange { + if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep)) throw new Error("Step update (in textDocMapping) should not be called with a non document changing step"); - /** Check whether the edit is a text edit and occurs within a stringcell */ + /** Check whether the edit is a text edit and occurs within a stringcell */ const isText: boolean = (step.slice.content.firstChild === null) ? this.inStringCell(step) : step.slice.content.firstChild.type.name === "text"; let result : ParsedStep; @@ -222,7 +223,7 @@ export class TextDocMappingV { if(this.checkDocChange(result.result)) this._version++; } else { if(this.checkDocChange(result.result.firstEdit) || this.checkDocChange(result.result.secondEdit)) this._version++; - } + } return result.result; } @@ -237,7 +238,7 @@ export class TextDocMappingV { /** * This checks if the doc change actually changed the document, since vscode - * does not register empty changes + * does not register empty changes */ private checkDocChange(change: DocChange) : boolean { if (change.endInFile === change.startInFile && change.finalText.length == 0) return false; @@ -245,17 +246,17 @@ export class TextDocMappingV { } /** - * Function that returns the next valid html tag in a string. + * Function that returns the next valid html tag in a string. * Throws an error if no valid html tag is found. - * @param The string that contains html tags + * @param The string that contains html tags * @returns The first valid html tag in the string and the position of this tag in the string */ - public static getNextHTMLtag(input: string): TagInformation { + public static getNextHTMLtag(input: string): TagInformation { // Find all html tags (this is necessary for the position and for invalid matches) const matches = Array.from(input.matchAll(/<(\/)?([\w-]+)( [^]*?)?>/g)); - // Loop through all matches + // Loop through all matches for (const match of matches) { // Check if there are no weird matches that we should throw an error on @@ -304,7 +305,7 @@ export class TextDocMappingV { if (postPreWhiteMatch[1] === "newLine") { preNum++; } - } + } // We check for the pre whiteline in front of the closing coqblock tag const prePostWhiteMatch = Array.from(whiteSpaceMatch.matchAll(/postPreWhite="(\w*?)"/g))[0] @@ -327,8 +328,8 @@ export class TextDocMappingV { } //return the resulting object - return {start: start, end: end, content: match[2], preNumber: preNum, postNumber: postNum} - + return {start: start, end: end, content: match[2], preNumber: preNum, postNumber: postNum} + // For coqdoc we repeat the same process } else if ((match[2] === "coqdoc") && match[1] == undefined){ @@ -353,16 +354,16 @@ export class TextDocMappingV { if (postWhiteMatch[1] === "newLine") { postNum++; } - } + } // Return the result - return {start: start, end: end, content: match[2], preNumber: preNum, postNumber: postNum} + return {start: start, end: end, content: match[2], preNumber: preNum, postNumber: postNum} } else { - + return {start: start, end: end, content: match[2], preNumber: 0, postNumber: 0} } - - } + + } } else { // WE have incountered an invalid match @@ -372,7 +373,7 @@ export class TextDocMappingV { // We have found no valid HTML tags which means the string on input was invalid. throw new Error(" No tag found "); - + } diff --git a/editor/src/kroqed-editor/autocomplete/coqTerms.json b/editor/src/waterproof-editor/autocomplete/coqTerms.json similarity index 100% rename from editor/src/kroqed-editor/autocomplete/coqTerms.json rename to editor/src/waterproof-editor/autocomplete/coqTerms.json diff --git a/editor/src/kroqed-editor/autocomplete/coqTerms.ts b/editor/src/waterproof-editor/autocomplete/coqTerms.ts similarity index 100% rename from editor/src/kroqed-editor/autocomplete/coqTerms.ts rename to editor/src/waterproof-editor/autocomplete/coqTerms.ts diff --git a/editor/src/kroqed-editor/autocomplete/emojis.json b/editor/src/waterproof-editor/autocomplete/emojis.json similarity index 100% rename from editor/src/kroqed-editor/autocomplete/emojis.json rename to editor/src/waterproof-editor/autocomplete/emojis.json diff --git a/editor/src/kroqed-editor/autocomplete/emojis.ts b/editor/src/waterproof-editor/autocomplete/emojis.ts similarity index 100% rename from editor/src/kroqed-editor/autocomplete/emojis.ts rename to editor/src/waterproof-editor/autocomplete/emojis.ts diff --git a/editor/src/kroqed-editor/autocomplete/index.ts b/editor/src/waterproof-editor/autocomplete/index.ts similarity index 100% rename from editor/src/kroqed-editor/autocomplete/index.ts rename to editor/src/waterproof-editor/autocomplete/index.ts diff --git a/editor/src/kroqed-editor/autocomplete/renderSymbol.ts b/editor/src/waterproof-editor/autocomplete/renderSymbol.ts similarity index 100% rename from editor/src/kroqed-editor/autocomplete/renderSymbol.ts rename to editor/src/waterproof-editor/autocomplete/renderSymbol.ts diff --git a/editor/src/kroqed-editor/autocomplete/symbols.ts b/editor/src/waterproof-editor/autocomplete/symbols.ts similarity index 100% rename from editor/src/kroqed-editor/autocomplete/symbols.ts rename to editor/src/waterproof-editor/autocomplete/symbols.ts diff --git a/editor/src/kroqed-editor/autocomplete/tactics.ts b/editor/src/waterproof-editor/autocomplete/tactics.ts similarity index 100% rename from editor/src/kroqed-editor/autocomplete/tactics.ts rename to editor/src/waterproof-editor/autocomplete/tactics.ts diff --git a/editor/src/kroqed-editor/codeview/coqcodeplugin.ts b/editor/src/waterproof-editor/codeview/code-plugin.ts similarity index 87% rename from editor/src/kroqed-editor/codeview/coqcodeplugin.ts rename to editor/src/waterproof-editor/codeview/code-plugin.ts index ec57be4f..8331ebbc 100644 --- a/editor/src/kroqed-editor/codeview/coqcodeplugin.ts +++ b/editor/src/waterproof-editor/codeview/code-plugin.ts @@ -12,7 +12,7 @@ import { LineNumber } from "../../../../shared"; //////////////////////////////////////////////////////////// -export interface ICoqCodePluginState { +export interface ICodePluginState { macros: { [cmd:string] : string }; /** A list of currently active `NodeView`s, in insertion order. */ activeNodeViews: Set; // I suspect this will break; @@ -24,7 +24,7 @@ export interface ICoqCodePluginState { lines: LineNumber; } -export const COQ_CODE_PLUGIN_KEY = new PluginKey("prosemirror-coq-code"); +export const CODE_PLUGIN_KEY = new PluginKey("waterproof-editor-code-plugin"); /** * Returns a function suitable for passing as a field in `EditorProps.nodeViews`. @@ -36,7 +36,7 @@ export function createCoqCodeView(){ * Docs says that for any function proprs, the current plugin instance * will be bound to `this`. However, the typings don't reflect this. */ - const pluginState = COQ_CODE_PLUGIN_KEY.getState(view.state); + const pluginState = CODE_PLUGIN_KEY.getState(view.state); if(!pluginState){ throw new Error("no codemirror code plugin!"); } const nodeViews = pluginState.activeNodeViews; @@ -49,8 +49,8 @@ export function createCoqCodeView(){ } -const CoqCodePluginSpec:PluginSpec = { - key: COQ_CODE_PLUGIN_KEY, +const CoqCodePluginSpec:PluginSpec = { + key: CODE_PLUGIN_KEY, state: { init(config, instance){ return { @@ -70,7 +70,7 @@ const CoqCodePluginSpec:PluginSpec = { for (const step of tr.steps) { if (step instanceof ReplaceStep && step.slice.content.firstChild === null) { for (const view of value.activeNodeViews) { - // @ts-expect-error TODO: Fix me + // @ts-expect-error FIXME: Handle possible undefined view._getPos() if (view._getPos() === undefined || (view._getPos() >= step.from && view._getPos() < step.to)) value.activeNodeViews.delete(view); } } @@ -78,7 +78,7 @@ const CoqCodePluginSpec:PluginSpec = { } // Update the state - const meta = tr.getMeta(COQ_CODE_PLUGIN_KEY); + const meta = tr.getMeta(CODE_PLUGIN_KEY); if (meta) { if (meta.setting === "update") lineState = meta.show; @@ -110,6 +110,6 @@ const CoqCodePluginSpec:PluginSpec = { } }; -export const coqCodePlugin = new ProsePlugin(CoqCodePluginSpec); +export const codePlugin = new ProsePlugin(CoqCodePluginSpec); diff --git a/editor/src/kroqed-editor/codeview/color-scheme.ts b/editor/src/waterproof-editor/codeview/color-scheme.ts similarity index 100% rename from editor/src/kroqed-editor/codeview/color-scheme.ts rename to editor/src/waterproof-editor/codeview/color-scheme.ts diff --git a/editor/src/kroqed-editor/codeview/coqTheme.json b/editor/src/waterproof-editor/codeview/coqTheme.json similarity index 100% rename from editor/src/kroqed-editor/codeview/coqTheme.json rename to editor/src/waterproof-editor/codeview/coqTheme.json diff --git a/editor/src/kroqed-editor/codeview/debouncer.ts b/editor/src/waterproof-editor/codeview/debouncer.ts similarity index 100% rename from editor/src/kroqed-editor/codeview/debouncer.ts rename to editor/src/waterproof-editor/codeview/debouncer.ts diff --git a/editor/src/waterproof-editor/codeview/index.ts b/editor/src/waterproof-editor/codeview/index.ts new file mode 100644 index 00000000..0805b51e --- /dev/null +++ b/editor/src/waterproof-editor/codeview/index.ts @@ -0,0 +1,4 @@ +// Export the CodeBlockView class. +export { CodeBlockView } from "./nodeview"; +// Export the coq plugin and plugin key for use in the prosemirror editor. +export { CODE_PLUGIN_KEY, codePlugin } from "./code-plugin"; \ No newline at end of file diff --git a/editor/src/kroqed-editor/codeview/lang-pack/index.ts b/editor/src/waterproof-editor/codeview/lang-pack/index.ts similarity index 100% rename from editor/src/kroqed-editor/codeview/lang-pack/index.ts rename to editor/src/waterproof-editor/codeview/lang-pack/index.ts diff --git a/editor/src/kroqed-editor/codeview/lang-pack/syntax.grammar b/editor/src/waterproof-editor/codeview/lang-pack/syntax.grammar similarity index 100% rename from editor/src/kroqed-editor/codeview/lang-pack/syntax.grammar rename to editor/src/waterproof-editor/codeview/lang-pack/syntax.grammar diff --git a/editor/src/kroqed-editor/codeview/lang-pack/syntax.grammar.d.ts b/editor/src/waterproof-editor/codeview/lang-pack/syntax.grammar.d.ts similarity index 100% rename from editor/src/kroqed-editor/codeview/lang-pack/syntax.grammar.d.ts rename to editor/src/waterproof-editor/codeview/lang-pack/syntax.grammar.d.ts diff --git a/editor/src/kroqed-editor/codeview/lang-pack/syntax.terms.ts b/editor/src/waterproof-editor/codeview/lang-pack/syntax.terms.ts similarity index 100% rename from editor/src/kroqed-editor/codeview/lang-pack/syntax.terms.ts rename to editor/src/waterproof-editor/codeview/lang-pack/syntax.terms.ts diff --git a/editor/src/kroqed-editor/codeview/lang-pack/syntax.ts b/editor/src/waterproof-editor/codeview/lang-pack/syntax.ts similarity index 100% rename from editor/src/kroqed-editor/codeview/lang-pack/syntax.ts rename to editor/src/waterproof-editor/codeview/lang-pack/syntax.ts diff --git a/editor/src/kroqed-editor/codeview/nodeview.ts b/editor/src/waterproof-editor/codeview/nodeview.ts similarity index 100% rename from editor/src/kroqed-editor/codeview/nodeview.ts rename to editor/src/waterproof-editor/codeview/nodeview.ts diff --git a/editor/src/kroqed-editor/commands/command-helpers.ts b/editor/src/waterproof-editor/commands/command-helpers.ts similarity index 100% rename from editor/src/kroqed-editor/commands/command-helpers.ts rename to editor/src/waterproof-editor/commands/command-helpers.ts diff --git a/editor/src/kroqed-editor/commands/commands.ts b/editor/src/waterproof-editor/commands/commands.ts similarity index 100% rename from editor/src/kroqed-editor/commands/commands.ts rename to editor/src/waterproof-editor/commands/commands.ts diff --git a/editor/src/kroqed-editor/commands/delete-command.ts b/editor/src/waterproof-editor/commands/delete-command.ts similarity index 100% rename from editor/src/kroqed-editor/commands/delete-command.ts rename to editor/src/waterproof-editor/commands/delete-command.ts diff --git a/editor/src/kroqed-editor/commands/index.ts b/editor/src/waterproof-editor/commands/index.ts similarity index 100% rename from editor/src/kroqed-editor/commands/index.ts rename to editor/src/waterproof-editor/commands/index.ts diff --git a/editor/src/kroqed-editor/commands/insert-command.ts b/editor/src/waterproof-editor/commands/insert-command.ts similarity index 100% rename from editor/src/kroqed-editor/commands/insert-command.ts rename to editor/src/waterproof-editor/commands/insert-command.ts diff --git a/editor/src/kroqed-editor/commands/types.ts b/editor/src/waterproof-editor/commands/types.ts similarity index 100% rename from editor/src/kroqed-editor/commands/types.ts rename to editor/src/waterproof-editor/commands/types.ts diff --git a/editor/src/kroqed-editor/context-menu/index.ts b/editor/src/waterproof-editor/context-menu/index.ts similarity index 100% rename from editor/src/kroqed-editor/context-menu/index.ts rename to editor/src/waterproof-editor/context-menu/index.ts diff --git a/editor/src/kroqed-editor/context-menu/menu.ts b/editor/src/waterproof-editor/context-menu/menu.ts similarity index 97% rename from editor/src/kroqed-editor/context-menu/menu.ts rename to editor/src/waterproof-editor/context-menu/menu.ts index 36c52a68..adfe6d9a 100644 --- a/editor/src/kroqed-editor/context-menu/menu.ts +++ b/editor/src/waterproof-editor/context-menu/menu.ts @@ -17,7 +17,7 @@ export function createContextMenuHTML(editor: WaterproofEditor): HTMLDivElement // Create a 'Help' button. On execution will send a command to coq-lsp to query for help, // this result is then displayed in a popup within the editor. listContainer.appendChild(contextMenuButton("?", "Help", () => { - editor.executeCommand("Help."); + editor.executeHelp(); })); listContainer.appendChild(contextMenuButton("X", "Close", () => {})); diff --git a/editor/src/kroqed-editor/context-menu/types.ts b/editor/src/waterproof-editor/context-menu/types.ts similarity index 100% rename from editor/src/kroqed-editor/context-menu/types.ts rename to editor/src/waterproof-editor/context-menu/types.ts diff --git a/editor/src/kroqed-editor/prosedoc-construction/blocks/block.ts b/editor/src/waterproof-editor/document/blocks/block.ts similarity index 100% rename from editor/src/kroqed-editor/prosedoc-construction/blocks/block.ts rename to editor/src/waterproof-editor/document/blocks/block.ts diff --git a/editor/src/kroqed-editor/prosedoc-construction/blocks/blocktypes.ts b/editor/src/waterproof-editor/document/blocks/blocktypes.ts similarity index 87% rename from editor/src/kroqed-editor/prosedoc-construction/blocks/blocktypes.ts rename to editor/src/waterproof-editor/document/blocks/blocktypes.ts index 9a7866f3..0a133787 100644 --- a/editor/src/kroqed-editor/prosedoc-construction/blocks/blocktypes.ts +++ b/editor/src/waterproof-editor/document/blocks/blocktypes.ts @@ -1,6 +1,6 @@ import { WaterproofSchema } from "../../schema"; import { BLOCK_NAME, Block, BlockRange } from "./block"; -import { createCoqDocInnerBlocks, createCoqInnerBlocks, createInputAndHintInnerBlocks } from "./inner-blocks"; +// import { createCoqDocInnerBlocks, createCoqInnerBlocks, createInputAndHintInnerBlocks } from "./inner-blocks"; import { coqCode, coqDoc, coqMarkdown, coqblock, hint, inputArea, markdown, mathDisplay } from "./schema"; const indentation = (level: number): string => " ".repeat(level); @@ -10,8 +10,8 @@ export class InputAreaBlock implements Block { public type = BLOCK_NAME.INPUT_AREA; public innerBlocks: Block[]; - constructor( public stringContent: string, public range: BlockRange ) { - this.innerBlocks = createInputAndHintInnerBlocks(stringContent); + constructor( public stringContent: string, public range: BlockRange, innerBlockConstructor: (content: string) => Block[] ) { + this.innerBlocks = innerBlockConstructor(stringContent); }; toProseMirror() { @@ -32,8 +32,8 @@ export class HintBlock implements Block { public innerBlocks: Block[]; // Note: Hint blocks have a title attribute. - constructor( public stringContent: string, public title: string, public range: BlockRange ) { - this.innerBlocks = createInputAndHintInnerBlocks(stringContent); + constructor( public stringContent: string, public title: string, public range: BlockRange, innerBlockConstructor: (content: string) => Block[] ) { + this.innerBlocks = innerBlockConstructor(stringContent); }; toProseMirror() { @@ -68,8 +68,8 @@ export class CoqBlock implements Block { public type = BLOCK_NAME.COQ; public innerBlocks: Block[]; - constructor( public stringContent: string, public prePreWhite: string, public prePostWhite: string, public postPreWhite: string, public postPostWhite : string, public range: BlockRange ) { - this.innerBlocks = createCoqInnerBlocks(stringContent); + constructor( public stringContent: string, public prePreWhite: string, public prePostWhite: string, public postPreWhite: string, public postPostWhite : string, public range: BlockRange, innerBlockConstructor: (content: string) => Block[] ) { + this.innerBlocks = innerBlockConstructor(stringContent); }; toProseMirror() { @@ -113,8 +113,8 @@ export class CoqDocBlock implements Block { public type = BLOCK_NAME.COQ_DOC; public innerBlocks: Block[]; - constructor( public stringContent: string, public preWhite: string, public postWhite: string, public range: BlockRange ) { - this.innerBlocks = createCoqDocInnerBlocks(stringContent); + constructor( public stringContent: string, public preWhite: string, public postWhite: string, public range: BlockRange, innerBlockConstructor: (content: string) => Block[] ) { + this.innerBlocks = innerBlockConstructor(stringContent); }; toProseMirror() { diff --git a/editor/src/waterproof-editor/document/blocks/index.ts b/editor/src/waterproof-editor/document/blocks/index.ts new file mode 100644 index 00000000..3a294fdf --- /dev/null +++ b/editor/src/waterproof-editor/document/blocks/index.ts @@ -0,0 +1,3 @@ +export { BlockRange, Block } from "./block"; + +export { InputAreaBlock, HintBlock, CoqBlock, MathDisplayBlock, MarkdownBlock, CoqDocBlock } from "./blocktypes"; \ No newline at end of file diff --git a/editor/src/kroqed-editor/prosedoc-construction/blocks/schema.ts b/editor/src/waterproof-editor/document/blocks/schema.ts similarity index 100% rename from editor/src/kroqed-editor/prosedoc-construction/blocks/schema.ts rename to editor/src/waterproof-editor/document/blocks/schema.ts diff --git a/editor/src/kroqed-editor/prosedoc-construction/blocks/typeguards.ts b/editor/src/waterproof-editor/document/blocks/typeguards.ts similarity index 100% rename from editor/src/kroqed-editor/prosedoc-construction/blocks/typeguards.ts rename to editor/src/waterproof-editor/document/blocks/typeguards.ts diff --git a/editor/src/waterproof-editor/document/construct-document.ts b/editor/src/waterproof-editor/document/construct-document.ts new file mode 100644 index 00000000..93f3394f --- /dev/null +++ b/editor/src/waterproof-editor/document/construct-document.ts @@ -0,0 +1,9 @@ +import { Block } from "./blocks"; +import { root } from "./blocks/schema"; +import { Node as ProseNode } from "prosemirror-model"; + +// Construct the prosemirror document from the top level blocks. +export function constructDocument(blocks: Block[]): ProseNode { + const documentContent: ProseNode[] = blocks.map(block => block.toProseMirror()); + return root(documentContent); +} diff --git a/editor/src/waterproof-editor/document/index.ts b/editor/src/waterproof-editor/document/index.ts new file mode 100644 index 00000000..d181f161 --- /dev/null +++ b/editor/src/waterproof-editor/document/index.ts @@ -0,0 +1,2 @@ +export { constructDocument } from "./construct-document"; +export { Block, HintBlock, InputAreaBlock, CoqBlock, MathDisplayBlock, MarkdownBlock, CoqDocBlock } from "./blocks"; \ No newline at end of file diff --git a/editor/src/kroqed-editor/prosedoc-construction/utils.ts b/editor/src/waterproof-editor/document/utils.ts similarity index 79% rename from editor/src/kroqed-editor/prosedoc-construction/utils.ts rename to editor/src/waterproof-editor/document/utils.ts index 58159b7d..d4fcd859 100644 --- a/editor/src/kroqed-editor/prosedoc-construction/utils.ts +++ b/editor/src/waterproof-editor/document/utils.ts @@ -3,14 +3,14 @@ import { Block } from "./blocks"; /** * Convert a list of blocks to a prosemirror compatible node list. * @param blocks Input array of blocks. - * @returns ProseMirror nodes. + * @returns ProseMirror nodes. */ export function blocksToProseMirrorNodes(blocks: Block[]) { return blocks.map((block) => block.toProseMirror()); } /** - * Helper function to sort block type objects. Will sort based on the range object of the block. + * Helper function to sort block type objects. Will sort based on the range object of the block. * Sorts in ascending (`range.from`) order. * @param blocks Blocks to sort. * @returns Sorted array of blocks. @@ -21,7 +21,7 @@ export function sortBlocks(blocks: Block[]) { /** * Map `f` over every consecutive pair from the `input` array. - * @param input Input array. + * @param input Input array. * @param f Function to map over the pairs. * @returns The result of mapping `f` over every consecutive pair. Will return an empty array if the input array has length < 2. */ @@ -51,10 +51,17 @@ export const extractInterBlockRanges = (blocks: Array, inputDocument: str return ranges; } -export function maskInputAndHints(inputDocument: string, blocks: Block[]): string { +/** + * Utility function to mask regions of a document covered by blocks. + * @param inputDocument The input document on which to apply the masking. + * @param blocks The blocks that will mask content from the input document. + * @param mask The mask to use (defaults to `" "`). + * @returns The document (`string`) with the ranges covered by the blocks in `blocks` masked using `mask`. + */ +export function maskInputAndHints(inputDocument: string, blocks: Block[], mask: string = " "): string { let maskedString = inputDocument; for (const block of blocks) { - maskedString = maskedString.substring(0, block.range.from) + " ".repeat(block.range.to - block.range.from) + maskedString.substring(block.range.to); + maskedString = maskedString.substring(0, block.range.from) + mask.repeat(block.range.to - block.range.from) + maskedString.substring(block.range.to); } return maskedString; } \ No newline at end of file diff --git a/editor/src/kroqed-editor/editor.ts b/editor/src/waterproof-editor/editor.ts similarity index 93% rename from editor/src/kroqed-editor/editor.ts rename to editor/src/waterproof-editor/editor.ts index 3e36ac1f..f1ac1970 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/waterproof-editor/editor.ts @@ -6,14 +6,13 @@ import { AllSelection, EditorState, NodeSelection, Plugin, Selection, TextSelect import { ReplaceAroundStep, ReplaceStep, Step } from "prosemirror-transform"; import { EditorView } from "prosemirror-view"; import { undo, redo, history } from "prosemirror-history"; -import { createProseMirrorDocument } from "./prosedoc-construction/construct-document"; +import { constructDocument } from "./document/construct-document"; -import { DocChange, FileFormat, LineNumber, QedStatus, SimpleProgressParams, WrappingDocChange } from "../../../shared"; -import { COQ_CODE_PLUGIN_KEY, coqCodePlugin } from "./codeview/coqcodeplugin"; +import { DocChange, FileFormat, LineNumber, InputAreaStatus, SimpleProgressParams, WrappingDocChange } from "../../../shared"; +import { CODE_PLUGIN_KEY, codePlugin } from "./codeview"; import { createHintPlugin } from "./hinting"; import { INPUT_AREA_PLUGIN_KEY, inputAreaPlugin } from "./inputArea"; import { WaterproofSchema } from "./schema"; -import { TextDocMapping } from "./mappingModel"; import { REAL_MARKDOWN_PLUGIN_KEY, coqdocPlugin, realMarkdownPlugin } from "./markup-views"; import { menuPlugin } from "./menubar"; import { MENU_PLUGIN_KEY } from "./menubar/menubar"; @@ -33,7 +32,7 @@ import { DiagnosticMessage, HistoryChangeType } from "../../../shared/Messages"; import { DiagnosticSeverity } from "vscode"; import { OS } from "./osType"; import { checkPrePost } from "./file-utils"; -import { Positioned, WaterproofEditorConfig } from "./utilities/types"; +import { Positioned, WaterproofMapping, WaterproofEditorConfig } from "./types"; /** Type that contains a coq diagnostics object fit for use in the ProseMirror editor context. */ @@ -61,11 +60,11 @@ export class WaterproofEditor { private _translator: FileTranslator | undefined; // The file document mapping - private _mapping: TextDocMapping | undefined; + private _mapping: WaterproofMapping | undefined; // User operating system. private readonly _userOS; - private _filef: FileFormat = FileFormat.Unknown; + private _filef: FileFormat = FileFormat.MarkdownV; private currentProseDiagnostics: Array; @@ -137,9 +136,9 @@ export class WaterproofEditor { const parsedContent = this._translator.toProsemirror(resultingDocument); // this._contentElem.innerHTML = parsedContent; - const proseDoc = createProseMirrorDocument(resultingDocument, fileFormat); + const proseDoc = constructDocument(this._editorConfig.documentConstructor(resultingDocument)); - this._mapping = new TextDocMapping(fileFormat, parsedContent, version); + this._mapping = new this._editorConfig.mapping(parsedContent, version); this.createProseMirrorEditor(proseDoc); /** Ask for line numbers */ @@ -167,7 +166,7 @@ export class WaterproofEditor { if (step instanceof ReplaceStep || step instanceof ReplaceAroundStep) { if (this._mapping === undefined) throw new Error(" Mapping is undefined, cannot synchronize with vscode"); try { - const change: DocChange | WrappingDocChange = this._mapping.stepUpdate(step); // Get text document update + const change: DocChange | WrappingDocChange = this._mapping.update(step); // Get text document update this._editorConfig.api.documentChange(change); } catch (error) { console.error((error as Error).message); @@ -246,7 +245,7 @@ export class WaterproofEditor { mathPlugin, realMarkdownPlugin(this._schema), coqdocPlugin(this._schema), - coqCodePlugin, + codePlugin, progressBarPlugin, menuPlugin(this._schema, this._filef, this._userOS), keymap({ @@ -277,7 +276,7 @@ export class WaterproofEditor { const state = view.state; - const nodeViews = COQ_CODE_PLUGIN_KEY.getState(state)?.activeNodeViews; + const nodeViews = CODE_PLUGIN_KEY.getState(state)?.activeNodeViews; if (!nodeViews) return; const positionedNodeViews: Array> = Array.from(nodeViews).map((codeblock) => { return { @@ -312,10 +311,10 @@ export class WaterproofEditor { /** Called on every transaction update in which the textdocument was modified */ public sendLineNumbers() { if (!this._lineNumbersShown) return; - if (!this._view || COQ_CODE_PLUGIN_KEY.getState(this._view.state) === undefined) return; + if (!this._view || CODE_PLUGIN_KEY.getState(this._view.state) === undefined) return; const linenumbers = Array(); // @ts-expect-error TODO: Fix me - for (const codeCell of COQ_CODE_PLUGIN_KEY.getState(this._view.state).activeNodeViews) { + for (const codeCell of CODE_PLUGIN_KEY.getState(this._view.state).activeNodeViews) { // @ts-expect-error TODO: Fix me linenumbers.push(this._mapping?.findPosition(codeCell._getPos() + 1)); } @@ -330,9 +329,9 @@ export class WaterproofEditor { /** Called whenever a line number message is received from vscode to update line numbers of codemirror cells */ public setLineNumbers(msg: LineNumber) { if (!this._view || !this._mapping || msg.version < this._mapping.version) return; - const state = COQ_CODE_PLUGIN_KEY.getState(this._view.state); + const state = CODE_PLUGIN_KEY.getState(this._view.state); if (!state) return; - const tr = this._view.state.tr.setMeta(COQ_CODE_PLUGIN_KEY, msg); + const tr = this._view.state.tr.setMeta(CODE_PLUGIN_KEY, msg); this._view.dispatch(tr); } @@ -409,7 +408,7 @@ export class WaterproofEditor { const view = this._view; if (view === undefined) return; const tr = view.state.tr; - tr.setMeta(COQ_CODE_PLUGIN_KEY, {setting: "update", show: this._lineNumbersShown}); + tr.setMeta(CODE_PLUGIN_KEY, {setting: "update", show: this._lineNumbersShown}); view.dispatch(tr); this.sendLineNumbers(); } @@ -450,7 +449,7 @@ export class WaterproofEditor { this._view.dispatch(tr); } - public updateQedStatus(status: QedStatus[]) : void { + public updateQedStatus(status: InputAreaStatus[]) : void { if (!this._view) return; const state = this._view.state; const tr = state.tr; @@ -470,7 +469,7 @@ export class WaterproofEditor { if (this._view === undefined || map === undefined) return; // Get the available coq views - const views = COQ_CODE_PLUGIN_KEY.getState(this._view.state)?.activeNodeViews; + const views = CODE_PLUGIN_KEY.getState(this._view.state)?.activeNodeViews; if (views === undefined) return; // Clear the errors for (const view of views) view.clearCoqErrors(); @@ -534,4 +533,8 @@ export class WaterproofEditor { public executeCommand(command: string) { this._editorConfig.api.executeCommand(command, (new Date()).getTime()); } + + public executeHelp() { + this._editorConfig.api.executeHelp(); + } } \ No newline at end of file diff --git a/editor/src/kroqed-editor/embedded-codemirror/embedded-codemirror-keymap.ts b/editor/src/waterproof-editor/embedded-codemirror/embedded-codemirror-keymap.ts similarity index 100% rename from editor/src/kroqed-editor/embedded-codemirror/embedded-codemirror-keymap.ts rename to editor/src/waterproof-editor/embedded-codemirror/embedded-codemirror-keymap.ts diff --git a/editor/src/kroqed-editor/embedded-codemirror/embeddedCodemirror.ts b/editor/src/waterproof-editor/embedded-codemirror/embeddedCodemirror.ts similarity index 100% rename from editor/src/kroqed-editor/embedded-codemirror/embeddedCodemirror.ts rename to editor/src/waterproof-editor/embedded-codemirror/embeddedCodemirror.ts diff --git a/editor/src/kroqed-editor/embedded-codemirror/index.ts b/editor/src/waterproof-editor/embedded-codemirror/index.ts similarity index 100% rename from editor/src/kroqed-editor/embedded-codemirror/index.ts rename to editor/src/waterproof-editor/embedded-codemirror/index.ts diff --git a/editor/src/kroqed-editor/embedded-codemirror/types.ts b/editor/src/waterproof-editor/embedded-codemirror/types.ts similarity index 100% rename from editor/src/kroqed-editor/embedded-codemirror/types.ts rename to editor/src/waterproof-editor/embedded-codemirror/types.ts diff --git a/editor/src/kroqed-editor/file-utils.ts b/editor/src/waterproof-editor/file-utils.ts similarity index 100% rename from editor/src/kroqed-editor/file-utils.ts rename to editor/src/waterproof-editor/file-utils.ts diff --git a/editor/src/kroqed-editor/hinting/hint-plugin.ts b/editor/src/waterproof-editor/hinting/hint-plugin.ts similarity index 98% rename from editor/src/kroqed-editor/hinting/hint-plugin.ts rename to editor/src/waterproof-editor/hinting/hint-plugin.ts index cb7b4898..2a4d4e8b 100644 --- a/editor/src/kroqed-editor/hinting/hint-plugin.ts +++ b/editor/src/waterproof-editor/hinting/hint-plugin.ts @@ -55,7 +55,7 @@ function getHintDecorations(state: EditorState, hintNodeType: NodeType): Decorat // Create a new widget decoration for every hint node. const widgetDeco = Decoration.widget(hint.pos, // Pass a DOM rendering function. - (view: EditorView) => createCollapseDOM(view, hint), + (view: EditorView) => createCollapseDOM(view, hint), // Display this DOM *before* the actual hint content. {side: -1}); return widgetDeco; @@ -66,10 +66,10 @@ function getHintDecorations(state: EditorState, hintNodeType: NodeType): Decorat } /** - * + * * @param view The current editor view. * @param hint The `hint` object (as returned by `findDescendantsWithType`) - * @returns + * @returns */ function createCollapseDOM(view: EditorView, hint: {node: PNode, pos: number}) { // Create hint title element. diff --git a/editor/src/kroqed-editor/hinting/index.ts b/editor/src/waterproof-editor/hinting/index.ts similarity index 100% rename from editor/src/kroqed-editor/hinting/index.ts rename to editor/src/waterproof-editor/hinting/index.ts diff --git a/editor/src/waterproof-editor/index.ts b/editor/src/waterproof-editor/index.ts new file mode 100644 index 00000000..569b49a0 --- /dev/null +++ b/editor/src/waterproof-editor/index.ts @@ -0,0 +1,4 @@ +// Export the Editor class +export { WaterproofEditor } from "./editor"; +export { WaterproofDocument, WaterproofCallbacks, WaterproofMapping, WaterproofEditorConfig } from "./types"; + diff --git a/editor/src/kroqed-editor/inputArea.ts b/editor/src/waterproof-editor/inputArea.ts similarity index 100% rename from editor/src/kroqed-editor/inputArea.ts rename to editor/src/waterproof-editor/inputArea.ts diff --git a/editor/src/kroqed-editor/markup-views/CoqdocPlugin.ts b/editor/src/waterproof-editor/markup-views/CoqdocPlugin.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/CoqdocPlugin.ts rename to editor/src/waterproof-editor/markup-views/CoqdocPlugin.ts diff --git a/editor/src/kroqed-editor/markup-views/CoqdocView.ts b/editor/src/waterproof-editor/markup-views/CoqdocView.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/CoqdocView.ts rename to editor/src/waterproof-editor/markup-views/CoqdocView.ts diff --git a/editor/src/kroqed-editor/markup-views/MarkdownPlugin.ts b/editor/src/waterproof-editor/markup-views/MarkdownPlugin.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/MarkdownPlugin.ts rename to editor/src/waterproof-editor/markup-views/MarkdownPlugin.ts diff --git a/editor/src/kroqed-editor/markup-views/MarkdownView.ts b/editor/src/waterproof-editor/markup-views/MarkdownView.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/MarkdownView.ts rename to editor/src/waterproof-editor/markup-views/MarkdownView.ts diff --git a/editor/src/kroqed-editor/markup-views/index.ts b/editor/src/waterproof-editor/markup-views/index.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/index.ts rename to editor/src/waterproof-editor/markup-views/index.ts diff --git a/editor/src/kroqed-editor/markup-views/switchable-view/EditableView.ts b/editor/src/waterproof-editor/markup-views/switchable-view/EditableView.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/switchable-view/EditableView.ts rename to editor/src/waterproof-editor/markup-views/switchable-view/EditableView.ts diff --git a/editor/src/kroqed-editor/markup-views/switchable-view/EditorTheme.ts b/editor/src/waterproof-editor/markup-views/switchable-view/EditorTheme.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/switchable-view/EditorTheme.ts rename to editor/src/waterproof-editor/markup-views/switchable-view/EditorTheme.ts diff --git a/editor/src/kroqed-editor/markup-views/switchable-view/MarkdownSchema.ts b/editor/src/waterproof-editor/markup-views/switchable-view/MarkdownSchema.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/switchable-view/MarkdownSchema.ts rename to editor/src/waterproof-editor/markup-views/switchable-view/MarkdownSchema.ts diff --git a/editor/src/kroqed-editor/markup-views/switchable-view/RenderedView.ts b/editor/src/waterproof-editor/markup-views/switchable-view/RenderedView.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/switchable-view/RenderedView.ts rename to editor/src/waterproof-editor/markup-views/switchable-view/RenderedView.ts diff --git a/editor/src/kroqed-editor/markup-views/switchable-view/SwitchableView.ts b/editor/src/waterproof-editor/markup-views/switchable-view/SwitchableView.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/switchable-view/SwitchableView.ts rename to editor/src/waterproof-editor/markup-views/switchable-view/SwitchableView.ts diff --git a/editor/src/kroqed-editor/markup-views/switchable-view/editorTheme.json b/editor/src/waterproof-editor/markup-views/switchable-view/editorTheme.json similarity index 100% rename from editor/src/kroqed-editor/markup-views/switchable-view/editorTheme.json rename to editor/src/waterproof-editor/markup-views/switchable-view/editorTheme.json diff --git a/editor/src/kroqed-editor/markup-views/switchable-view/index.ts b/editor/src/waterproof-editor/markup-views/switchable-view/index.ts similarity index 100% rename from editor/src/kroqed-editor/markup-views/switchable-view/index.ts rename to editor/src/waterproof-editor/markup-views/switchable-view/index.ts diff --git a/editor/src/kroqed-editor/math-integration/index.ts b/editor/src/waterproof-editor/math-integration/index.ts similarity index 100% rename from editor/src/kroqed-editor/math-integration/index.ts rename to editor/src/waterproof-editor/math-integration/index.ts diff --git a/editor/src/kroqed-editor/math-integration/nodespecs.ts b/editor/src/waterproof-editor/math-integration/nodespecs.ts similarity index 100% rename from editor/src/kroqed-editor/math-integration/nodespecs.ts rename to editor/src/waterproof-editor/math-integration/nodespecs.ts diff --git a/editor/src/kroqed-editor/menubar/index.ts b/editor/src/waterproof-editor/menubar/index.ts similarity index 100% rename from editor/src/kroqed-editor/menubar/index.ts rename to editor/src/waterproof-editor/menubar/index.ts diff --git a/editor/src/kroqed-editor/menubar/menubar.ts b/editor/src/waterproof-editor/menubar/menubar.ts similarity index 100% rename from editor/src/kroqed-editor/menubar/menubar.ts rename to editor/src/waterproof-editor/menubar/menubar.ts diff --git a/editor/src/kroqed-editor/osType.ts b/editor/src/waterproof-editor/osType.ts similarity index 100% rename from editor/src/kroqed-editor/osType.ts rename to editor/src/waterproof-editor/osType.ts diff --git a/editor/src/kroqed-editor/progressBar.ts b/editor/src/waterproof-editor/progressBar.ts similarity index 100% rename from editor/src/kroqed-editor/progressBar.ts rename to editor/src/waterproof-editor/progressBar.ts diff --git a/editor/src/kroqed-editor/qedStatus.ts b/editor/src/waterproof-editor/qedStatus.ts similarity index 93% rename from editor/src/kroqed-editor/qedStatus.ts rename to editor/src/waterproof-editor/qedStatus.ts index cc0e13d7..9efaad0d 100644 --- a/editor/src/kroqed-editor/qedStatus.ts +++ b/editor/src/waterproof-editor/qedStatus.ts @@ -2,25 +2,25 @@ import { EditorState, Plugin, PluginKey, PluginSpec } from 'prosemirror-state'; import { Decoration, DecorationSet } from 'prosemirror-view'; import { findDescendantsWithType } from './utilities'; -import { QedStatus } from '../../../shared'; +import { InputAreaStatus } from '../../../shared'; import { WaterproofEditor } from './editor'; // Define interface for UpdateStatusPluginState export interface IUpdateStatusPluginState { - status: QedStatus[]; + status: InputAreaStatus[]; } // Key to identify the plugin within ProseMirror's plugin system export const UPDATE_STATUS_PLUGIN_KEY = new PluginKey('prosemirror-status-update'); // Helper function to convert status updates to appropriate CSS classes -const statusToDecoration = (status: QedStatus) => { +const statusToDecoration = (status: InputAreaStatus) => { switch (status) { - case QedStatus.Proven: + case InputAreaStatus.Proven: return 'proven'; - case QedStatus.Incomplete: + case InputAreaStatus.Incomplete: return 'incomplete'; - default: + case InputAreaStatus.Invalid: return ''; } }; diff --git a/editor/src/kroqed-editor/schema/index.ts b/editor/src/waterproof-editor/schema/index.ts similarity index 100% rename from editor/src/kroqed-editor/schema/index.ts rename to editor/src/waterproof-editor/schema/index.ts diff --git a/editor/src/kroqed-editor/schema/notes.md b/editor/src/waterproof-editor/schema/notes.md similarity index 100% rename from editor/src/kroqed-editor/schema/notes.md rename to editor/src/waterproof-editor/schema/notes.md diff --git a/editor/src/kroqed-editor/schema/schema-nodes.ts b/editor/src/waterproof-editor/schema/schema-nodes.ts similarity index 100% rename from editor/src/kroqed-editor/schema/schema-nodes.ts rename to editor/src/waterproof-editor/schema/schema-nodes.ts diff --git a/editor/src/kroqed-editor/schema/schema.ts b/editor/src/waterproof-editor/schema/schema.ts similarity index 100% rename from editor/src/kroqed-editor/schema/schema.ts rename to editor/src/waterproof-editor/schema/schema.ts diff --git a/editor/src/kroqed-editor/styles/autocomplete.css b/editor/src/waterproof-editor/styles/autocomplete.css similarity index 100% rename from editor/src/kroqed-editor/styles/autocomplete.css rename to editor/src/waterproof-editor/styles/autocomplete.css diff --git a/editor/src/kroqed-editor/styles/context-menu.css b/editor/src/waterproof-editor/styles/context-menu.css similarity index 100% rename from editor/src/kroqed-editor/styles/context-menu.css rename to editor/src/waterproof-editor/styles/context-menu.css diff --git a/editor/src/kroqed-editor/styles/coqdoc.css b/editor/src/waterproof-editor/styles/coqdoc.css similarity index 100% rename from editor/src/kroqed-editor/styles/coqdoc.css rename to editor/src/waterproof-editor/styles/coqdoc.css diff --git a/editor/src/kroqed-editor/styles/freeze.css b/editor/src/waterproof-editor/styles/freeze.css similarity index 100% rename from editor/src/kroqed-editor/styles/freeze.css rename to editor/src/waterproof-editor/styles/freeze.css diff --git a/editor/src/kroqed-editor/styles/hints.css b/editor/src/waterproof-editor/styles/hints.css similarity index 100% rename from editor/src/kroqed-editor/styles/hints.css rename to editor/src/waterproof-editor/styles/hints.css diff --git a/editor/src/kroqed-editor/styles/index.ts b/editor/src/waterproof-editor/styles/index.ts similarity index 100% rename from editor/src/kroqed-editor/styles/index.ts rename to editor/src/waterproof-editor/styles/index.ts diff --git a/editor/src/kroqed-editor/styles/input-area.css b/editor/src/waterproof-editor/styles/input-area.css similarity index 100% rename from editor/src/kroqed-editor/styles/input-area.css rename to editor/src/waterproof-editor/styles/input-area.css diff --git a/editor/src/kroqed-editor/styles/markdown.css b/editor/src/waterproof-editor/styles/markdown.css similarity index 100% rename from editor/src/kroqed-editor/styles/markdown.css rename to editor/src/waterproof-editor/styles/markdown.css diff --git a/editor/src/kroqed-editor/styles/math.css b/editor/src/waterproof-editor/styles/math.css similarity index 100% rename from editor/src/kroqed-editor/styles/math.css rename to editor/src/waterproof-editor/styles/math.css diff --git a/editor/src/kroqed-editor/styles/menubar.css b/editor/src/waterproof-editor/styles/menubar.css similarity index 100% rename from editor/src/kroqed-editor/styles/menubar.css rename to editor/src/waterproof-editor/styles/menubar.css diff --git a/editor/src/kroqed-editor/styles/notifications.css b/editor/src/waterproof-editor/styles/notifications.css similarity index 100% rename from editor/src/kroqed-editor/styles/notifications.css rename to editor/src/waterproof-editor/styles/notifications.css diff --git a/editor/src/kroqed-editor/styles/progressBar.css b/editor/src/waterproof-editor/styles/progressBar.css similarity index 100% rename from editor/src/kroqed-editor/styles/progressBar.css rename to editor/src/waterproof-editor/styles/progressBar.css diff --git a/editor/src/kroqed-editor/styles/spinner.css b/editor/src/waterproof-editor/styles/spinner.css similarity index 100% rename from editor/src/kroqed-editor/styles/spinner.css rename to editor/src/waterproof-editor/styles/spinner.css diff --git a/editor/src/kroqed-editor/styles/style.css b/editor/src/waterproof-editor/styles/style.css similarity index 100% rename from editor/src/kroqed-editor/styles/style.css rename to editor/src/waterproof-editor/styles/style.css diff --git a/editor/src/kroqed-editor/styles/waterproof.css b/editor/src/waterproof-editor/styles/waterproof.css similarity index 100% rename from editor/src/kroqed-editor/styles/waterproof.css rename to editor/src/waterproof-editor/styles/waterproof.css diff --git a/editor/src/kroqed-editor/translation/Translator.ts b/editor/src/waterproof-editor/translation/Translator.ts similarity index 71% rename from editor/src/kroqed-editor/translation/Translator.ts rename to editor/src/waterproof-editor/translation/Translator.ts index 2fa5b918..f630c906 100644 --- a/editor/src/kroqed-editor/translation/Translator.ts +++ b/editor/src/waterproof-editor/translation/Translator.ts @@ -6,18 +6,14 @@ export class FileTranslator { private _filef: FileFormat; /** - * Create a new file format translator. + * Create a new file format translator. * @param fileFormat The input file format to use. Can be either `FileFormat.MarkdownV` for .mv files or * `FileFormat.RegularV` for regular coq files. */ constructor(fileFormat: FileFormat) { this._filef = fileFormat; - switch (this._filef) { - case FileFormat.Unknown: - throw new Error("Cannot initialize Translator for `unknown` file type."); - } } - + /** * Get the file format this FileTranslator was configured for. */ @@ -26,20 +22,18 @@ export class FileTranslator { } /** - * Convert an input file to a prosemirror compatible HTML representation. + * Convert an input file to a prosemirror compatible HTML representation. * Input format is set by `fileFormat` in the constructor. - * @param inputDocument The input document read from disk. + * @param inputDocument The input document read from disk. * @returns A prosemirror compatible HTML document (as string). */ public toProsemirror(inputDocument: string): string { switch (this._filef) { case FileFormat.MarkdownV: - return translateMvToProsemirror(inputDocument); // + return translateMvToProsemirror(inputDocument); case FileFormat.RegularV: // TODO: Parser for regular .v return translateMvToProsemirror(inputDocument); - case FileFormat.Unknown: - throw new Error("Cannot convert from unknown format"); } } } diff --git a/editor/src/kroqed-editor/translation/index.ts b/editor/src/waterproof-editor/translation/index.ts similarity index 100% rename from editor/src/kroqed-editor/translation/index.ts rename to editor/src/waterproof-editor/translation/index.ts diff --git a/editor/src/kroqed-editor/translation/toProsemirror/index.ts b/editor/src/waterproof-editor/translation/toProsemirror/index.ts similarity index 100% rename from editor/src/kroqed-editor/translation/toProsemirror/index.ts rename to editor/src/waterproof-editor/translation/toProsemirror/index.ts diff --git a/editor/src/kroqed-editor/translation/toProsemirror/mvFileToProsemirror.ts b/editor/src/waterproof-editor/translation/toProsemirror/mvFileToProsemirror.ts similarity index 100% rename from editor/src/kroqed-editor/translation/toProsemirror/mvFileToProsemirror.ts rename to editor/src/waterproof-editor/translation/toProsemirror/mvFileToProsemirror.ts diff --git a/editor/src/kroqed-editor/translation/toProsemirror/parseAsMv.ts b/editor/src/waterproof-editor/translation/toProsemirror/parseAsMv.ts similarity index 100% rename from editor/src/kroqed-editor/translation/toProsemirror/parseAsMv.ts rename to editor/src/waterproof-editor/translation/toProsemirror/parseAsMv.ts diff --git a/editor/src/kroqed-editor/translation/toProsemirror/parser.ts b/editor/src/waterproof-editor/translation/toProsemirror/parser.ts similarity index 100% rename from editor/src/kroqed-editor/translation/toProsemirror/parser.ts rename to editor/src/waterproof-editor/translation/toProsemirror/parser.ts diff --git a/editor/src/kroqed-editor/translation/types.ts b/editor/src/waterproof-editor/translation/types.ts similarity index 100% rename from editor/src/kroqed-editor/translation/types.ts rename to editor/src/waterproof-editor/translation/types.ts diff --git a/editor/src/waterproof-editor/types.ts b/editor/src/waterproof-editor/types.ts new file mode 100644 index 00000000..696c2a6c --- /dev/null +++ b/editor/src/waterproof-editor/types.ts @@ -0,0 +1,35 @@ +import { Step } from "prosemirror-transform"; +import { DocChange, WrappingDocChange } from "../../../shared"; +import { Block } from "./document"; +import { StringCell } from "../mapping/mvFile/types"; + +export type Positioned = { + obj: A; + pos: number | undefined; +}; + +export type WaterproofDocument = Block[]; + +export type WaterproofCallbacks = { + executeCommand: (command: string, time: number) => void, + executeHelp: () => void, + editorReady: () => void, + documentChange: (change: DocChange | WrappingDocChange) => void, + applyStepError: (errorMessage: string) => void, + cursorChange: (cursorPosition: number) => void + lineNumbers: (linenumbers: Array, version: number) => void, +} + +export abstract class WaterproofMapping { + abstract getMapping: () => Map; + abstract get version(): number; + abstract findPosition: (index: number) => number; + abstract findInvPosition: (index: number) => number; + abstract update: (step: Step) => DocChange | WrappingDocChange; +} + +export type WaterproofEditorConfig = { + api: WaterproofCallbacks, + documentConstructor: (document: string) => WaterproofDocument, + mapping: new (inputString: string, versionNum: number) => WaterproofMapping +} \ No newline at end of file diff --git a/editor/src/kroqed-editor/utilities/index.ts b/editor/src/waterproof-editor/utilities/index.ts similarity index 100% rename from editor/src/kroqed-editor/utilities/index.ts rename to editor/src/waterproof-editor/utilities/index.ts diff --git a/editor/src/kroqed-editor/utilities/prosemirror.ts b/editor/src/waterproof-editor/utilities/prosemirror.ts similarity index 100% rename from editor/src/kroqed-editor/utilities/prosemirror.ts rename to editor/src/waterproof-editor/utilities/prosemirror.ts diff --git a/package.json b/package.json index 9958394d..6c3123db 100644 --- a/package.json +++ b/package.json @@ -578,7 +578,7 @@ "clean": "rm -R test_out/*; rm -R editor_output/*; rm -R out/*", "package": "npm run clean && npm run test-package", "unit-tests": "npx jest", - "lezer-generator": "npx lezer-generator ./editor/src/kroqed-editor/codeview/lang-pack/syntax.grammar -o ./editor/src/kroqed-editor/codeview/lang-pack/syntax.ts", + "lezer-generator": "echo FIX", "cypress-tests": "npx cypress run", "lint": "npx eslint .", "lint-fix": "npx eslint . --fix" diff --git a/shared/FileFormat.ts b/shared/FileFormat.ts index 8a3e3302..5cc1e1a2 100644 --- a/shared/FileFormat.ts +++ b/shared/FileFormat.ts @@ -5,6 +5,5 @@ export enum FileFormat { /** Markdown enabled coq file (extension: `.mv`) */ MarkdownV = "MarkdownV", /** Regular coq file, with the possibility for coqdoc comments (extension: `.v`) */ - RegularV = "RegularV", - Unknown = "Unknown" + RegularV = "RegularV" } diff --git a/shared/QedStatus.ts b/shared/InputAreaStatus.ts similarity index 83% rename from shared/QedStatus.ts rename to shared/InputAreaStatus.ts index f92ee843..d9f11cdd 100644 --- a/shared/QedStatus.ts +++ b/shared/InputAreaStatus.ts @@ -1,11 +1,11 @@ /** * The proof status of an input area. */ -export enum QedStatus { +export enum InputAreaStatus { /** The proof is correct. */ Proven = "proven", /** The proof is unfinished or contains an error. */ Incomplete = "incomplete", /** The input area does not contain `Qed.` at the end, so the status cannot be determined. */ - InvalidInputArea = "invalid", + Invalid = "invalid", } \ No newline at end of file diff --git a/shared/Messages.ts b/shared/Messages.ts index 724bd2e5..9759509a 100644 --- a/shared/Messages.ts +++ b/shared/Messages.ts @@ -2,7 +2,7 @@ import { DiagnosticSeverity } from "vscode"; import { FileFormat } from "./FileFormat"; import { LineNumber } from "./LineNumber"; import { DocChange, WrappingDocChange } from "./DocChange"; -import { QedStatus } from "./QedStatus"; +import { InputAreaStatus } from "./InputAreaStatus"; import { Completion } from "@codemirror/autocomplete"; import { GoalAnswer, HypVisibility, PpString } from "../lib/types"; @@ -37,7 +37,7 @@ export type Message = | MessageBase | MessageBase | MessageBase - | MessageBase + | MessageBase | MessageBase | MessageBase, visibility?: HypVisibility }> | MessageBase[]}> diff --git a/shared/index.ts b/shared/index.ts index 38c99253..258f08f1 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -12,5 +12,5 @@ export { SimpleProgressParams } from "./Messages"; // Export QedStatus type -export { QedStatus } from "./QedStatus"; +export { InputAreaStatus } from "./InputAreaStatus"; export { LineNumber} from "./LineNumber"; diff --git a/src/lsp-client/client.ts b/src/lsp-client/client.ts index 94941dbd..d9edf1d5 100644 --- a/src/lsp-client/client.ts +++ b/src/lsp-client/client.ts @@ -10,7 +10,7 @@ import { } from "vscode-languageclient"; import { GoalAnswer, GoalRequest, PpString } from "../../lib/types"; -import { MessageType, OffsetDiagnostic, QedStatus, SimpleProgressParams } from "../../shared"; +import { MessageType, OffsetDiagnostic, InputAreaStatus, SimpleProgressParams } from "../../shared"; import { IFileProgressComponent } from "../components"; import { WebviewManager } from "../webviewManager"; import { ICoqLspClient, WpDiagnostic } from "./clientTypes"; @@ -184,7 +184,7 @@ export function CoqLspClient(Base: T) { } // for each input area, check the proof status - let statuses: QedStatus[]; + let statuses: InputAreaStatus[]; try { statuses = await Promise.all(inputAreas.map(a => determineProofStatus(this, document, a) diff --git a/src/lsp-client/qedStatus.ts b/src/lsp-client/qedStatus.ts index 9022fc09..883fc880 100644 --- a/src/lsp-client/qedStatus.ts +++ b/src/lsp-client/qedStatus.ts @@ -1,7 +1,7 @@ import { Range, TextDocument } from "vscode"; import { GoalAnswer, PpString } from "../../lib/types"; -import { QedStatus } from "../../shared"; +import { InputAreaStatus } from "../../shared"; import { ICoqLspClient } from "./clientTypes"; // TODO: only consider Markdown parts @@ -45,13 +45,13 @@ function isComplete(response: GoalAnswer): boolean { return !("error" in response); } -export async function determineProofStatus(client: ICoqLspClient, document: TextDocument, inputArea: Range): Promise { +export async function determineProofStatus(client: ICoqLspClient, document: TextDocument, inputArea: Range): Promise { // get the (end) position of the last line in the input area // funnily, it can be in a next input area, and we accept this const position = client.sentenceManager.getEndOfSentence(inputArea.end, true); if (!position) { // console.warn("qedStatus.ts : No sentence after input area"); - return QedStatus.InvalidInputArea; + return InputAreaStatus.Invalid; } // check that last command is "Qed" (or return "invalid") @@ -59,10 +59,10 @@ export async function determineProofStatus(client: ICoqLspClient, document: Text const i = position.character - 4; if (i < 0 || document.lineAt(position).text.slice(i, i+4) !== "Qed.") { // console.warn("qedStatus.ts : Last sentence is not `Qed.`"); - return QedStatus.InvalidInputArea; + return InputAreaStatus.Invalid; } // request goals and return conclusion based on them const response = await client.requestGoals(position.translate(0, -1)); - return isComplete(response) ? QedStatus.Proven : QedStatus.Incomplete; + return isComplete(response) ? InputAreaStatus.Proven : InputAreaStatus.Incomplete; } diff --git a/src/pm-editor/fileUtils.ts b/src/pm-editor/fileUtils.ts index f9b8c6db..705ed0a2 100644 --- a/src/pm-editor/fileUtils.ts +++ b/src/pm-editor/fileUtils.ts @@ -4,7 +4,7 @@ import { FileFormat } from "../../shared"; /** Gets the file format from the text doc uri */ -export function getFormatFromExtension(doc: TextDocument): FileFormat { +export function getFormatFromExtension(doc: TextDocument): FileFormat | undefined { // Get the parts from uri const uriParts = doc.uri.fsPath.split("."); // Get the extension @@ -15,10 +15,9 @@ export function getFormatFromExtension(doc: TextDocument): FileFormat { return FileFormat.MarkdownV; } else if (extension === "v") { return FileFormat.RegularV; - } else { - // Unknown filed type this should not happen - return FileFormat.Unknown; } + + return undefined; } export function isIllegalFileName(fileName: string): boolean { diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index 7b2d8b56..050e6582 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -21,7 +21,7 @@ export class ProseMirrorWebview extends EventEmitter { private readonly _document: TextDocument; /** The file format of the doc associated with this ProseMirror instance */ - private readonly _format: FileFormat; + private readonly _format: FileFormat = FileFormat.MarkdownV; /** The editor that appends changes to the document associated with this panel */ private readonly _workspaceEditor = new SequentialEditor(); @@ -90,7 +90,6 @@ export class ProseMirrorWebview extends EventEmitter { WaterproofLogger.log("Error: Illegal file name encountered."); } - this._format = getFormatFromExtension(doc); this._panel = webview; this._workspaceEditor.onFinish(() => { @@ -106,6 +105,14 @@ export class ProseMirrorWebview extends EventEmitter { this._teacherMode = WaterproofConfigHelper.teacherMode; this._enforceCorrectNonInputArea = WaterproofConfigHelper.enforceCorrectNonInputArea; this._lastCorrectDocString = doc.getText(); + + const format = getFormatFromExtension(doc); + if (format === undefined) { + // FIXME: We should never encounter this, as the extension is only activated for .v and .mv files? + WaterproofLogger.log("Aborting creation of Waterproof editor. Encountered a file with extension different from .mv or .v!"); + return; + } + this._format = format; } private handleFileNameSaveAs(value: typeof SAVE_AS | undefined) { From de427dc2a662da79665698b25317efe9e268d0b6 Mon Sep 17 00:00:00 2001 From: Alisakondratyev Date: Tue, 29 Jul 2025 14:04:23 -0500 Subject: [PATCH 66/93] adding flashing side panel buttons and cleaning up old button functionalities --- src/extension.ts | 2 +- src/webviewManager.ts | 34 ++++++------ src/webviews/coqWebview.ts | 5 +- src/webviews/sidePanel.ts | 108 +++++-------------------------------- 4 files changed, 33 insertions(+), 116 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 740e354c..213b3858 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -155,7 +155,7 @@ export class Waterproof implements Disposable { const goalsPanel = new GoalsPanel(this.context.extensionUri, CoqLspClientConfig.create(WaterproofConfigHelper.configuration)) this.goalsComponents.push(goalsPanel); this.webviewManager.addToolWebview("goals", goalsPanel); - this.webviewManager.open("goals") + this.webviewManager.open("goals"); this.webviewManager.addToolWebview("symbols", new SymbolsPanel(this.context.extensionUri)); this.webviewManager.addToolWebview("search", new Search(this.context.extensionUri)); this.webviewManager.addToolWebview("help", new Help(this.context.extensionUri)); diff --git a/src/webviewManager.ts b/src/webviewManager.ts index 0bba204f..08236c17 100644 --- a/src/webviewManager.ts +++ b/src/webviewManager.ts @@ -12,7 +12,8 @@ export enum WebviewManagerEvents { focus = "focus", cursorChange = "cursorChange", command = "command", - updateButton = "updateButton" + updateButton = "updateButton", + buttonClick = "buttonClick", } /** @@ -110,9 +111,16 @@ export class WebviewManager extends EventEmitter { webview.on(WebviewEvents.message, (msg: Message) => { this.onToolsMessage(name, msg); }); - webview.on(WebviewEvents.change, (state) => { + /**webview.on(WebviewEvents.change, (state) => { if (state == WebviewState.focus && webview.supportInsert) this._active.insert(name); this.emit(WebviewManagerEvents.updateButton, { name, open: webview.isOpened}); + });*/ + webview.on(WebviewEvents.change, (state) => { + if (state == WebviewState.focus && webview.supportInsert) this._active.insert(name); + // Emit buttonClick for any state change that indicates user interaction + //if (state == WebviewState.focus || state == WebviewState.visible) { + //this.emit(WebviewManagerEvents.buttonClick, { name}); + //} }); } @@ -146,6 +154,7 @@ export class WebviewManager extends EventEmitter { /** * Opens a tool webview to the user + * If the webview is already open, it will be revealed. * @param id the id of the tool webview */ public open(id: string) { @@ -153,39 +162,30 @@ export class WebviewManager extends EventEmitter { throw new Error("Tool webview does not have this panel: " + id); } + // Emit button click event before performing any state checks + const panel = this._toolWebviews.get(id); - console.log("called open " + id); // Check if the panel is already open if (panel?.isOpened) { - console.log("Panel is already open: " + id); + this.emit(WebviewManagerEvents.buttonClick, { name: id }); return; } // Open the panel if it is not already open else if(panel?.isHidden) { - console.log("Panel is hidden, revealing: " + id); - panel?.readyPanel(); + this.emit(WebviewManagerEvents.buttonClick, { name: id }); panel?.revealPanel(); } // Open the panel if it is not hidden and not already open else{ - console.log("Panel is not open, creating: " + id); + this.emit(WebviewManagerEvents.buttonClick, { name: id }); panel?.readyPanel(); panel?.activatePanel(); + panel?.revealPanel(); } } - - /** - * Reveals a panel to the user - * @param id the id of the tool webview - */ - public reveal(id: string) { - if (this._toolWebviews.has(id)) new Error("Tool webview does not have this panel: " + id); - this._toolWebviews.get(id)?.revealPanel() - } - /** * Sends `message` to the specified panel. * @param panelName a URI to refer to a ProseMirror panel, or a name to refer to a tool panel diff --git a/src/webviews/coqWebview.ts b/src/webviews/coqWebview.ts index e4c4da9e..59bc8471 100644 --- a/src/webviews/coqWebview.ts +++ b/src/webviews/coqWebview.ts @@ -66,7 +66,7 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { return (this._state == WebviewState.open); } - protected get state() { + public get state() { return this._state; } @@ -172,6 +172,7 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { public activatePanel() { if (this.state == WebviewState.ready) { this.create(); + this.changeState(WebviewState.open); } } @@ -180,7 +181,7 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { */ public revealPanel() { if (!this._panel?.visible) { - this._panel?.reveal(ViewColumn.Two) + this._panel?.reveal(ViewColumn.Two); } } diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index fa260b44..f125c03f 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -22,31 +22,16 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { private readonly _extensionUri: vscode.Uri, private readonly _manager: WebviewManager, ) { - // Subscribe to the updateButton event to handle button transparency updates - this._manager.on(WebviewManagerEvents.updateButton, (e) => { - // Update the transparency of the button based on the event - // This is done when a panel is open - this.updateButtonTransparency(e.name, e.open); + this._manager.on(WebviewManagerEvents.buttonClick, (e) => { + // Button flashes when clicked + this.flashButton(e.name); }); } - public updateGreyedOutButton(buttonId: string, open: boolean) { - if (this._greyedOutButtons.has(buttonId) && !open) { - this._greyedOutButtons.delete(buttonId); - } else if (open){ - this._greyedOutButtons.add(buttonId); - } - } - public updateButtonTransparency(buttonId: string, open: boolean) { - console.log(`Updating button transparency for ${buttonId} to ${open}`); - // Instead of setting permanent transparency, flash the button briefly - this.flashButton(buttonId); - } private flashButton(buttonId: string) { // If the view is not available, return without flashing if (!this._view) return; - console.log(`Flashing button: ${buttonId}`); // Post a message to the webview to flash the button this._view.webview.postMessage({ type: 'flash', @@ -54,17 +39,6 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { }); } - private updateButtonStyles(buttonId: string, open: boolean) { - // If the view is not available, return without updating the styles - if (!this._view) return; - - // Post a message to the webview to update the button transparency - this._view.webview.postMessage({ - type: 'trans', - buttonId: buttonId, - open: open, - }); - } public resolveWebviewView( webviewView: vscode.WebviewView, @@ -94,18 +68,7 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { // Handle other messages by opening the command in the manager this._manager.open(message.command); } - }); - // Handle the visibility of the webview when user reopens side panel - webviewView.onDidChangeVisibility(() => { - if (webviewView.visible) { - - webviewView.webview.postMessage({ - type: 'restoreTransparency', - greyedOutButtonsList: Array.from(this._greyedOutButtons) - }); - } - }); - + }); } // Now we create the actual web page @@ -136,13 +99,8 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { background-color: var(--vscode-button-hoverBackground); } - .transparent { - opacity: 0.6; - cursor: default; - } - .flash { - animation: flashGrey 0.7s ease-in-out; + animation: flashGrey 0.5s ease-in-out; } @keyframes flashGrey { @@ -227,55 +185,13 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { window.addEventListener('message', event => { const message = event.data; - switch (message.type) { - // If the message is for flashing the button - case 'flash': { - const button = document.getElementById(message.buttonId); - if (button) { - // Add flash class to trigger animation - button.classList.add('flash'); - // Remove the class after animation completes - setTimeout(() => { - button.classList.remove('flash'); - }, 300); // Match the animation duration - } - break; - } - // If the message is for changing the transparency (legacy case) - case 'trans': { - const button = document.getElementById(message.buttonId); - if (button) { - const transparent = message.open; - if (transparent) { - // Add the 'transparent' class to the button if it should be transparent - button.classList.add('transparent'); - } else { - // Remove the 'transparent' class from the button if it should not be transparent - button.classList.remove('transparent'); - } - } - break; - } - //If the message is for restoring transparency after side panel is reopened - case 'restoreTransparency': { - const buttonIds = [ - 'goals', 'logbook', 'debug', 'execute', - 'help', 'search', 'expandDefinition', 'symbols', 'tactics' - ]; - buttonIds.forEach(id => { - const btn = document.getElementById(id); - if (!btn) return; - // Check if the button ID is in the greyedOutButtonsList - if (message.greyedOutButtonsList?.includes(id)) { - // If it is, make transparent - btn.classList.add('transparent'); - } - else { - // If it is not, remove the transparent class - btn.classList.remove('transparent'); - } - }); - break; + if (message.type === 'flash') { + const button = document.getElementById(message.buttonId); + if (button) { + button.classList.add('flash'); + setTimeout(() => { + button.classList.remove('flash'); + }, 500); } } }); From 52ed9f50b4ab6f7c701aa50ff36db5409d26ede7 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 30 Jul 2025 09:25:45 +0200 Subject: [PATCH 67/93] Fix merge --- src/webviews/sidePanel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index 502d42d8..83a64b84 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -1,7 +1,6 @@ import * as vscode from 'vscode'; import { getNonce } from '../util'; import { WebviewManager, WebviewManagerEvents } from '../webviewManager'; -import { WaterproofLogger as wpl} from '../helpers'; // this function add the side panel to the vs code side panel export function addSidePanel(context: vscode.ExtensionContext, manager: WebviewManager) { const provider = new SidePanelProvider(context.extensionUri, manager); From 160f63e745181ff5a2e96459e0157e1fcec656b0 Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:41:30 +0200 Subject: [PATCH 68/93] Move dynamic auto completion logic into WaterproofEditor (#196) --- editor/src/index.ts | 14 ++------------ editor/src/waterproof-editor/editor.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/editor/src/index.ts b/editor/src/index.ts index ef389632..06e5bb89 100644 --- a/editor/src/index.ts +++ b/editor/src/index.ts @@ -1,8 +1,5 @@ -import { Completion } from "@codemirror/autocomplete"; - import { FileFormat, Message, MessageType } from "../../shared"; import { WaterproofEditor, WaterproofEditorConfig } from "./waterproof-editor"; -import { CODE_PLUGIN_KEY } from "./waterproof-editor/codeview"; // TODO: Move this to a types location. import { TextDocMappingMV, TextDocMappingV } from "./mapping"; import { blocksFromMV, blocksFromV } from "./document-construction/construct-document"; @@ -91,15 +88,8 @@ window.onload = () => { break; } case MessageType.setAutocomplete: // Handle autocompletion - { const state = editor.state; - if (!state) break; - const completions: Completion[] = msg.body; - // Apply autocomplete to all coq cells - CODE_PLUGIN_KEY - .getState(state) - ?.activeNodeViews - ?.forEach(codeBlock => codeBlock.handleNewComplete(completions)); - break; } + editor.handleCompletions(msg.body); + break; case MessageType.qedStatus: { const statuses = msg.body; // one status for each input area, in order editor.updateQedStatus(statuses); diff --git a/editor/src/waterproof-editor/editor.ts b/editor/src/waterproof-editor/editor.ts index f1ac1970..d44fa05f 100644 --- a/editor/src/waterproof-editor/editor.ts +++ b/editor/src/waterproof-editor/editor.ts @@ -33,6 +33,7 @@ import { DiagnosticSeverity } from "vscode"; import { OS } from "./osType"; import { checkPrePost } from "./file-utils"; import { Positioned, WaterproofMapping, WaterproofEditorConfig } from "./types"; +import { Completion } from "@codemirror/autocomplete"; /** Type that contains a coq diagnostics object fit for use in the ProseMirror editor context. */ @@ -326,6 +327,16 @@ export class WaterproofEditor { this._editorConfig.api.lineNumbers(linenumbers, this._mapping.version); } + public handleCompletions(completions: Array) { + const state = this._view?.state; + if (!state) return; + // Apply autocomplete to all coq cells + CODE_PLUGIN_KEY + .getState(state) + ?.activeNodeViews + ?.forEach(codeBlock => codeBlock.handleNewComplete(completions)); + } + /** Called whenever a line number message is received from vscode to update line numbers of codemirror cells */ public setLineNumbers(msg: LineNumber) { if (!this._view || !this._mapping || msg.version < this._mapping.version) return; From 6c2cd1f92394dad8df3230b69dc829cf0037d04f Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:36:10 +0200 Subject: [PATCH 69/93] Fix lezer-generator command --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6c3123db..5b6252ca 100644 --- a/package.json +++ b/package.json @@ -573,12 +573,12 @@ "esbuild": "node esbuild.mjs", "typecheck": "tsc -b", "compile": "npm run esbuild", - "watch": "npm run lezer-generator && npm run esbuild -- --watch", + "watch": "npm run esbuild -- --watch", "test-package": "node_modules/.bin/vsce package --ignoreFile .vscodeignore -o test_out/extension.vsix", "clean": "rm -R test_out/*; rm -R editor_output/*; rm -R out/*", "package": "npm run clean && npm run test-package", "unit-tests": "npx jest", - "lezer-generator": "echo FIX", + "lezer-generator": "npx lezer-generator ./editor/src/waterproof-editor/codeview/lang-pack/syntax.grammar -o ./editor/src/waterproof-editor/codeview/lang-pack/syntax.ts", "cypress-tests": "npx cypress run", "lint": "npx eslint .", "lint-fix": "npx eslint . --fix" From d72c3e2e1ebf60e8d4a9876842a0be9916984daf Mon Sep 17 00:00:00 2001 From: DikieDick <26772815+DikieDick@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:51:09 +0200 Subject: [PATCH 70/93] Set tactic completions when initializing editor. (#195) * Set tactic completions when initializing editor. * Clean-up as per review * Remove syntax message --- editor/src/index.ts | 6 +- .../waterproof-editor/autocomplete/index.ts | 1 - .../waterproof-editor/autocomplete/tactics.ts | 67 ------------------- .../waterproof-editor/codeview/code-plugin.ts | 21 ++++-- .../waterproof-editor/codeview/nodeview.ts | 24 ++++++- editor/src/waterproof-editor/editor.ts | 7 +- editor/src/waterproof-editor/types.ts | 8 +++ package.json | 5 -- shared/Messages.ts | 2 - src/helpers/config-helper.ts | 5 -- src/pm-editor/pmWebview.ts | 12 ---- 11 files changed, 49 insertions(+), 109 deletions(-) delete mode 100644 editor/src/waterproof-editor/autocomplete/tactics.ts diff --git a/editor/src/index.ts b/editor/src/index.ts index 06e5bb89..24a91234 100644 --- a/editor/src/index.ts +++ b/editor/src/index.ts @@ -3,6 +3,8 @@ import { WaterproofEditor, WaterproofEditorConfig } from "./waterproof-editor"; // TODO: Move this to a types location. import { TextDocMappingMV, TextDocMappingV } from "./mapping"; import { blocksFromMV, blocksFromV } from "./document-construction/construct-document"; +// TODO: The tactics completions are static, we want them to be dynamic (LSP supplied and/or configurable when the editor is running) +import tactics from "../../shared/completions/tactics.json"; /** * Very basic representation of the acquirable VSCodeApi. @@ -15,6 +17,7 @@ interface VSCodeAPI { function createConfiguration(format: FileFormat, codeAPI: VSCodeAPI) { // Create the WaterproofEditorConfig object const cfg: WaterproofEditorConfig = { + completions: tactics, api: { executeHelp() { codeAPI.postMessage({ type: MessageType.command, body: { command: "Help.", time: (new Date()).getTime()} }); @@ -117,9 +120,6 @@ window.onload = () => { { const diagnostics = msg.body; editor.parseCoqDiagnostics(diagnostics); break; } - case MessageType.syntax: - editor.initTacticCompletion(msg.body); - break; default: // If we reach this 'default' case, then we have encountered an unknown message type. console.log(`[WEBVIEW] Unrecognized message type '${msg.type}'`); diff --git a/editor/src/waterproof-editor/autocomplete/index.ts b/editor/src/waterproof-editor/autocomplete/index.ts index 95d055bf..9e645ff6 100644 --- a/editor/src/waterproof-editor/autocomplete/index.ts +++ b/editor/src/waterproof-editor/autocomplete/index.ts @@ -2,5 +2,4 @@ export { emojiCompletionSource } from "./emojis"; export { coqCompletionSource } from "./coqTerms"; export { symbolCompletionSource } from "./symbols"; -export { tacticCompletionSource } from "./tactics"; export { renderIcon } from "./renderSymbol"; diff --git a/editor/src/waterproof-editor/autocomplete/tactics.ts b/editor/src/waterproof-editor/autocomplete/tactics.ts deleted file mode 100644 index 05e8ca82..00000000 --- a/editor/src/waterproof-editor/autocomplete/tactics.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Completion, CompletionContext, CompletionResult, CompletionSource, snippetCompletion } from "@codemirror/autocomplete"; -import tactics from "../../../../shared/completions/tactics.json"; -import tacticsCoq from "../../../../shared/completions/tacticsCoq.json"; - -export function initializeTacticCompletion(useTacticsCoq: boolean = false) { - TacticCompletion.initialize(useTacticsCoq); // Initialize the singleton instance -} - -// Singleton method -class TacticCompletion { - private tacticCompletions: Completion[] = []; - private static instance: TacticCompletion | null = null; - - private constructor(useTacticsCoq: boolean) { - this.initializeCompletions(useTacticsCoq); - } - - private async initializeCompletions(useTacticsCoq: boolean) { - if (useTacticsCoq) { - this.tacticCompletions = tacticsCoq.map((value) => { - return snippetCompletion(value.template, value); - }); - } else { - this.tacticCompletions = tactics.map((value) => { - return snippetCompletion(value.template, value); - }); - } - } - - /* Public function to initialize based on selected setting */ - public static initialize(useTacticsCoq: boolean = false): void { - this.instance = new TacticCompletion(useTacticsCoq); - } - - /* Instance getter to pass instance to nodeview.ts */ - static getInstance(useTacticsCoq: boolean = false): TacticCompletion { - if (!this.instance) { - this.instance = new TacticCompletion(useTacticsCoq); - } - return this.instance; - } - - - - public tacticCompletionSource: CompletionSource = function(context: CompletionContext): Promise { - return new Promise((resolve, _reject) => { - const before = context.matchBefore(/([^\s.\n\t\-+*])[^\s\n\t\-+*]*/gm); - const period = /\./gm - const line = context.state.doc.lineAt(context.pos); - const firstletter = line.text.match(/[a-zA-Z]/); - const lineBeforeCursor = line.text.slice(0, context.pos - line.from); - - if ((!context.explicit && !before) || period.test(lineBeforeCursor)) resolve(null); - resolve({ - // start completion instance from first letter of line - from: firstletter ? line.from + firstletter.index!: context.pos, - // non-null assertion operator "!" used to remove 'possibly null' error - options: TacticCompletion.instance!.tacticCompletions, - validFor: /^[\t]*[^.]*/gm - }) - }); - } -} - - -// Export the singleton instance to nodeview.ts -export const tacticCompletionSource = TacticCompletion.getInstance().tacticCompletionSource; diff --git a/editor/src/waterproof-editor/codeview/code-plugin.ts b/editor/src/waterproof-editor/codeview/code-plugin.ts index 8331ebbc..881d047b 100644 --- a/editor/src/waterproof-editor/codeview/code-plugin.ts +++ b/editor/src/waterproof-editor/codeview/code-plugin.ts @@ -9,6 +9,8 @@ import { EditorView } from "prosemirror-view"; import { CodeBlockView } from "./nodeview"; import { ReplaceStep } from "prosemirror-transform"; import { LineNumber } from "../../../../shared"; +import { Completion, snippetCompletion } from "@codemirror/autocomplete"; +import { WaterproofCompletion } from "../types"; //////////////////////////////////////////////////////////// @@ -30,7 +32,7 @@ export const CODE_PLUGIN_KEY = new PluginKey("waterproof-edito * Returns a function suitable for passing as a field in `EditorProps.nodeViews`. * @see https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews */ -export function createCoqCodeView(){ +export function createCoqCodeView(completions: Array){ return (node: ProseNode, view: EditorView, getPos: () => number | undefined): CodeBlockView => { /** @todo is this necessary? * Docs says that for any function proprs, the current plugin instance @@ -41,7 +43,7 @@ export function createCoqCodeView(){ const nodeViews = pluginState.activeNodeViews; // set up NodeView - const nodeView = new CodeBlockView(node, view, getPos, pluginState.schema); + const nodeView = new CodeBlockView(node, view, getPos, pluginState.schema, completions); nodeViews.add(nodeView); return nodeView; @@ -49,7 +51,7 @@ export function createCoqCodeView(){ } -const CoqCodePluginSpec:PluginSpec = { +const CoqCodePluginSpec = (completions: Array) : PluginSpec => { return { key: CODE_PLUGIN_KEY, state: { init(config, instance){ @@ -105,11 +107,18 @@ const CoqCodePluginSpec:PluginSpec = { }, props: { nodeViews: { - "coqcode" : createCoqCodeView() + "coqcode" : createCoqCodeView(completions) } } -}; +}}; -export const codePlugin = new ProsePlugin(CoqCodePluginSpec); +export const codePlugin = (completions: Array) => { + // Here we turn the waterproof completions into proper codemirror completions + // with template 'holes' + const cmCompletions = completions.map((value) => { + return snippetCompletion(value.template, value); + }); + return new ProsePlugin(CoqCodePluginSpec(cmCompletions)); +}; diff --git a/editor/src/waterproof-editor/codeview/nodeview.ts b/editor/src/waterproof-editor/codeview/nodeview.ts index c45fff4d..f2658d8b 100644 --- a/editor/src/waterproof-editor/codeview/nodeview.ts +++ b/editor/src/waterproof-editor/codeview/nodeview.ts @@ -9,7 +9,7 @@ import { import { Node, Schema } from "prosemirror-model" import { EditorView } from "prosemirror-view" import { customTheme } from "./color-scheme" -import { symbolCompletionSource, coqCompletionSource, tacticCompletionSource, renderIcon } from "../autocomplete"; +import { symbolCompletionSource, coqCompletionSource, renderIcon } from "../autocomplete"; import { EmbeddedCodeMirrorEditor } from "../embedded-codemirror"; import { linter, LintSource, Diagnostic, setDiagnosticsEffect, lintGutter } from "@codemirror/lint"; import { Debouncer } from "./debouncer"; @@ -36,7 +36,8 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { node: Node, view: EditorView, getPos: (() => number | undefined), - schema: Schema + schema: Schema, + completions: Array ) { super(node, view, getPos, schema); this._node = node; @@ -47,6 +48,25 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { this._readOnlyCompartment = new Compartment; this._diags = []; + const tacticCompletionSource: CompletionSource = function(context: CompletionContext): Promise { + return new Promise((resolve, _reject) => { + const before = context.matchBefore(/([^\s.\n\t\-+*])[^\s\n\t\-+*]*/gm); + const period = /\./gm + const line = context.state.doc.lineAt(context.pos); + const firstletter = line.text.match(/[a-zA-Z]/); + const lineBeforeCursor = line.text.slice(0, context.pos - line.from); + + if ((!context.explicit && !before) || period.test(lineBeforeCursor)) resolve(null); + resolve({ + // start completion instance from first letter of line + from: firstletter ? line.from + firstletter.index!: context.pos, + // non-null assertion operator "!" used to remove 'possibly null' error + options: completions, + validFor: /^[\t]*[^.]*/gm + }) + }); + } + // Shadow this._outerView for use in the next function. const outerView = this._outerView; diff --git a/editor/src/waterproof-editor/editor.ts b/editor/src/waterproof-editor/editor.ts index d44fa05f..ef3e513e 100644 --- a/editor/src/waterproof-editor/editor.ts +++ b/editor/src/waterproof-editor/editor.ts @@ -19,7 +19,6 @@ import { MENU_PLUGIN_KEY } from "./menubar/menubar"; import { PROGRESS_PLUGIN_KEY, progressBarPlugin } from "./progressBar"; import { FileTranslator } from "./translation"; import { createContextMenuHTML } from "./context-menu"; -import { initializeTacticCompletion } from "./autocomplete/tactics"; // CSS imports import "katex/dist/katex.min.css"; @@ -246,7 +245,7 @@ export class WaterproofEditor { mathPlugin, realMarkdownPlugin(this._schema), coqdocPlugin(this._schema), - codePlugin, + codePlugin(this._editorConfig.completions), progressBarPlugin, menuPlugin(this._schema, this._filef, this._userOS), keymap({ @@ -468,10 +467,6 @@ export class WaterproofEditor { this._view.dispatch(tr); } - public initTacticCompletion(useTacticsCoq: boolean) { - initializeTacticCompletion(useTacticsCoq); - } - public parseCoqDiagnostics(msg: DiagnosticMessage) { if (this._mapping === undefined || msg.version < this._mapping.version) return; diff --git a/editor/src/waterproof-editor/types.ts b/editor/src/waterproof-editor/types.ts index 696c2a6c..38f23ea5 100644 --- a/editor/src/waterproof-editor/types.ts +++ b/editor/src/waterproof-editor/types.ts @@ -28,7 +28,15 @@ export abstract class WaterproofMapping { abstract update: (step: Step) => DocChange | WrappingDocChange; } +export type WaterproofCompletion = { + label: string, + type: string, + detail: string, + template: string +} + export type WaterproofEditorConfig = { + completions: Array, api: WaterproofCallbacks, documentConstructor: (document: string) => WaterproofDocument, mapping: new (inputString: string, versionNum: number) => WaterproofMapping diff --git a/package.json b/package.json index 6c3123db..ef2df3f6 100644 --- a/package.json +++ b/package.json @@ -337,11 +337,6 @@ { "title": "Waterproof Use Case", "properties": { - "waterproof.standardCoqSyntax": { - "type": "boolean", - "default": false, - "description": "Enable Standard Coq syntax; if true, standard Coq syntax will be used instead of the Waterproof proof language." - }, "waterproof.enforceCorrectNonInputArea": { "type": "boolean", "default": true, diff --git a/shared/Messages.ts b/shared/Messages.ts index 9759509a..0322519a 100644 --- a/shared/Messages.ts +++ b/shared/Messages.ts @@ -46,7 +46,6 @@ export type Message = | MessageBase > | MessageBase | MessageBase - | MessageBase | MessageBase; /** @@ -75,7 +74,6 @@ export const enum MessageType { setData, setShowLineNumbers, setShowMenuItems, - syntax, teacher, } diff --git a/src/helpers/config-helper.ts b/src/helpers/config-helper.ts index b30bda72..98455bdb 100644 --- a/src/helpers/config-helper.ts +++ b/src/helpers/config-helper.ts @@ -47,11 +47,6 @@ export class WaterproofConfigHelper { return config().get("enforceCorrectNonInputArea") as boolean; } - /** `waterproof.standardCoqSyntax` */ - static get standardCoqSyntax() { - return config().get("standardCoqSyntax") as boolean; - } - /** `waterproof.eager_diagnostics` */ static get eager_diagnostics() { return config().get("eager_diagnostics") as boolean; diff --git a/src/pm-editor/pmWebview.ts b/src/pm-editor/pmWebview.ts index 050e6582..28e85376 100644 --- a/src/pm-editor/pmWebview.ts +++ b/src/pm-editor/pmWebview.ts @@ -186,10 +186,6 @@ export class ProseMirrorWebview extends EventEmitter { this.updateTeacherMode(); } - if (e.affectsConfiguration("waterproof.standardCoqSyntax")) { - this.updateSyntaxMode(); - } - if (e.affectsConfiguration("waterproof.enforceCorrectNonInputArea")) { this._enforceCorrectNonInputArea = WaterproofConfigHelper.enforceCorrectNonInputArea; } @@ -316,14 +312,6 @@ export class ProseMirrorWebview extends EventEmitter { }, true); } - /** Toggle the syntax mode*/ - private updateSyntaxMode() { - this.postMessage({ - type: MessageType.syntax, - body: WaterproofConfigHelper.standardCoqSyntax - }, true); - } - /** Apply new doc changes to the underlying file */ private applyChangeToWorkspace(change: DocChange, edit: WorkspaceEdit) { if (change.startInFile === change.endInFile) { From a96d2608dd20a6cf82c661e71f100fb7e64ff7b9 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 6 Aug 2025 17:23:35 +0200 Subject: [PATCH 71/93] Special-casing snippet communication --- .../waterproof-editor/codeview/nodeview.ts | 156 +++++++----------- 1 file changed, 57 insertions(+), 99 deletions(-) diff --git a/editor/src/waterproof-editor/codeview/nodeview.ts b/editor/src/waterproof-editor/codeview/nodeview.ts index c45fff4d..e6a1697e 100644 --- a/editor/src/waterproof-editor/codeview/nodeview.ts +++ b/editor/src/waterproof-editor/codeview/nodeview.ts @@ -16,6 +16,7 @@ import { Debouncer } from "./debouncer"; import { INPUT_AREA_PLUGIN_KEY } from "../inputArea"; + /** * Export CodeBlockView class that implements the custom codeblock nodeview. * Corresponds with the example as can be found here: @@ -170,12 +171,12 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { return true; } - public handleSnippet(template: string, posFrom: number, posTo: number) { + public handleSnippet(template: string, posFrom: number, posTo: number, completion? : Completion | undefined) { this._codemirror?.focus(); snippet(template)({ state: this._codemirror!.state, dispatch: this._codemirror!.dispatch - }, null, posFrom, posTo); + }, completion ?? null, posFrom, posTo); } private lintingFunction: LintSource = (_view: CodeMirror): readonly Diagnostic[] => { @@ -242,9 +243,11 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { */ public addCoqError(from: number, to: number, message: string, severity: number) { const severityString = severityToString(severity); - const errorsCount = this._diags.filter(diag => diag.from === from && diag.to === to && diag.severity === "error").length; - //all diags have the copy action - const actions = [{ + console.log(this._dynamicCompletions); + + console.log(`Adding coq error: ${message} at ${from}-${to} with severity ${severityString}`); + // By default, there is the copy action + let actions = [{ name: "Copy 📋", apply: (view: CodeMirror, from: number, _to: number) => { // give focus to this current codeblock instante to ensure it updates @@ -253,109 +256,64 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { this.showCopyNotification(from); } }]; - - if (severityString !== "error") { - if (errorsCount > 0) { - actions.push({ - name: "Replace", - apply:(view: CodeMirror, from: number, to: number) => { - // give focus to this current codeblock instante to ensure it updates - this._codemirror?.focus(); - const trimmedMessage = message.trim(); - const toInsert = trimmedMessage; - view.dispatch({ - changes: { - from:from, - to:to, - insert: toInsert - }, - selection: { anchor: from + toInsert.length } - }); - this.forceUpdateLinting(); - } - }); - } else { - actions.push({ - name: "Insert ↓", - apply:(view: CodeMirror, from: number, to: number) => { - // give focus to this current codeblock instante to ensure it updates - this._codemirror?.focus(); - const textAtErrorLine = view.state.doc.lineAt(from).text; - const idents = textAtErrorLine.match(/^(\s*)/g)?.[0] ?? ""; - const trimmedMessage = message.trim(); - const toInsert = "\n".concat(idents, trimmedMessage); - view.dispatch({ - changes: { - from: to, to, - insert: toInsert - }, - selection: {anchor: to + toInsert.length} - }); - } - }); - } + let trimmedMessage : string = ""; + if (message.startsWith("Hint, replace with: ")) { + trimmedMessage = message.trim().replace("Hint, replace with: ", "").replace(/\.\${}$/, ".").replaceAll(/\$\{.*?\}/g,"...") + actions = [({ + name: "Replace ↩️", + apply:(view: CodeMirror, from: number, to: number) => { + // give focus to this current codeblock instante to ensure it updates + this._codemirror?.focus(); + const toInsert = message.trim().replace("Hint, replace with: ", ""); + view.dispatch({ + changes: { + from:from, + to:to, + insert: "" + }, + selection: { anchor: from } + }); + this.handleSnippet(toInsert, from, from); + this.forceUpdateLinting(); + } + })]; + } else if (message.startsWith("Hint, insert: ")) { + trimmedMessage = message.trim().replace("Hint, insert: ", "").replace(/\.\${}$/, ".").replaceAll(/\$\{.*?\}/g,"..."); + actions = [({ + name: "Insert ⤵️", + apply:(view: CodeMirror, from: number, to: number) => { + // give focus to this current codeblock instante to ensure it updates + this._codemirror?.focus(); + const toInsert = "\n" + message.trim().replace("Hint, insert: ", ""); + this.handleSnippet(toInsert, to, to); + } + })]; + } else if (message.startsWith("Remove this line")) { + actions = [({ + name: "Delete 🗑️", + apply: (view: CodeMirror, from: number, to: number) => { + // give focus to this current codeblock instante to ensure it updates + this._codemirror?.focus(); + view.dispatch({ + changes: { + from: from, + to: to, + insert: "" + }, + selection: { anchor: from } + }); + } + })]; } this._diags.push({ from:from, to:to, - message: message, + message: (trimmedMessage === "" ? message : trimmedMessage), severity: severityString, actions, }); - //only when the first error is added, the other diagnostics are updated accordingly - if (severityString === "error" && errorsCount===0) { - this.updateDiagnostics(from, to, message); - } - } - - private updateDiagnostics(from:number, to:number, _message :string) { - const diagUnchanged = this._diags.filter(diag => diag.from !== from || diag.to !== to); - const diagnosticsToUpdate = this._diags.filter(diag => diag.from === from && diag.to === to); - this.clearCoqErrors(); - this._diags = diagUnchanged; - for (const diag of diagnosticsToUpdate) { - const actions = [{ - name: "Copy 📋", - apply: (view: CodeMirror, from: number, _to: number) => { - // give focus to this current codeblock instante to ensure it updates - this._codemirror?.focus(); - navigator.clipboard.writeText(diag.message); - this.showCopyNotification(from); - } - }]; - - if (diag.severity !== "error"){ - actions.push({ - name: "Replace", - apply:(view: CodeMirror, from: number, to: number) => { - // give focus to this current codeblock instante to ensure it updates - this._codemirror?.focus(); - const trimmedMessage = diag.message.trim(); - const toInsert = trimmedMessage; - view.dispatch({ - changes: { - from:from, - to:to, - insert: toInsert - }, - selection: { anchor: from + toInsert.length } - }); - this.forceUpdateLinting(); - } - }); - } - - this._diags.push({ - from: diag.from, - to: diag.to, - message: diag.message, - severity: diag.severity, - actions - }); - } - // Trigger the linter update to refresh diagnostics display this.debouncer.call(); } From 94b8e65f9fde268b303dab77638941b287d66b9a Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Wed, 6 Aug 2025 17:34:15 +0200 Subject: [PATCH 72/93] Remove definition panel --- src/extension.ts | 2 - src/webviews/coqWebview.ts | 7 - src/webviews/sidePanel.ts | 7 - .../standardviews/expandDefinition.ts | 44 ---- views/expandDefinition/expandDefinition.tsx | 244 ------------------ views/expandDefinition/index.tsx | 16 -- 6 files changed, 320 deletions(-) delete mode 100644 src/webviews/standardviews/expandDefinition.ts delete mode 100644 views/expandDefinition/expandDefinition.tsx delete mode 100644 views/expandDefinition/index.tsx diff --git a/src/extension.ts b/src/extension.ts index 3c08e398..e433b5b0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -22,7 +22,6 @@ import { GoalsPanel } from "./webviews/goalviews/goalsPanel"; import { SidePanelProvider, addSidePanel } from "./webviews/sidePanel"; import { Search } from "./webviews/standardviews/search"; import { Help } from "./webviews/standardviews/help"; -import { ExpandDefinition } from "./webviews/standardviews/expandDefinition"; import { ExecutePanel } from "./webviews/standardviews/execute"; import { SymbolsPanel } from "./webviews/standardviews/symbols"; import { TacticsPanel } from "./webviews/standardviews/tactics"; @@ -155,7 +154,6 @@ export class Waterproof implements Disposable { this.webviewManager.addToolWebview("symbols", new SymbolsPanel(this.context.extensionUri)); this.webviewManager.addToolWebview("search", new Search(this.context.extensionUri)); this.webviewManager.addToolWebview("help", new Help(this.context.extensionUri)); - this.webviewManager.addToolWebview("expandDefinition", new ExpandDefinition(this.context.extensionUri)); const executorPanel = new ExecutePanel(this.context.extensionUri); this.webviewManager.addToolWebview("execute", executorPanel); this.webviewManager.addToolWebview("tactics", new TacticsPanel(this.context.extensionUri)); diff --git a/src/webviews/coqWebview.ts b/src/webviews/coqWebview.ts index 3bc8bab7..6302231c 100644 --- a/src/webviews/coqWebview.ts +++ b/src/webviews/coqWebview.ts @@ -89,13 +89,6 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { { preserveFocus: true, viewColumn: ViewColumn.Two }, webviewOpts ); - } else if (this.name == "expandDefinition") { - this._panel = window.createWebviewPanel( - this.name, - "Expand definition", - { preserveFocus: true, viewColumn: ViewColumn.Two }, - webviewOpts - ); } else { this._panel = window.createWebviewPanel( this.name, diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index b7a7a928..d7246a8c 100644 --- a/src/webviews/sidePanel.ts +++ b/src/webviews/sidePanel.ts @@ -117,7 +117,6 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { - @@ -132,7 +131,6 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { const executeButton = document.getElementById('execute'); const helpButton = document.getElementById('help'); const searchButton = document.getElementById('search'); - const expandButton = document.getElementById('expandDefinition'); const symbolsButton = document.getElementById('symbols'); const tacticsButton = document.getElementById('tactics'); @@ -161,11 +159,6 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { vscode.postMessage({ command: 'search' }); }); - expandButton.addEventListener('click', () => { - // Handle expandDefinition button click event by sending a message to vscode - vscode.postMessage({ command: 'expandDefinition' }); - }); - symbolsButton.addEventListener('click', () => { // Handle symbols button click event by sending a message to vscode vscode.postMessage({ command: 'symbols' }); diff --git a/src/webviews/standardviews/expandDefinition.ts b/src/webviews/standardviews/expandDefinition.ts deleted file mode 100644 index bf471155..00000000 --- a/src/webviews/standardviews/expandDefinition.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - Uri, -} from "vscode"; -import { CoqWebview, WebviewEvents, WebviewState } from "../coqWebview"; -import { MessageType } from "../../../shared"; -import { IExecutor } from "../../components"; - -export class ExpandDefinition extends CoqWebview implements IExecutor { - // Initialize the data for the results - private data: string[] = ['no results']; - - constructor(extensionUri: Uri) { - // Initialize the expand definition panel with the extension Uri and the webview name - super(extensionUri, "expandDefinition", true); - this.readyPanel(); - // Set up an event listener for WebviewEvents.change event - this.on(WebviewEvents.change, (_e) => { - switch (this.state) { // Check the state of the webview - // If the webview is open - case WebviewState.open: - break; - // If the webview is ready - case WebviewState.ready: - break; - // If the webview is visible - case WebviewState.visible: - break; - // If the webview is closed - case WebviewState.closed: - // Get panel ready again - this.readyPanel() - break; - } - }); - } - - setResults(results: string[]): void { - // Set the data property to the provided results - this.data = results; - // Send a postMessage to the webview with the MessageType.setData and the data - this.postMessage({ type: MessageType.setData, body: this.data }); - } - -} diff --git a/views/expandDefinition/expandDefinition.tsx b/views/expandDefinition/expandDefinition.tsx deleted file mode 100644 index 2ceb4d41..00000000 --- a/views/expandDefinition/expandDefinition.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { VSCodeButton, VSCodeDivider } from '@vscode/webview-ui-toolkit/react'; -import React, { useEffect, useRef, useState } from "react"; - -import { Message, MessageType } from "../../shared"; -import "../styles/execute.css"; -import { Messages } from '../goals/Messages'; - -const date = new Date(); -const vscode = acquireVsCodeApi(); - -export function ExpandDefinition() { - // create the states for the components that need them - const [inputText1, setInputText1] = useState(""); - const [cursor1, setCursor1] = useState(0) - const [inputText2, setInputText2] = useState(""); - const [cursor2, setCursor2] = useState(0) - const [info, setInfo] = useState([""]); - const [current, setCurrent] = useState(""); - const [isLoading, setIsLoading] = useState(false); - - const input1Ref = useRef(null); - const input2Ref = useRef(null); - - //on changes in component useEffect is run - useEffect(() => { - //handling a message - const handleMessage = (msg: Message) => { - switch (msg.type){ - // insert message which is either a symbol or tactic - case MessageType.insert: - insertText(msg.body.symbolUnicode); - break; - // receiving info of the executed commands - case MessageType.setData: - //@ts-expect-error FIXME: setInfo expects string[] - // in theory setData can also contain GoalAnswer - setInfo(msg.body); - setIsLoading(false); - break; - } - }; - - const callback = (ev: MessageEvent) => {handleMessage(ev.data);}; - - //adding event listener to component - window.addEventListener('message', callback); - - return () => { - // on dismount of component the eventlistener is removed - window.removeEventListener('message', callback); - }; - }, [ cursor1, cursor2, inputText1, inputText2, current, info ]); - - // the cursor position is updated together with the current textarea - const setCursorPos = (textarea, cur) => { - setCurrent(cur); - if (textarea) { - const startPos = textarea.selectionStart; - const val = textarea.value; - switch (cur) { - // first input area of expand def in context - case "input1": - setCursor1(startPos); - setInputText1(val); - break; - // second input area of expand def in context - case "input2": - setCursor2(startPos); - setInputText2(val); - break; - } - } - } - - //inserting text at the previous cursor position - const insertText = (textToInsert: string) => { - let textarea = null; - let cursor = 0; - let value = ""; - switch (current) { - // first input area of expand def in context - case "input1": - textarea = input1Ref.current; - cursor = cursor1; - value = inputText1; - break; - // second input area of expand def in context - case "input2": - textarea = input2Ref.current; - cursor = cursor2; - value = inputText2; - break; - default: - break; - } - if (textarea) { - const newValue = value.substring(0, cursor) +textToInsert +value.substring(cursor, value.length); - (textarea as HTMLTextAreaElement).value = newValue; - (textarea as HTMLTextAreaElement).setSelectionRange(cursor+textToInsert.length, cursor+textToInsert.length); - (textarea as HTMLTextAreaElement).focus(); - //handles the insertion in the current text area - switch (current) { - // first input area of expand def in context - case "input1": - setInputText1(newValue); - setCursor1(cursor +textToInsert.length); - break; - // first input area of expand def in context - case "input2": - setInputText2(newValue); - setCursor2(cursor +textToInsert.length); - break; - } - } - }; - - //button press execute - const handleExecute = () => { - //Send the message the execute button was pressed - vscode.postMessage({time: date.getTime(), - type: MessageType.command, - body: `_internal_ Expand the definition of ${inputText1} in (${inputText2.replace(/(\.\s*|\s*)$/s, '')}).`}) - setIsLoading(true); - }; - - //execute by pressing search + enter in the first input field - const handleKeyDown = (event) => { - if (event.key === 'Enter' && event.shiftKey) { - // Prevents adding a new line in the textarea - event.preventDefault(); - } - setCursorPos(input1Ref.current, "input1"); - }; - - //execute by pressing search + enter in the second input field - const handleKeyDown2 = (event) => { - if (event.key === 'Enter' && event.shiftKey) { - // Handle Shift + Enter key press logic here - // Prevent adding a new line in the textarea - event.preventDefault(); - } - }; - - //handle change in the first input of execute - const handleChange = (_event) => { - setCursorPos(input1Ref.current, "input1"); - }; - - //handle change in the second input of execute - const handleChange2 = (_event) => { - setCursorPos(input2Ref.current, "input2"); - }; - - //handle click in the first input of execute - const onClick1 = () => { - setCursorPos(input1Ref.current, "input1"); - } - - //handle click in the second input of execute - const onClick2 =() => { - setCursorPos(input2Ref.current, "input2"); - } - - return ( -
-
- - - - -
Expand -
-