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: diff --git a/.gitignore b/.gitignore index 2bb309af..b8514427 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,10 @@ node_modules/ editor_output .DS_store .vscode-test +.vscode-test-web test_out/ .ui-test/ __tests_output__/ **/*.cache ExampleFiles/ +/cypress/screenshots/ \ No newline at end of file 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/.vscodeignore b/.vscodeignore index 01e470d8..51cebbdb 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -21,4 +21,6 @@ tsconfig-base.json tsconfig.json .github/ ExampleFiles/ -developer-resources/ \ No newline at end of file +developer-resources/ +scripts/ +docs/ diff --git a/README.md b/README.md index 7309afbd..01ef11f6 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. @@ -104,9 +108,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) @@ -132,3 +134,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 a [Markdown](/docs/tactics-sheet.md) version of the tactics sheet. 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 5240a3b7..91071422 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"); @@ -42,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/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, diff --git a/cypress/e2e/with-mock-messages.cy.ts b/cypress/e2e/with-mock-messages.cy.ts index 85e9e721..963409ba 100644 --- a/cypress/e2e/with-mock-messages.cy.ts +++ b/cypress/e2e/with-mock-messages.cy.ts @@ -1,9 +1,11 @@ /// -import { QedStatus } from "../../shared"; +import { InputAreaStatus } from "../../shared"; import { Message, MessageType } from "../../shared/Messages"; const edits = []; +const diagMessage = " = 6\n : nat"; + const messages: Array = [ { "type": MessageType.diagnostics, @@ -29,7 +31,7 @@ const messages: Array = [ { "type": MessageType.qedStatus, "body": [ - QedStatus.InvalidInputArea + InputAreaStatus.Invalid ] }, { @@ -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 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/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 diff --git a/developer-resources/messages.md b/developer-resources/messages.md index 4e612e11..fbb8a696 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. @@ -165,7 +178,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/docs/tactics-sheet.md b/docs/tactics-sheet.md new file mode 100644 index 00000000..e216e4ea --- /dev/null +++ b/docs/tactics-sheet.md @@ -0,0 +1,672 @@ +# Waterproof Tactics Sheet + +## `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. +``` 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 7cc1e996..828d327f 100644 --- a/editor/src/index.ts +++ b/editor/src/index.ts @@ -1,9 +1,10 @@ -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"; +// 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. @@ -13,25 +14,16 @@ 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 = { + completions: tactics, 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) => { @@ -74,17 +89,13 @@ window.onload = () => { editor.insertSymbol(symbolUnicode, symbolLatex); } break; } + case MessageType.refreshDocument: + editor.refreshDocument(msg.body.value, msg.body.version); + break; case MessageType.setAutocomplete: // Handle autocompletion - { const state = editor.state; - if (!state) break; - const completions: Completion[] = msg.body; - // Apply autocomplete to all coq cells - COQ_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); @@ -93,6 +104,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; @@ -110,9 +123,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/kroqed-editor/autocomplete/tactics.ts b/editor/src/kroqed-editor/autocomplete/tactics.ts deleted file mode 100644 index 05e8ca82..00000000 --- a/editor/src/kroqed-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/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 81% rename from editor/src/kroqed-editor/autocomplete/index.ts rename to editor/src/waterproof-editor/autocomplete/index.ts index 95d055bf..9e645ff6 100644 --- a/editor/src/kroqed-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/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/codeview/coqcodeplugin.ts b/editor/src/waterproof-editor/codeview/code-plugin.ts similarity index 75% rename from editor/src/kroqed-editor/codeview/coqcodeplugin.ts rename to editor/src/waterproof-editor/codeview/code-plugin.ts index ec57be4f..881d047b 100644 --- a/editor/src/kroqed-editor/codeview/coqcodeplugin.ts +++ b/editor/src/waterproof-editor/codeview/code-plugin.ts @@ -9,10 +9,12 @@ 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"; //////////////////////////////////////////////////////////// -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,24 +26,24 @@ 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`. * @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 * 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; // 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,8 +51,8 @@ export function createCoqCodeView(){ } -const CoqCodePluginSpec:PluginSpec = { - key: COQ_CODE_PLUGIN_KEY, +const CoqCodePluginSpec = (completions: Array) : PluginSpec => { return { + key: CODE_PLUGIN_KEY, state: { init(config, instance){ return { @@ -70,7 +72,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 +80,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; @@ -105,11 +107,18 @@ const CoqCodePluginSpec:PluginSpec = { }, props: { nodeViews: { - "coqcode" : createCoqCodeView() + "coqcode" : createCoqCodeView(completions) } } -}; +}}; -export const coqCodePlugin = 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/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 72% rename from editor/src/kroqed-editor/codeview/coqTheme.json rename to editor/src/waterproof-editor/codeview/coqTheme.json index dfa974f3..663a4dcf 100644 --- a/editor/src/kroqed-editor/codeview/coqTheme.json +++ b/editor/src/waterproof-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"}, @@ -66,6 +64,10 @@ "color": "var(--vscode-menu-foreground)", "backgroundColor": "var(--vscode-menu-background)" }, + ".cm-tooltip-lint": { + "display": "flex", + "flexFlow": "column" + }, ".cm-tooltip .cm-tooltip-arrow:before": { "borderTopColor": "transparent", "borderBottomColor": "transparent" @@ -85,4 +87,4 @@ "width": "1.5em", "opacity": "1.0" } -} +} \ No newline at end of file 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 70% rename from editor/src/kroqed-editor/codeview/nodeview.ts rename to editor/src/waterproof-editor/codeview/nodeview.ts index 2c530009..b76d20e5 100644 --- a/editor/src/kroqed-editor/codeview/nodeview.ts +++ b/editor/src/waterproof-editor/codeview/nodeview.ts @@ -9,19 +9,19 @@ 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 } from "@codemirror/lint"; +import { linter, LintSource, Diagnostic, setDiagnosticsEffect, lintGutter } from "@codemirror/lint"; 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: * https://prosemirror.net/examples/codemirror/ */ - export class CodeBlockView extends EmbeddedCodeMirrorEditor { dom: HTMLElement; @@ -37,7 +37,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; @@ -48,6 +49,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; @@ -77,11 +97,18 @@ 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()] : []; + 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), @@ -117,14 +144,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; @@ -155,12 +180,23 @@ export class CodeBlockView extends EmbeddedCodeMirrorEditor { this.handleNewComplete([]); } - public handleSnippet(template: string, posFrom: number, posTo: number) { + 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, 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[] => { @@ -227,10 +263,10 @@ 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 = [{ - name: "Copy 📋", + + // By default, there is the copy action + let actions = [{ + name: "📋", apply: (view: CodeMirror, from: number, _to: number) => { // give focus to this current codeblock instante to ensure it updates this._codemirror?.focus(); @@ -238,109 +274,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(); } @@ -403,4 +394,4 @@ const severityToString = (sv: number) => { default: return "error"; } -} +} \ No newline at end of file diff --git a/editor/src/kroqed-editor/commands/command-helpers.ts b/editor/src/waterproof-editor/commands/command-helpers.ts similarity index 99% rename from editor/src/kroqed-editor/commands/command-helpers.ts rename to editor/src/waterproof-editor/commands/command-helpers.ts index e5b33668..b1b682c6 100644 --- a/editor/src/kroqed-editor/commands/command-helpers.ts +++ b/editor/src/waterproof-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 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 84% rename from editor/src/kroqed-editor/editor.ts rename to editor/src/waterproof-editor/editor.ts index 9e62b6fb..360dbd75 100644 --- a/editor/src/kroqed-editor/editor.ts +++ b/editor/src/waterproof-editor/editor.ts @@ -6,21 +6,19 @@ 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"; 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"; @@ -33,7 +31,8 @@ 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"; +import { Completion } from "@codemirror/autocomplete"; /** 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; @@ -114,7 +113,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(); @@ -138,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 */ @@ -150,6 +148,40 @@ export class WaterproofEditor { this._editorConfig.api.editorReady(); } + refreshDocument(content: string, version: number) { + 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); + + 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); + const newState = EditorState.create({ + doc: proseDoc, + plugins: this._view.state.plugins, + schema: this._schema + }); + this._view.updateState(newState); + + this.sendLineNumbers(); + } + + get state(): EditorState | undefined { return this._view?.state; } @@ -168,7 +200,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); @@ -247,7 +279,7 @@ export class WaterproofEditor { mathPlugin, realMarkdownPlugin(this._schema), coqdocPlugin(this._schema), - coqCodePlugin, + codePlugin(this._editorConfig.completions), progressBarPlugin, menuPlugin(this._schema, this._filef, this._userOS), keymap({ @@ -278,7 +310,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 { @@ -313,10 +345,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)); } @@ -328,12 +360,22 @@ 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; - 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); } @@ -373,14 +415,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; } @@ -410,11 +452,19 @@ 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(); } + 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) { @@ -425,13 +475,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,!isTeacher); + trans.setMeta(INPUT_AREA_PLUGIN_KEY, {teacher: isTeacher}); this._view.dispatch(trans); } @@ -443,7 +493,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; @@ -451,10 +501,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; @@ -463,7 +509,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(); @@ -527,4 +573,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 59% rename from editor/src/kroqed-editor/inputArea.ts rename to editor/src/waterproof-editor/inputArea.ts index 3d885cb8..72cdadca 100644 --- a/editor/src/kroqed-editor/inputArea.ts +++ b/editor/src/waterproof-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/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 79% rename from editor/src/kroqed-editor/menubar/menubar.ts rename to editor/src/waterproof-editor/menubar/menubar.ts index 5ec77afc..2efbf30b 100644 --- a/editor/src/kroqed-editor/menubar/menubar.ts +++ b/editor/src/waterproof-editor/menubar/menubar.ts @@ -11,6 +11,7 @@ import { FileFormat } from "../../../../shared"; type MenuEntry = { dom: HTMLElement; teacherModeOnly: boolean; + showByDefault: boolean; cmd: Command; }; @@ -19,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): 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. @@ -31,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}; + return {cmd, dom: menuItem, teacherModeOnly: buttonSettings?.teacherModeOnly ?? false, showByDefault: buttonSettings?.showByDefault ?? false}; } /** @@ -58,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) => { @@ -74,18 +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) { + + + update() { // Whether we are currently in teacher mode. - const inTeacherMode = MENU_PLUGIN_KEY.getState(view.state)?.teacher; + 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, @@ -96,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.dom.style.display = "none"; - } } } @@ -125,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 @@ -163,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. @@ -178,7 +202,7 @@ function createDefaultMenu(schema: Schema, outerView: EditorView, filef: FileFor * Interface describing the state of the menu plugin */ interface IMenuPluginState { - teacher: boolean; + showMenuItems: boolean; } /** @@ -195,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) @@ -205,29 +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... - let teacherState = !tr.getMeta(INPUT_AREA_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, - }; - }, + showMenuItems: tr.getMeta(MENU_PLUGIN_KEY) ?? value.showMenuItems, + }; + } } - }) + }); } 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 97% rename from editor/src/kroqed-editor/progressBar.ts rename to editor/src/waterproof-editor/progressBar.ts index 531bf732..2c025411 100644 --- a/editor/src/kroqed-editor/progressBar.ts +++ b/editor/src/waterproof-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(); + } }; } }; 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 87% rename from editor/src/kroqed-editor/styles/index.ts rename to editor/src/waterproof-editor/styles/index.ts index 9fac4fbb..4f5242eb 100644 --- a/editor/src/kroqed-editor/styles/index.ts +++ b/editor/src/waterproof-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/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 99% rename from editor/src/kroqed-editor/styles/menubar.css rename to editor/src/waterproof-editor/styles/menubar.css index 93de205f..e6363f54 100644 --- a/editor/src/kroqed-editor/styles/menubar.css +++ b/editor/src/waterproof-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/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/waterproof-editor/styles/waterproof.css b/editor/src/waterproof-editor/styles/waterproof.css new file mode 100644 index 00000000..8ec2c24e --- /dev/null +++ b/editor/src/waterproof-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 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..38f23ea5 --- /dev/null +++ b/editor/src/waterproof-editor/types.ts @@ -0,0 +1,43 @@ +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 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 +} \ 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/esbuild.mjs b/esbuild.mjs index cd1ef046..2a8c35ce 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"), @@ -107,12 +107,10 @@ 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"); var searchView = viewBuild("./views/search/index.tsx"); -var expandDefinitionView = viewBuild("./views/expandDefinition/index.tsx"); var symbolsView = viewBuild("./views/symbols/index.tsx"); var executeView = viewBuild("./views/execute/index.tsx"); var tacticsView = viewBuild("./views/tactics/index.tsx"); 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: [ 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/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 diff --git a/package-lock.json b/package-lock.json index 88f94552..e06a727c 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": { "@eslint/js": "^9.18.0", @@ -5480,7 +5482,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" } @@ -12379,6 +12380,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", diff --git a/package.json b/package.json index 8de14c8d..286a6c13 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "displayName": "Waterproof", "description": "Waterproof for VSC", "version": "0.9.18", - "requiredCoqLspVersion": "==0.2.2", - "requiredCoqWaterproofVersion": "==2.2.0", + "requiredCoqLspVersion": ">=0.2.2", + "requiredCoqWaterproofVersion": ">=3.0.0", "contributors": [ "Collin Harcarik ", "Milo Goolkate ", @@ -321,17 +321,22 @@ "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!" + }, + "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." } } }, { "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, @@ -393,6 +398,21 @@ "Use jsCoq's Pp rich layout printer", "Coq Layout Engine (experimental)" ] + }, + "waterproof.visibilityOfHypotheses" : { + "type": "string", + "default": "none", + "enum": [ + "all", + "limited", + "none" + ], + "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." } } }, @@ -514,6 +534,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", @@ -534,7 +555,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" @@ -546,12 +568,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": "npx lezer-generator ./editor/src/kroqed-editor/codeview/lang-pack/syntax.grammar -o ./editor/src/kroqed-editor/codeview/lang-pack/syntax.ts", + "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" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..285f44d4 --- /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 +```bash +pandoc tactics-sheet.md -o tactics-sheet.pdf --pdf-engine=lualatex -V 'monofont:DejaVu Sans Mono' -V 'mainfont:DejaVu Sans' +``` diff --git a/scripts/createmd.js b/scripts/createmd.js new file mode 100644 index 00000000..44ef99dd --- /dev/null +++ b/scripts/createmd.js @@ -0,0 +1,16 @@ +const { writeFileSync } = require("fs"); + +// Open the tactics file +const tactics = require("../shared/completions/tactics.json"); +const outputLocation = "../docs/tactics-sheet.md"; + +let mdContent = `# Waterproof Tactics Sheet\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(outputLocation, mdContent); 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 5fb0bf00..4de0eb4a 100644 --- a/shared/Messages.ts +++ b/shared/Messages.ts @@ -2,9 +2,9 @@ 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, 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) * @@ -34,17 +34,19 @@ export type Message = | MessageBase | MessageBase | MessageBase + | MessageBase | MessageBase | MessageBase | MessageBase - | MessageBase + | MessageBase | MessageBase - | MessageBase + | MessageBase, visibility?: HypVisibility }> + | MessageBase[]}> | MessageBase | MessageBase | MessageBase > | MessageBase - | MessageBase + | MessageBase | MessageBase; /** @@ -61,18 +63,21 @@ export const enum MessageType { editorReady, errorGoals, init, + refreshDocument, insert, lineNumbers, progress, qedStatus, ready, renderGoals, + renderGoalsList, response, setAutocomplete, setData, setShowLineNumbers, - syntax, + setShowMenuItems, teacher, + flash, } export const enum HistoryChangeType { diff --git a/shared/completions/README.md b/shared/completions/README.md index c250b134..778f3ae6 100644 --- a/shared/completions/README.md +++ b/shared/completions/README.md @@ -7,6 +7,7 @@ These are used in the symbols panel: - 3 = arrows - 4 = number systems - 5 = super and subscripts +- 6 = Calligraphic upper case letters (mathcal-style) - 99 = Not supposed to show up in symbols panel. ## Syntax to create a snippet template: diff --git a/shared/completions/symbols.json b/shared/completions/symbols.json index ffe196a7..826de72c 100644 --- a/shared/completions/symbols.json +++ b/shared/completions/symbols.json @@ -317,6 +317,12 @@ "apply": "∈", "symbolPanelCategory": 2 }, + { + "label": "\\not-in", + "type": "symbol", + "apply": "∉", + "symbolPanelCategory": 2 + }, { "label": "\\QED", "type": "symbol", @@ -359,12 +365,36 @@ "apply": "∪", "symbolPanelCategory": 2 }, + { + "label": "\\bigintersection", + "type": "symbol", + "apply": "⋂", + "symbolPanelCategory": 2 + }, + { + "label": "\\bigunion", + "type": "symbol", + "apply": "⋃", + "symbolPanelCategory": 2 + }, { "label": "\\empty-set", "type": "symbol", "apply": "∅", "symbolPanelCategory": 2 }, + { + "label": "\\subset", + "type": "symbol", + "apply": "⊂", + "symbolPanelCategory": 2 + }, + { + "label": "\\subseteq", + "type": "symbol", + "apply": "⊆", + "symbolPanelCategory": 2 + }, { "label": "\\less-or-equal", "type": "symbol", @@ -587,6 +617,18 @@ "apply": "⁹", "symbolPanelCategory": 5 }, + { + "label": "\\^-", + "type": "symbol", + "apply": "⁻", + "symbolPanelCategory": 5 + }, + { + "label": "\\inv", + "type": "symbol", + "apply": "⁻¹", + "symbolPanelCategory": 5 + }, { "label": "\\^0", "type": "symbol", @@ -652,5 +694,161 @@ "type": "symbol", "apply": "₀", "symbolPanelCategory": 5 + }, + { + "label": "\\McA", + "type": "symbol", + "apply": "𝒜", + "symbolPanelCategory": 6 + }, + { + "label": "\\McB", + "type": "symbol", + "apply": "ℬ", + "symbolPanelCategory": 6 + }, + { + "label": "\\McC", + "type": "symbol", + "apply": "𝒞", + "symbolPanelCategory": 6 + }, + { + "label": "\\McD", + "type": "symbol", + "apply": "𝒟", + "symbolPanelCategory": 6 + }, + { + "label": "\\McE", + "type": "symbol", + "apply": "ℰ", + "symbolPanelCategory": 6 + }, + { + "label": "\\McF", + "type": "symbol", + "apply": "ℱ", + "symbolPanelCategory": 6 + }, + { + "label": "\\McG", + "type": "symbol", + "apply": "𝒢", + "symbolPanelCategory": 6 + }, + { + "label": "\\McH", + "type": "symbol", + "apply": "ℋ", + "symbolPanelCategory": 6 + }, + { + "label": "\\McI", + "type": "symbol", + "apply": "ℐ", + "symbolPanelCategory": 6 + }, + { + "label": "\\McJ", + "type": "symbol", + "apply": "𝒥", + "symbolPanelCategory": 6 + }, + { + "label": "\\McK", + "type": "symbol", + "apply": "𝒦", + "symbolPanelCategory": 6 + }, + { + "label": "\\McL", + "type": "symbol", + "apply": "ℒ", + "symbolPanelCategory": 6 + }, + { + "label": "\\McM", + "type": "symbol", + "apply": "ℳ", + "symbolPanelCategory": 6 + }, + { + "label": "\\McN", + "type": "symbol", + "apply": "𝒩", + "symbolPanelCategory": 6 + }, + { + "label": "\\McO", + "type": "symbol", + "apply": "𝒪", + "symbolPanelCategory": 6 + }, + { + "label": "\\McP", + "type": "symbol", + "apply": "𝒫", + "symbolPanelCategory": 6 + }, + { + "label": "\\McQ", + "type": "symbol", + "apply": "𝒬", + "symbolPanelCategory": 6 + }, + { + "label": "\\McR", + "type": "symbol", + "apply": "ℛ", + "symbolPanelCategory": 6 + }, + { + "label": "\\McS", + "type": "symbol", + "apply": "𝒮", + "symbolPanelCategory": 6 + }, + { + "label": "\\McT", + "type": "symbol", + "apply": "𝒯", + "symbolPanelCategory": 6 + }, + { + "label": "\\McU", + "type": "symbol", + "apply": "𝒰", + "symbolPanelCategory": 6 + }, + { + "label": "\\McV", + "type": "symbol", + "apply": "𝒱", + "symbolPanelCategory": 6 + }, + { + "label": "\\McW", + "type": "symbol", + "apply": "𝒲", + "symbolPanelCategory": 6 + }, + { + "label": "\\McX", + "type": "symbol", + "apply": "𝒳", + "symbolPanelCategory": 6 + }, + { + "label": "\\McY", + "type": "symbol", + "apply": "𝒴", + "symbolPanelCategory": 6 + }, + { + "label": "\\McZ", + "type": "symbol", + "apply": "𝒵", + "symbolPanelCategory": 6 } ] diff --git a/shared/completions/tactics.json b/shared/completions/tactics.json index d00862b2..eebd7a58 100644 --- a/shared/completions/tactics.json +++ b/shared/completions/tactics.json @@ -3,105 +3,105 @@ "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.", + "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,152 +111,152 @@ "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*)", "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." + "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*)).", "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." + "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 }, { "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.", + "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 }, { "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 }, { - "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 }, { "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,9 +266,9 @@ "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)).", + "example": "Expand the definition of upper bound in (4 is an upper bound for [0, 3).", "advanced": false, "boost": 1 }, @@ -276,155 +276,155 @@ "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.", + "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 }, { "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.", + "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 }, { "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 }, { - "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 } ] 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/extension.ts b/src/extension.ts index 359d6d22..fed9863d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,25 +15,21 @@ 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"; -import { Logbook } from "./webviews/goalviews/logbook"; 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"; import { VersionChecker } from "./version-checker"; -import { readFile } from "fs"; -import { join as joinPath} from "path"; -import { homedir } from "os"; -import { WaterproofConfigHelper, WaterproofLogger } from "./helpers"; -import { exec } from "child_process" +import { Utils } from "vscode-uri"; +import { WaterproofConfigHelper, WaterproofLogger as wpl } from "./helpers"; + export function activate(_context: ExtensionContext): void { @@ -81,10 +77,11 @@ export class Waterproof implements Disposable { * * @param context the extension context object */ - constructor(context: ExtensionContext, clientFactory: CoqLspClientFactory) { - WaterproofLogger.log("Waterproof initialized"); + constructor(context: ExtensionContext, clientFactory: CoqLspClientFactory, private readonly _isWeb = false) { + wpl.log("Waterproof initialized"); checkConflictingExtensions(); excludeCoqFileTypes(); + checkTrimmingWhitespace(); this.context = context; this.clientFactory = clientFactory; @@ -94,7 +91,7 @@ export class Waterproof implements Disposable { this.client.updateCompletions(document); }); this.webviewManager.on(WebviewManagerEvents.focus, async (document: TextDocument) => { - WaterproofLogger.log("Focus event received"); + wpl.log("Focus event received"); // Wait for client to initialize if (!this.clientRunning) { @@ -110,20 +107,21 @@ export class Waterproof implements Disposable { }); }; await waitForClient(); - WaterproofLogger.log("Client ready. Proceeding with focus event."); + wpl.log("Client ready. Proceeding with focus event."); } - WaterproofLogger.log("Client state"); + wpl.log("Client state"); // 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()) { 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.on(WebviewManagerEvents.cursorChange, (document: TextDocument, position: Position) => { // update active document and cursor @@ -154,25 +152,23 @@ 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)); - 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)); - const logbook = new Logbook(this.context.extensionUri, CoqLspClientConfig.create(WaterproofConfigHelper.configuration)); - this.webviewManager.addToolWebview("logbook", 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); this.executorComponent = executorPanel; // register commands - WaterproofLogger.log("Calling initializeClient..."); + wpl.log("Calling initializeClient..."); this.registerCommand("start", this.initializeClient); this.registerCommand("restart", this.restartClient); this.registerCommand("toggle", this.toggleClient); @@ -213,7 +209,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); }); @@ -236,7 +232,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); @@ -246,14 +242,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) { @@ -281,7 +277,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) } }); @@ -298,7 +294,10 @@ export class Waterproof implements Disposable { */ private async autoInstall(command: string): Promise { return new Promise((resolve, _reject) => { - exec(command, (err, _stdout, _stderr) => { + // 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. @@ -321,7 +320,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) { @@ -330,27 +329,26 @@ 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")); + 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"); 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; + }) }); } @@ -360,27 +358,26 @@ export class Waterproof implements Disposable { * or by using the File -> New File... option. */ 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")); + 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"); return; } - const newFilePath = Uri.joinPath(this.context.extensionUri, "misc-includes", "empty_waterproof_document.mv").fsPath; - readFile(newFilePath, (err, data) => { - if (err) { - window.showErrorMessage("Could not a new Waterproof file."); + 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 create 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; + }) }); } @@ -418,55 +415,65 @@ export class Waterproof implements Disposable { * Create the lsp client and update relevant status components */ 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(); + wpl.log("Start of initializeClient"); + + // Whether the user has decided to skip the launch checks + const launchChecksDisabled = WaterproofConfigHelper.skipLaunchChecks; + + 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; + 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"); + } + } - if (foundServer) { + if (this.client?.isRunning()) { + return Promise.reject(new Error("Cannot initialize client; one is already running.")) + } - versionChecker.run(); + const serverOptions = CoqLspServerConfig.create( + // TODO: Support +coqversion versions. + this.context.extension.packageJSON.requiredCoqLspVersion.slice(2), + WaterproofConfigHelper.configuration + ); - if (this.client?.isRunning()) { - return Promise.reject(new Error("Cannot initialize client; one is already running.")) + 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 }, + }; + + 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; + wpl.log("Client initialization complete."); + }, + reason => { + const message = reason.toString(); + wpl.log(`Error during client initialization: ${message}`); + this.statusBar.failed(message); + throw reason; // keep chain rejected } - - 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"); - } + ); + } /** @@ -507,15 +514,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/helpers/config-helper.ts b/src/helpers/config-helper.ts index d4252abc..98455bdb 100644 --- a/src/helpers/config-helper.ts +++ b/src/helpers/config-helper.ts @@ -1,4 +1,6 @@ import { workspace } from "vscode"; +import { HypVisibility } from "../../lib/types"; +import { WaterproofLogger as wpl } from "./logger"; export class WaterproofConfigHelper { @@ -22,6 +24,16 @@ export class WaterproofConfigHelper { return config().get("showLineNumbersInEditor") as boolean; } + /** `waterproof.skipLaunchChecks` */ + static get skipLaunchChecks() { + 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 @@ -35,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; @@ -75,6 +82,21 @@ export class WaterproofConfigHelper { return config().get("pp_type") as number; } + + /** `waterproof.visibilityOfHypotheses` */ + static get visibilityOfHypotheses() : HypVisibility { + const hypVisibility = config().get("visibilityOfHypotheses"); + wpl.log(`Hypothesis visibility set to: ${hypVisibility}`); + 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() { return config().get<"off" | "messages" | "verbose">("trace.server") as "off" | "messages" | "verbose"; diff --git a/src/helpers/file.ts b/src/helpers/file.ts new file mode 100644 index 00000000..c7b551f7 --- /dev/null +++ b/src/helpers/file.ts @@ -0,0 +1,24 @@ +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 { + // 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); + } +} \ 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/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/lsp-client/client.ts b/src/lsp-client/client.ts index 9102c893..d9edf1d5 100644 --- a/src/lsp-client/client.ts +++ b/src/lsp-client/client.ts @@ -10,14 +10,14 @@ 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"; 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(); - + wpl.debug("CoqLspClient constructor"); // forward progress notifications to editor this.fileProgressComponents.push({ dispose() { /* noop */ }, @@ -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) @@ -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); } diff --git a/src/lsp-client/clientTypes.ts b/src/lsp-client/clientTypes.ts index 83bd5ba7..b28e6c64 100644 --- a/src/lsp-client/clientTypes.ts +++ b/src/lsp-client/clientTypes.ts @@ -1,4 +1,4 @@ -import { Position, TextDocument, Uri, WorkspaceConfiguration, Range, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag } from "vscode"; +import { Position, ExtensionContext, TextDocument, Uri, WorkspaceConfiguration, Range, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag } 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/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/mainBrowser.ts b/src/mainBrowser.ts index 39998520..5f20c78d 100644 --- a/src/mainBrowser.ts +++ b/src/mainBrowser.ts @@ -1,11 +1,40 @@ -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` */ -export function activate(_context: ExtensionContext): void { +const clientFactory: CoqLspClientFactory = (context: ExtensionContext, clientOptions: LanguageClientOptions, _wsConfig: WorkspaceConfiguration) => { + const serverMain = Uri.joinPath(context.extensionUri, 'out/src/mainBrowser.js'); + // 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", + "Waterproof Document Checker", + clientOptions, + lspWorker + ); +}; + + +export function activate(context: ExtensionContext): void { + console.log("Browser activate function"); + const extension: Waterproof = new Waterproof(context, clientFactory, true); + 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 61607cb1..910f967d 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, 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 b1d4d0bd..f168b962 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, Range, TextDocument, Uri, WebviewPanel, WorkspaceEdit, commands, window, workspace } from "vscode"; import { DocChange, FileFormat, Message, MessageType, WrappingDocChange, LineNumber } from "../../shared"; @@ -8,13 +8,11 @@ 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"; -import * as path from 'path'; - export class ProseMirrorWebview extends EventEmitter { /** The webview panel of this ProseMirror instance */ private _panel: WebviewPanel; @@ -23,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(); @@ -41,6 +39,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: { @@ -83,17 +82,14 @@ export class ProseMirrorWebview extends EventEmitter { this.undoHandler.bind(this), this.redoHandler.bind(this)); - - - const fileName = path.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.` window.showErrorMessage(error, { modal: true }, SAVE_AS).then(this.handleFileNameSaveAs); WaterproofLogger.log("Error: Illegal file name encountered."); } - this._format = getFormatFromExtension(doc); this._panel = webview; this._workspaceEditor.onFinish(() => { @@ -109,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) { @@ -177,15 +181,17 @@ export class ProseMirrorWebview extends EventEmitter { } })); + this._disposables.push(workspace.onDidSaveTextDocument(e => { + if (e.uri.toString() === this._document.uri.toString()) { + this.refreshOnSave(); + } + })); + this._disposables.push(workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration("waterproof.teacherMode")) { this.updateTeacherMode(); } - if (e.affectsConfiguration("waterproof.standardCoqSyntax")) { - this.updateSyntaxMode(); - } - if (e.affectsConfiguration("waterproof.enforceCorrectNonInputArea")) { this._enforceCorrectNonInputArea = WaterproofConfigHelper.enforceCorrectNonInputArea; } @@ -194,6 +200,12 @@ export class ProseMirrorWebview extends EventEmitter { this._showLineNrsInEditor = WaterproofConfigHelper.showLineNumbersInEditor; this.updateLineNumberStatusInEditor(); } + + if (e.affectsConfiguration("waterproof.showMenuItemsInEditor")) { + this._showMenuItemsInEditor = WaterproofConfigHelper.showMenuItems; + WaterproofLogger.log(`Menu items will now be ${WaterproofConfigHelper.showMenuItems ? "shown" : "hidden"} in student mode`); + this.updateMenuItemsInEditor(); + } })); this._disposables.push(this._panel.webview.onDidReceiveMessage((msg) => { @@ -266,6 +278,19 @@ export class ProseMirrorWebview extends EventEmitter { for (const m of this._cachedMessages.values()) this.postMessage(m); } + private refreshOnSave() { + this.postMessage({ + type: MessageType.refreshDocument, + body: { + value: this._document.getText(), + version: this._document.version, + } + }); + this.updateLineNumberStatusInEditor(); + // send any cached messages + for (const m of this._cachedMessages.values()) this.postMessage(m); + } + private updateLineNumberStatusInEditor() { this.updateLineNumbers(); this.postMessage({ @@ -275,6 +300,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. @@ -299,14 +331,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) { @@ -394,7 +418,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; 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", { diff --git a/src/version-checker/version-checker.ts b/src/version-checker/version-checker.ts index 681fcd0d..3d9459c9 100644 --- a/src/version-checker/version-checker.ts +++ b/src/version-checker/version-checker.ts @@ -1,8 +1,7 @@ import { ExtensionContext, Uri, WorkspaceConfiguration, commands, env, window } from "vscode"; -import { exec } from "child_process"; -import * as path from 'path'; import { Version, VersionRequirement } from "./version"; import { COMPARE_MODE } from "./types"; +import { WaterproofFileUtil, WaterproofLogger as wpl } from "../helpers"; export type VersionError = { reason: string; @@ -43,6 +42,7 @@ export class VersionChecker { */ public async prelaunchChecks(): Promise { const version = await this.checkLSPBinary(); + wpl.log(`Version of coq-lsp: ${version}`); if (!isVersionError(version)) { if (version.needsUpdate(this._reqVersionCoqLSP)) { this.informUpdateAvailable("coq-lsp", this._reqVersionCoqLSP, version); @@ -95,19 +95,22 @@ 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 = path.join(path.parse(this._wpPath).dir, "coqc"); - const command = `${coqcBinary} --version`; - const regex = /version (?\d+\.\d+\.\d+)/g; + if (this._wpPath === undefined) return { reason: "Waterproof.path is undefined" }; - 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"); - resolve(Version.fromString(groups!["version"])); - }); - }); + 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: unknown) { + return err instanceof Error + ? { reason: err.message } + : { reason: "Unknown error" }; + } } /** @@ -115,23 +118,25 @@ 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" : ""; - const coqtopPath = path.join(path.parse(this._wpPath).dir, `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 }); - - 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.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`; + + 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: unknown) { + return err instanceof Error + ? { reason: err.message } + : { reason: "Unknown error" }; + } } /** @@ -139,15 +144,35 @@ 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`; + 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: unknown) { + return err instanceof Error + ? { reason: err.message } + : { reason: "Unknown error" }; + } + } - exec(command, (err, stdout, _stderr) => { - if (err) return resolve({ reason: err.message }); - const version = Version.fromString(stdout.trim()); - resolve(version); + /** Wrapper around shellIntegration */ + private async exec(command: string): Promise { + wpl.log(`Running command: ${command}`) + 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); + } }); + }); } diff --git a/src/webviewManager.ts b/src/webviewManager.ts index c35fe70d..dc8fffee 100644 --- a/src/webviewManager.ts +++ b/src/webviewManager.ts @@ -1,6 +1,5 @@ -import { EventEmitter } from "stream"; import { TextDocument, Uri, window } from "vscode"; - +import { EventEmitter } from "events"; import { Message, MessageType } from "../shared"; import { ILineNumberComponent } from "./components"; import { LineStatusBar } from "./components/lineNumber"; @@ -12,7 +11,8 @@ export enum WebviewManagerEvents { focus = "focus", cursorChange = "cursorChange", command = "command", - updateButton = "updateButton" + updateButton = "updateButton", + buttonClick = "buttonClick", } /** @@ -110,9 +110,9 @@ export class WebviewManager extends EventEmitter { webview.on(WebviewEvents.message, (msg: Message) => { this.onToolsMessage(name, msg); }); + webview.on(WebviewEvents.change, (state) => { if (state == WebviewState.focus && webview.supportInsert) this._active.insert(name); - this.emit(WebviewManagerEvents.updateButton, { name, open: webview.isOpened}); }); } @@ -146,24 +146,38 @@ 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) { - 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 - * @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); - console.log(this._toolWebviews) - this._toolWebviews.get(id)?.revealPanel() + if (!this._toolWebviews.has(id)) { + 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); + // Check if the panel is already open + if (panel?.isOpened) { + this.emit(WebviewManagerEvents.buttonClick, { name: id }); + return; + } + + // Open the panel if it is not already open + else if(panel?.isHidden) { + this.emit(WebviewManagerEvents.buttonClick, { name: id }); + panel?.revealPanel(); + } + + // Open the panel if it is not hidden and not already open + else{ + this.emit(WebviewManagerEvents.buttonClick, { name: id }); + panel?.readyPanel(); + panel?.activatePanel(); + panel?.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 e4515b8a..ca81bbd2 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, @@ -60,10 +60,13 @@ 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() { + public get state() { return this._state; } @@ -89,19 +92,12 @@ 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, this.name.charAt(0).toUpperCase() + this.name.slice(1), { preserveFocus: true, viewColumn: ViewColumn.Two }, - webviewOpts + webviewOpts, ); } @@ -160,6 +156,7 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { const prev = this._state; this._state = next; this.emit(WebviewEvents.change, prev); + } /** @@ -168,6 +165,7 @@ export abstract class CoqWebview extends EventEmitter implements Disposable { public activatePanel() { if (this.state == WebviewState.ready) { this.create(); + this.changeState(WebviewState.open); } } @@ -176,7 +174,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/goalviews/goalsBase.ts b/src/webviews/goalviews/goalsBase.ts index 575c21ab..9b3e82f6 100644 --- a/src/webviews/goalviews/goalsBase.ts +++ b/src/webviews/goalviews/goalsBase.ts @@ -4,7 +4,7 @@ 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 +17,10 @@ export abstract class GoalsBase extends CoqWebview implements IGoalsComponent { //sends message for renderGoals updateGoals(goals: GoalAnswer | undefined) { - this.postMessage({ type: MessageType.renderGoals, body: goals }); + if (goals) { + const visibility = WaterproofConfigHelper.visibilityOfHypotheses; + this.postMessage({ type: MessageType.renderGoals, body: {goals, visibility } }); + } } //sends message for errorGoals 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/goalviews/logbook.ts b/src/webviews/goalviews/logbook.ts deleted file mode 100644 index 2e24f252..00000000 --- a/src/webviews/goalviews/logbook.ts +++ /dev/null @@ -1,33 +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"; - -//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.renderGoals, body: this.messages }); - } - }); - } - - //override updateGoals to save the previous message and activating the panel before posting the message - override updateGoals(goals: GoalAnswer | undefined) { - if (!goals) return; - this.messages.push(goals); - this.activatePanel(); - this.postMessage({ type: MessageType.renderGoals, body: this.messages }); - } - -} diff --git a/src/webviews/sidePanel.ts b/src/webviews/sidePanel.ts index f131ca4d..58827431 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'; - // 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); @@ -11,49 +10,42 @@ 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, 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 updateButtonTransparency(buttonId: string, open: boolean) { - // Set the transparency state of the button in the transparency map - this._transparencyMap.set(buttonId, open); - // Update the button styles to reflect the transparency state - this.updateButtonStyles(buttonId, open); - } - private updateButtonStyles(buttonId: string, open: boolean) { - // If the view is not available, return without updating the styles + private flashButton(buttonId: string) { + // If the view is not available, return without flashing if (!this._view) return; - - // Post a message to the webview to update the button transparency + // Post a message to the webview to flash the button this._view.webview.postMessage({ - type: 'trans', + type: 'flash', buttonId: buttonId, - open: open, }); } + public resolveWebviewView( webviewView: vscode.WebviewView, _context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken, ) { this._view = webviewView; - + // Set options for the webview webviewView.webview.options = { enableScripts: true, @@ -70,11 +62,12 @@ 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); } - }); + }); } // Now we create the actual web page @@ -105,9 +98,14 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { background-color: var(--vscode-button-hoverBackground); } - .transparent { - opacity: 0.6; - cursor: default; + .flash { + animation: flashGrey 0.5s ease-in-out; + } + + @keyframes flashGrey { + 0% { opacity: 1; } + 50% { opacity: 0.4; } + 100% { opacity: 1; } } @@ -116,11 +114,9 @@ export class SidePanelProvider implements vscode.WebviewViewProvider { - - @@ -128,12 +124,10 @@ 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'); const searchButton = document.getElementById('search'); - const expandButton = document.getElementById('expandDefinition'); const symbolsButton = document.getElementById('symbols'); const tacticsButton = document.getElementById('tactics'); @@ -142,11 +136,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' }); @@ -167,11 +156,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' }); @@ -185,20 +169,15 @@ 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': - 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'); - } - } + + if (message.type === 'flash') { + const button = document.getElementById(message.buttonId); + if (button) { + button.classList.add('flash'); + setTimeout(() => { + button.classList.remove('flash'); + }, 500); + } } }); 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/debug/Debug.tsx b/views/debug/Debug.tsx index cd0e14d5..eca0052f 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, 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"; +import { Messages } from "../goals/Messages"; + import "../styles/info.css"; import { Message, MessageType } from "../../shared"; @@ -17,38 +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) { if (msg.type === MessageType.renderGoals) { - // most important case that actually get the information - setGoals(msg.body); + 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 : (
@@ -58,7 +67,8 @@ export function Debug() {
)}
- ) + ); } export default Debug; + 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/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/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 -
-