diff --git a/e2e/playwright/file-tree.spec.ts b/e2e/playwright/file-tree.spec.ts index ece92ac67..edafbac9c 100644 --- a/e2e/playwright/file-tree.spec.ts +++ b/e2e/playwright/file-tree.spec.ts @@ -1135,3 +1135,189 @@ _test.describe('Deleting items from the file pane', () => { } ) }) + +_test.describe( + 'Undo and redo do not keep history when navigating between files', + () => { + _test( + `open a file, change something, open a different file, hitting undo should do nothing`, + { tag: '@electron' }, + async ({ browserName }, testInfo) => { + const { page } = await setupElectron({ + testInfo, + folderSetupFn: async (dir) => { + const testDir = join(dir, 'testProject') + await fsp.mkdir(testDir, { recursive: true }) + await fsp.copyFile( + executorInputPath('cylinder.kcl'), + join(testDir, 'main.kcl') + ) + await fsp.copyFile( + executorInputPath('basic_fillet_cube_end.kcl'), + join(testDir, 'other.kcl') + ) + }, + }) + const u = await getUtils(page) + await page.setViewportSize({ width: 1200, height: 500 }) + page.on('console', console.log) + + // Constants and locators + const projectCard = page.getByText('testProject') + const otherFile = page + .getByRole('listitem') + .filter({ has: page.getByRole('button', { name: 'other.kcl' }) }) + + await _test.step( + 'Open project and make a change to the file', + async () => { + await projectCard.click() + await u.waitForPageLoad() + + // Get the text in the code locator. + const originalText = await u.codeLocator.innerText() + // Click in the editor and add some new lines. + await u.codeLocator.click() + + await page.keyboard.type(`sketch001 = startSketchOn('XY') + some other shit`) + + // Ensure the content in the editor changed. + const newContent = await u.codeLocator.innerText() + + expect(originalText !== newContent) + } + ) + + await _test.step('navigate to other.kcl', async () => { + await u.openFilePanel() + + await otherFile.click() + await u.waitForPageLoad() + await u.openKclCodePanel() + await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)') + }) + + await _test.step('hit undo', async () => { + // Get the original content of the file. + const originalText = await u.codeLocator.innerText() + // Now hit undo + await page.keyboard.down('ControlOrMeta') + await page.keyboard.press('KeyZ') + await page.keyboard.up('ControlOrMeta') + + await page.waitForTimeout(100) + await expect(u.codeLocator).toContainText(originalText) + }) + } + ) + + _test( + `open a file, change something, undo it, open a different file, hitting redo should do nothing`, + { tag: '@electron' }, + // Skip on windows i think the keybindings are different for redo. + async ({ browserName }, testInfo) => { + test.skip(process.platform === 'win32', 'Skip on windows') + const { page } = await setupElectron({ + testInfo, + folderSetupFn: async (dir) => { + const testDir = join(dir, 'testProject') + await fsp.mkdir(testDir, { recursive: true }) + await fsp.copyFile( + executorInputPath('cylinder.kcl'), + join(testDir, 'main.kcl') + ) + await fsp.copyFile( + executorInputPath('basic_fillet_cube_end.kcl'), + join(testDir, 'other.kcl') + ) + }, + }) + const u = await getUtils(page) + await page.setViewportSize({ width: 1200, height: 500 }) + page.on('console', console.log) + + // Constants and locators + const projectCard = page.getByText('testProject') + const otherFile = page + .getByRole('listitem') + .filter({ has: page.getByRole('button', { name: 'other.kcl' }) }) + + const badContent = 'this shit' + await _test.step( + 'Open project and make a change to the file', + async () => { + await projectCard.click() + await u.waitForPageLoad() + + // Get the text in the code locator. + const originalText = await u.codeLocator.innerText() + // Click in the editor and add some new lines. + await u.codeLocator.click() + + await page.keyboard.type(badContent) + + // Ensure the content in the editor changed. + const newContent = await u.codeLocator.innerText() + + expect(originalText !== newContent) + + // Now hit undo + await page.keyboard.down('ControlOrMeta') + await page.keyboard.press('KeyZ') + await page.keyboard.up('ControlOrMeta') + + await page.waitForTimeout(100) + await expect(u.codeLocator).toContainText(originalText) + await expect(u.codeLocator).not.toContainText(badContent) + + // Hit redo. + await page.keyboard.down('Shift') + await page.keyboard.down('ControlOrMeta') + await page.keyboard.press('KeyZ') + await page.keyboard.up('ControlOrMeta') + await page.keyboard.up('Shift') + + await page.waitForTimeout(100) + await expect(u.codeLocator).toContainText(originalText) + await expect(u.codeLocator).toContainText(badContent) + + // Now hit undo + await page.keyboard.down('ControlOrMeta') + await page.keyboard.press('KeyZ') + await page.keyboard.up('ControlOrMeta') + + await page.waitForTimeout(100) + await expect(u.codeLocator).toContainText(originalText) + await expect(u.codeLocator).not.toContainText(badContent) + } + ) + + await _test.step('navigate to other.kcl', async () => { + await u.openFilePanel() + + await otherFile.click() + await u.waitForPageLoad() + await u.openKclCodePanel() + await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)') + await expect(u.codeLocator).not.toContainText(badContent) + }) + + await _test.step('hit redo', async () => { + // Get the original content of the file. + const originalText = await u.codeLocator.innerText() + // Now hit redo + await page.keyboard.down('Shift') + await page.keyboard.down('ControlOrMeta') + await page.keyboard.press('KeyZ') + await page.keyboard.up('ControlOrMeta') + await page.keyboard.up('Shift') + + await page.waitForTimeout(100) + await expect(u.codeLocator).toContainText(originalText) + await expect(u.codeLocator).not.toContainText(badContent) + }) + } + ) + } +) diff --git a/src/components/ModelingSidebar/ModelingPanes/KclEditorPane.tsx b/src/components/ModelingSidebar/ModelingPanes/KclEditorPane.tsx index e43132958..ab4705fc0 100644 --- a/src/components/ModelingSidebar/ModelingPanes/KclEditorPane.tsx +++ b/src/components/ModelingSidebar/ModelingPanes/KclEditorPane.tsx @@ -43,6 +43,7 @@ import { completionKeymap, } from '@codemirror/autocomplete' import CodeEditor from './CodeEditor' +import { codeManagerHistoryCompartment } from 'lang/codeManager' export const editorShortcutMeta = { formatCode: { @@ -89,7 +90,7 @@ export const KclEditorPane = () => { cursorBlinkRate: cursorBlinking.current ? 1200 : 0, }), lineHighlightField, - history(), + codeManagerHistoryCompartment.of(history()), closeBrackets(), codeFolding(), keymap.of([ @@ -121,7 +122,6 @@ export const KclEditorPane = () => { lineNumbers(), highlightActiveLineGutter(), highlightSpecialChars(), - history(), foldGutter(), EditorState.allowMultipleSelections.of(true), indentOnInput(), diff --git a/src/lang/codeManager.ts b/src/lang/codeManager.ts index 39a21e360..bb295bd24 100644 --- a/src/lang/codeManager.ts +++ b/src/lang/codeManager.ts @@ -6,14 +6,17 @@ import { isDesktop } from 'lib/isDesktop' import toast from 'react-hot-toast' import { editorManager } from 'lib/singletons' import { Annotation, Transaction } from '@codemirror/state' -import { KeyBinding } from '@codemirror/view' +import { EditorView, KeyBinding } from '@codemirror/view' import { recast, Program } from 'lang/wasm' import { err } from 'lib/trap' +import { Compartment } from '@codemirror/state' +import { history } from '@codemirror/commands' const PERSIST_CODE_KEY = 'persistCode' const codeManagerUpdateAnnotation = Annotation.define() export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(true) +export const codeManagerHistoryCompartment = new Compartment() export default class CodeManager { private _code: string = bracket @@ -90,9 +93,12 @@ export default class CodeManager { /** * Update the code in the editor. */ - updateCodeEditor(code: string): void { + updateCodeEditor(code: string, clearHistory?: boolean): void { this.code = code if (editorManager.editorView) { + if (clearHistory) { + clearCodeMirrorHistory(editorManager.editorView) + } editorManager.editorView.dispatch({ changes: { from: 0, @@ -101,7 +107,7 @@ export default class CodeManager { }, annotations: [ codeManagerUpdateEvent, - Transaction.addToHistory.of(true), + Transaction.addToHistory.of(!clearHistory), ], }) } @@ -110,11 +116,11 @@ export default class CodeManager { /** * Update the code, state, and the code the code mirror editor sees. */ - updateCodeStateEditor(code: string): void { + updateCodeStateEditor(code: string, clearHistory?: boolean): void { if (this._code !== code) { this.code = code this.#updateState(code) - this.updateCodeEditor(code) + this.updateCodeEditor(code, clearHistory) } } @@ -167,3 +173,17 @@ function safeLSSetItem(key: string, value: string) { if (typeof window === 'undefined') return localStorage?.setItem(key, value) } + +function clearCodeMirrorHistory(view: EditorView) { + // Clear history + view.dispatch({ + effects: [codeManagerHistoryCompartment.reconfigure([])], + annotations: [codeManagerUpdateEvent], + }) + + // Add history back + view.dispatch({ + effects: [codeManagerHistoryCompartment.reconfigure([history()])], + annotations: [codeManagerUpdateEvent], + }) +} diff --git a/src/lib/routeLoaders.ts b/src/lib/routeLoaders.ts index f2fdda4b3..8089d28f0 100644 --- a/src/lib/routeLoaders.ts +++ b/src/lib/routeLoaders.ts @@ -124,7 +124,9 @@ export const fileLoader: LoaderFunction = async ( // We explicitly do not write to the file here since we are loading from // the file system and not the editor. codeManager.updateCurrentFilePath(currentFilePath) - codeManager.updateCodeStateEditor(code) + // We pass true on the end here to clear the code editor history. + // This way undo and redo are not super weird when opening new files. + codeManager.updateCodeStateEditor(code, true) } // Set the file system manager to the project path