* move useHotkey for undo/redo into App * _editorView should be private * get editorView should be a real get method for consistency * resolve tsc errors * fmt * setView, setState are not exposed * make undo work without editorview when kcl pane is closed * lint * circular deps * resolve circular deps * fix undo being 1 step late * unrelated console.warn removed * fix undo when code pane is closed during editing * cleanup * allow undo to get beyond when code editor has been mounted * fix up clearHistory * add test for testing Undo with closed code pane
This commit is contained in:
@ -183,14 +183,15 @@ export class EditorFixture {
|
|||||||
scrollToText(text: string, placeCursor?: boolean) {
|
scrollToText(text: string, placeCursor?: boolean) {
|
||||||
return this.page.evaluate(
|
return this.page.evaluate(
|
||||||
(args: { text: string; placeCursor?: boolean }) => {
|
(args: { text: string; placeCursor?: boolean }) => {
|
||||||
|
const editorView = window.editorManager.getEditorView()
|
||||||
// error TS2339: Property 'docView' does not exist on type 'EditorView'.
|
// error TS2339: Property 'docView' does not exist on type 'EditorView'.
|
||||||
// Except it does so :shrug:
|
// Except it does so :shrug:
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let index = window.editorManager._editorView?.docView.view.state.doc
|
const index = editorView?.docView.view.state.doc
|
||||||
.toString()
|
.toString()
|
||||||
.indexOf(args.text)
|
.indexOf(args.text)
|
||||||
window.editorManager._editorView?.focus()
|
editorView?.focus()
|
||||||
window.editorManager._editorView?.dispatch({
|
editorView?.dispatch({
|
||||||
selection: window.EditorSelection.create([
|
selection: window.EditorSelection.create([
|
||||||
window.EditorSelection.cursor(index),
|
window.EditorSelection.cursor(index),
|
||||||
]),
|
]),
|
||||||
|
@ -1478,6 +1478,7 @@ sketch001 = startSketchOn(XZ)
|
|||||||
await page.mouse.move(1200, 139)
|
await page.mouse.move(1200, 139)
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(870, 250)
|
await page.mouse.move(870, 250)
|
||||||
|
await page.mouse.up()
|
||||||
|
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(200)
|
||||||
|
|
||||||
@ -1487,6 +1488,60 @@ sketch001 = startSketchOn(XZ)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Can undo with closed code pane', async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
scene,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
|
||||||
|
const viewportSize = { width: 1500, height: 750 }
|
||||||
|
await page.setBodyDimensions(viewportSize)
|
||||||
|
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`@settings(defaultLengthUnit=in)
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
|> startProfile(at = [-10, -10])
|
||||||
|
|> line(end = [20.0, 10.0])
|
||||||
|
|> tangentialArc(end = [5.49, 8.37])`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await toolbar.waitForFeatureTreeToBeBuilt()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
await (await toolbar.getFeatureTreeOperation('Sketch', 0)).dblclick()
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
|
await page.mouse.move(1200, 139)
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(870, 250)
|
||||||
|
await page.mouse.up()
|
||||||
|
|
||||||
|
await editor.expectEditor.toContain(`tangentialArc(end=[-5.85,4.32])`, {
|
||||||
|
shouldNormalise: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await u.closeKclCodePanel()
|
||||||
|
|
||||||
|
// Undo the last change
|
||||||
|
await page.keyboard.down('Control')
|
||||||
|
await page.keyboard.press('KeyZ')
|
||||||
|
await page.keyboard.up('Control')
|
||||||
|
|
||||||
|
await u.openKclCodePanel()
|
||||||
|
await editor.expectEditor.toContain(`tangentialArc(end = [5.49, 8.37])`, {
|
||||||
|
shouldNormalise: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('Can delete a single segment line with keyboard', async ({
|
test('Can delete a single segment line with keyboard', async ({
|
||||||
page,
|
page,
|
||||||
scene,
|
scene,
|
||||||
|
@ -158,10 +158,10 @@ async function openKclCodePanel(page: Page) {
|
|||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
// editorManager is available on the window object.
|
// editorManager is available on the window object.
|
||||||
//@ts-ignore this is in an entirely different context that tsc can't see.
|
//@ts-ignore this is in an entirely different context that tsc can't see.
|
||||||
editorManager._editorView.dispatch({
|
editorManager.getEditorView().dispatch({
|
||||||
selection: {
|
selection: {
|
||||||
//@ts-ignore this is in an entirely different context that tsc can't see.
|
//@ts-ignore this is in an entirely different context that tsc can't see.
|
||||||
anchor: editorManager._editorView.docView.length,
|
anchor: editorManager.getEditorView().docView.length,
|
||||||
},
|
},
|
||||||
scrollIntoView: true,
|
scrollIntoView: true,
|
||||||
})
|
})
|
||||||
|
11
src/App.tsx
11
src/App.tsx
@ -28,6 +28,7 @@ import {
|
|||||||
codeManager,
|
codeManager,
|
||||||
kclManager,
|
kclManager,
|
||||||
settingsActor,
|
settingsActor,
|
||||||
|
editorManager,
|
||||||
getSettings,
|
getSettings,
|
||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { maybeWriteToDisk } from '@src/lib/telemetry'
|
import { maybeWriteToDisk } from '@src/lib/telemetry'
|
||||||
@ -107,6 +108,16 @@ export function App() {
|
|||||||
useHotkeys('backspace', (e) => {
|
useHotkeys('backspace', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
})
|
})
|
||||||
|
// Since these already exist in the editor, we don't need to define them
|
||||||
|
// with the wrapper.
|
||||||
|
useHotkeys('mod+z', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
editorManager.undo()
|
||||||
|
})
|
||||||
|
useHotkeys('mod+shift+z', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
editorManager.redo()
|
||||||
|
})
|
||||||
useHotkeyWrapper(
|
useHotkeyWrapper(
|
||||||
[isDesktop() ? 'mod + ,' : 'shift + mod + ,'],
|
[isDesktop() ? 'mod + ,' : 'shift + mod + ,'],
|
||||||
() => navigate(filePath + PATHS.SETTINGS),
|
() => navigate(filePath + PATHS.SETTINGS),
|
||||||
|
@ -88,14 +88,14 @@ export function Toolbar({
|
|||||||
modelingState: state,
|
modelingState: state,
|
||||||
modelingSend: send,
|
modelingSend: send,
|
||||||
sketchPathId,
|
sketchPathId,
|
||||||
editorHasFocus: editorManager.editorView?.hasFocus,
|
editorHasFocus: editorManager.getEditorView()?.hasFocus,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
state,
|
state,
|
||||||
send,
|
send,
|
||||||
commandBarActor.send,
|
commandBarActor.send,
|
||||||
sketchPathId,
|
sketchPathId,
|
||||||
editorManager.editorView?.hasFocus,
|
editorManager.getEditorView()?.hasFocus,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3943,7 +3943,7 @@ function isGroupStartProfileForCurrentProfile(sketchEntryNodePath: PathToNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the 2D tangent direction vector at the end of the segmentGroup if it's an arc.
|
// Returns the 2D tangent direction vector at the end of the segmentGroup
|
||||||
function findTangentDirection(segmentGroup: Group) {
|
function findTangentDirection(segmentGroup: Group) {
|
||||||
let tangentDirection: Coords2d | undefined
|
let tangentDirection: Coords2d | undefined
|
||||||
if (segmentGroup.userData.type === TANGENTIAL_ARC_TO_SEGMENT) {
|
if (segmentGroup.userData.type === TANGENTIAL_ARC_TO_SEGMENT) {
|
||||||
@ -3972,11 +3972,6 @@ function findTangentDirection(segmentGroup: Group) {
|
|||||||
const from = segmentGroup.userData.from as Coords2d
|
const from = segmentGroup.userData.from as Coords2d
|
||||||
tangentDirection = subVec(to, from)
|
tangentDirection = subVec(to, from)
|
||||||
tangentDirection = normalizeVec(tangentDirection)
|
tangentDirection = normalizeVec(tangentDirection)
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
'Unsupported segment type for tangent direction calculation: ',
|
|
||||||
segmentGroup.userData.type
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return tangentDirection
|
return tangentDirection
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
|
|||||||
} = props
|
} = props
|
||||||
const editor = useRef<HTMLDivElement>(null)
|
const editor = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const { view, state, container } = useCodeMirror({
|
const { view, container } = useCodeMirror({
|
||||||
container: editor.current,
|
container: editor.current,
|
||||||
onCreateEditor,
|
onCreateEditor,
|
||||||
extensions,
|
extensions,
|
||||||
@ -77,8 +77,8 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
|
|||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
() => ({ editor: editor.current, view: view, state: state }),
|
() => ({ editor: editor.current, view: view, state: view?.state }),
|
||||||
[editor, container, view, state]
|
[editor, container, view]
|
||||||
)
|
)
|
||||||
|
|
||||||
return <div ref={editor}></div>
|
return <div ref={editor}></div>
|
||||||
@ -138,7 +138,7 @@ export function useCodeMirror(props: UseCodeMirror) {
|
|||||||
parent: container,
|
parent: container,
|
||||||
})
|
})
|
||||||
setView(viewCurrent)
|
setView(viewCurrent)
|
||||||
onCreateEditor && onCreateEditor(viewCurrent)
|
onCreateEditor?.(viewCurrent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
@ -156,6 +156,7 @@ export function useCodeMirror(props: UseCodeMirror) {
|
|||||||
if (view) {
|
if (view) {
|
||||||
view.destroy()
|
view.destroy()
|
||||||
setView(undefined)
|
setView(undefined)
|
||||||
|
onCreateEditor?.(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[view]
|
[view]
|
||||||
@ -175,7 +176,7 @@ export function useCodeMirror(props: UseCodeMirror) {
|
|||||||
}
|
}
|
||||||
}, [targetExtensions, view, isFirstRender])
|
}, [targetExtensions, view, isFirstRender])
|
||||||
|
|
||||||
return { view, setView, container, setContainer, state, setState }
|
return { view, container, setContainer, state }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CodeEditor
|
export default CodeEditor
|
||||||
|
@ -45,7 +45,7 @@ export const FeatureTreePane = () => {
|
|||||||
guards: {
|
guards: {
|
||||||
codePaneIsOpen: () =>
|
codePaneIsOpen: () =>
|
||||||
modelingState.context.store.openPanes.includes('code') &&
|
modelingState.context.store.openPanes.includes('code') &&
|
||||||
editorManager.editorView !== null,
|
editorManager.getEditorView() !== null,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
openCodePane: () => {
|
openCodePane: () => {
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
defaultKeymap,
|
defaultKeymap,
|
||||||
history,
|
history,
|
||||||
|
historyField,
|
||||||
historyKeymap,
|
historyKeymap,
|
||||||
indentWithTab,
|
indentWithTab,
|
||||||
} from '@codemirror/commands'
|
} from '@codemirror/commands'
|
||||||
@ -37,13 +38,12 @@ import interact from '@replit/codemirror-interact'
|
|||||||
import { TEST } from '@src/env'
|
import { TEST } from '@src/env'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
|
||||||
|
|
||||||
import { useLspContext } from '@src/components/LspProvider'
|
import { useLspContext } from '@src/components/LspProvider'
|
||||||
import CodeEditor from '@src/components/ModelingSidebar/ModelingPanes/CodeEditor'
|
import CodeEditor from '@src/components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||||
import { lineHighlightField } from '@src/editor/highlightextension'
|
import { lineHighlightField } from '@src/editor/highlightextension'
|
||||||
import { modelingMachineEvent } from '@src/editor/manager'
|
import { modelingMachineEvent } from '@src/editor/manager'
|
||||||
import { codeManagerHistoryCompartment } from '@src/lang/codeManager'
|
import { historyCompartment } from '@src/editor/compartments'
|
||||||
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
||||||
import { Themes, getSystemTheme } from '@src/lib/theme'
|
import { Themes, getSystemTheme } from '@src/lib/theme'
|
||||||
import { onMouseDragMakeANewNumber, onMouseDragRegex } from '@src/lib/utils'
|
import { onMouseDragMakeANewNumber, onMouseDragRegex } from '@src/lib/utils'
|
||||||
@ -75,17 +75,6 @@ export const KclEditorPane = () => {
|
|||||||
: context.app.theme.current
|
: context.app.theme.current
|
||||||
const { copilotLSP, kclLSP } = useLspContext()
|
const { copilotLSP, kclLSP } = useLspContext()
|
||||||
|
|
||||||
// Since these already exist in the editor, we don't need to define them
|
|
||||||
// with the wrapper.
|
|
||||||
useHotkeys('mod+z', (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
editorManager.undo()
|
|
||||||
})
|
|
||||||
useHotkeys('mod+shift+z', (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
editorManager.redo()
|
|
||||||
})
|
|
||||||
|
|
||||||
// When this component unmounts, we need to tell the machine that the editor
|
// When this component unmounts, we need to tell the machine that the editor
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -96,12 +85,13 @@ export const KclEditorPane = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editorIsMounted || !lastSelectionEvent || !editorManager.editorView) {
|
const editorView = editorManager.getEditorView()
|
||||||
|
if (!editorIsMounted || !lastSelectionEvent || !editorView) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
editorManager.editorView.dispatch({
|
editorView.dispatch({
|
||||||
selection: lastSelectionEvent.codeMirrorSelection,
|
selection: lastSelectionEvent.codeMirrorSelection,
|
||||||
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],
|
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],
|
||||||
scrollIntoView: lastSelectionEvent.scrollIntoView,
|
scrollIntoView: lastSelectionEvent.scrollIntoView,
|
||||||
@ -119,13 +109,21 @@ export const KclEditorPane = () => {
|
|||||||
// Instead, hot load hotkeys via code mirror native.
|
// Instead, hot load hotkeys via code mirror native.
|
||||||
const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys()
|
const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys()
|
||||||
|
|
||||||
|
// When opening the editor, use the existing history in editorManager.
|
||||||
|
// This is needed to ensure users can undo beyond when the editor has been openeed.
|
||||||
|
// (Another solution would be to reuse the same state instead of creating a new one in CodeEditor.)
|
||||||
|
const existingHistory = editorManager.editorState.field(historyField)
|
||||||
|
const initialHistory = existingHistory
|
||||||
|
? historyField.init(() => existingHistory)
|
||||||
|
: history()
|
||||||
|
|
||||||
const editorExtensions = useMemo(() => {
|
const editorExtensions = useMemo(() => {
|
||||||
const extensions = [
|
const extensions = [
|
||||||
drawSelection({
|
drawSelection({
|
||||||
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
|
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
|
||||||
}),
|
}),
|
||||||
lineHighlightField,
|
lineHighlightField,
|
||||||
codeManagerHistoryCompartment.of(history()),
|
historyCompartment.of(initialHistory),
|
||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
codeFolding(),
|
codeFolding(),
|
||||||
keymap.of([
|
keymap.of([
|
||||||
@ -206,10 +204,9 @@ export const KclEditorPane = () => {
|
|||||||
extensions={editorExtensions}
|
extensions={editorExtensions}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onCreateEditor={(_editorView) => {
|
onCreateEditor={(_editorView) => {
|
||||||
if (_editorView === null) return
|
|
||||||
|
|
||||||
editorManager.setEditorView(_editorView)
|
editorManager.setEditorView(_editorView)
|
||||||
kclEditorActor.send({ type: 'setKclEditorMounted', data: true })
|
|
||||||
|
if (!_editorView) return
|
||||||
|
|
||||||
// Update diagnostics as they are cleared when the editor is unmounted.
|
// Update diagnostics as they are cleared when the editor is unmounted.
|
||||||
// Without this, errors would not be shown when closing and reopening the editor.
|
// Without this, errors would not be shown when closing and reopening the editor.
|
||||||
|
3
src/editor/compartments.ts
Normal file
3
src/editor/compartments.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { Compartment } from '@codemirror/state'
|
||||||
|
|
||||||
|
export const historyCompartment = new Compartment()
|
@ -1,10 +1,22 @@
|
|||||||
import { redo, undo } from '@codemirror/commands'
|
import {
|
||||||
|
defaultKeymap,
|
||||||
|
history,
|
||||||
|
historyKeymap,
|
||||||
|
redo,
|
||||||
|
undo,
|
||||||
|
} from '@codemirror/commands'
|
||||||
import { syntaxTree } from '@codemirror/language'
|
import { syntaxTree } from '@codemirror/language'
|
||||||
import type { Diagnostic } from '@codemirror/lint'
|
import type { Diagnostic } from '@codemirror/lint'
|
||||||
import { forEachDiagnostic, setDiagnosticsEffect } from '@codemirror/lint'
|
import { forEachDiagnostic, setDiagnosticsEffect } from '@codemirror/lint'
|
||||||
import { Annotation, EditorSelection, Transaction } from '@codemirror/state'
|
import {
|
||||||
|
Annotation,
|
||||||
|
EditorSelection,
|
||||||
|
EditorState,
|
||||||
|
Transaction,
|
||||||
|
type TransactionSpec,
|
||||||
|
} from '@codemirror/state'
|
||||||
import type { ViewUpdate } from '@codemirror/view'
|
import type { ViewUpdate } from '@codemirror/view'
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView, keymap } from '@codemirror/view'
|
||||||
import type { StateFrom } from 'xstate'
|
import type { StateFrom } from 'xstate'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -22,6 +34,8 @@ import type {
|
|||||||
ModelingMachineEvent,
|
ModelingMachineEvent,
|
||||||
modelingMachine,
|
modelingMachine,
|
||||||
} from '@src/machines/modelingMachine'
|
} from '@src/machines/modelingMachine'
|
||||||
|
import { historyCompartment } from '@src/editor/compartments'
|
||||||
|
import type CodeManager from '@src/lang/codeManager'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -65,11 +79,28 @@ export default class EditorManager {
|
|||||||
|
|
||||||
private _highlightRange: Array<[number, number]> = [[0, 0]]
|
private _highlightRange: Array<[number, number]> = [[0, 0]]
|
||||||
|
|
||||||
public _editorView: EditorView | null = null
|
private _editorState: EditorState
|
||||||
|
private _editorView: EditorView | null = null
|
||||||
public kclManager?: KclManager
|
public kclManager?: KclManager
|
||||||
|
public codeManager?: CodeManager
|
||||||
|
|
||||||
constructor(engineCommandManager: EngineCommandManager) {
|
constructor(engineCommandManager: EngineCommandManager) {
|
||||||
this.engineCommandManager = engineCommandManager
|
this.engineCommandManager = engineCommandManager
|
||||||
|
|
||||||
|
this._editorState = EditorState.create({
|
||||||
|
doc: '',
|
||||||
|
extensions: [
|
||||||
|
historyCompartment.of(history()),
|
||||||
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get editorState(): EditorState {
|
||||||
|
return this._editorView?.state || this._editorState
|
||||||
|
}
|
||||||
|
get state() {
|
||||||
|
return this.editorState
|
||||||
}
|
}
|
||||||
|
|
||||||
setCopilotEnabled(enabled: boolean) {
|
setCopilotEnabled(enabled: boolean) {
|
||||||
@ -80,12 +111,25 @@ export default class EditorManager {
|
|||||||
return this._copilotEnabled
|
return this._copilotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditorView(editorView: EditorView) {
|
// Invoked when editorView is created and each time when it is updated (eg. user is sketching)..
|
||||||
|
setEditorView(editorView: EditorView | null) {
|
||||||
|
// Update editorState to the latest editorView state.
|
||||||
|
// This is needed because if kcl pane is closed, editorView will become null but we still want to use the last state.
|
||||||
|
this._editorState = editorView?.state || this._editorState
|
||||||
|
|
||||||
this._editorView = editorView
|
this._editorView = editorView
|
||||||
kclEditorActor.send({ type: 'setKclEditorMounted', data: true })
|
|
||||||
|
kclEditorActor.send({
|
||||||
|
type: 'setKclEditorMounted',
|
||||||
|
data: Boolean(editorView),
|
||||||
|
})
|
||||||
this.overrideTreeHighlighterUpdateForPerformanceTracking()
|
this.overrideTreeHighlighterUpdateForPerformanceTracking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEditorView(): EditorView | null {
|
||||||
|
return this._editorView
|
||||||
|
}
|
||||||
|
|
||||||
overrideTreeHighlighterUpdateForPerformanceTracking() {
|
overrideTreeHighlighterUpdateForPerformanceTracking() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this._editorView?.plugins.forEach((e) => {
|
this._editorView?.plugins.forEach((e) => {
|
||||||
@ -132,10 +176,6 @@ export default class EditorManager {
|
|||||||
return this._isAllTextSelected
|
return this._isAllTextSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
get editorView(): EditorView | null {
|
|
||||||
return this._editorView
|
|
||||||
}
|
|
||||||
|
|
||||||
get isShiftDown(): boolean {
|
get isShiftDown(): boolean {
|
||||||
return this._isShiftDown
|
return this._isShiftDown
|
||||||
}
|
}
|
||||||
@ -287,12 +327,39 @@ export default class EditorManager {
|
|||||||
undo() {
|
undo() {
|
||||||
if (this._editorView) {
|
if (this._editorView) {
|
||||||
undo(this._editorView)
|
undo(this._editorView)
|
||||||
|
} else if (this._editorState) {
|
||||||
|
const undoPerformed = undo(this) // invokes dispatch which updates this._editorState
|
||||||
|
if (undoPerformed) {
|
||||||
|
const newState = this._editorState
|
||||||
|
// Update the code, this is similar to kcl/index.ts / update, updateDoc,
|
||||||
|
// needed to update the code, so sketch segments can update themselves.
|
||||||
|
// In the editorView case this happens within the kcl plugin's update method being called during updates.
|
||||||
|
this.codeManager!.code = newState.doc.toString()
|
||||||
|
void this.kclManager!.executeCode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redo() {
|
redo() {
|
||||||
if (this._editorView) {
|
if (this._editorView) {
|
||||||
redo(this._editorView)
|
redo(this._editorView)
|
||||||
|
} else if (this._editorState) {
|
||||||
|
const redoPerformed = redo(this)
|
||||||
|
if (redoPerformed) {
|
||||||
|
const newState = this._editorState
|
||||||
|
this.codeManager!.code = newState.doc.toString()
|
||||||
|
void this.kclManager!.executeCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoked by codeMirror during undo/redo.
|
||||||
|
// Call with incorrect "this" so it needs to be an arrow function.
|
||||||
|
dispatch = (spec: TransactionSpec) => {
|
||||||
|
if (this._editorView) {
|
||||||
|
this._editorView.dispatch(spec)
|
||||||
|
} else if (this._editorState) {
|
||||||
|
this._editorState = this._editorState.update(spec).state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +50,6 @@ export function useQueryParamEffects() {
|
|||||||
searchParams.has(CMD_NAME_QUERY_PARAM) &&
|
searchParams.has(CMD_NAME_QUERY_PARAM) &&
|
||||||
searchParams.has(CMD_GROUP_QUERY_PARAM)
|
searchParams.has(CMD_GROUP_QUERY_PARAM)
|
||||||
|
|
||||||
console.log(window.location.href)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watches for legacy `?create-file` hook, which share links currently use.
|
* Watches for legacy `?create-file` hook, which share links currently use.
|
||||||
*/
|
*/
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
// NOT updating the code state when we don't need to.
|
// NOT updating the code state when we don't need to.
|
||||||
// This prevents re-renders of the codemirror editor, when typing.
|
// This prevents re-renders of the codemirror editor, when typing.
|
||||||
import { history } from '@codemirror/commands'
|
import { history } from '@codemirror/commands'
|
||||||
import { Annotation, Compartment, Transaction } from '@codemirror/state'
|
import { Annotation, Transaction } from '@codemirror/state'
|
||||||
import type { EditorView, KeyBinding } from '@codemirror/view'
|
import type { KeyBinding } from '@codemirror/view'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
import { historyCompartment } from '@src/editor/compartments'
|
||||||
import type { Program } from '@src/lang/wasm'
|
import type { Program } from '@src/lang/wasm'
|
||||||
import { parse, recast } from '@src/lang/wasm'
|
import { parse, recast } from '@src/lang/wasm'
|
||||||
import { bracket } from '@src/lib/exampleKcl'
|
import { bracket } from '@src/lib/exampleKcl'
|
||||||
@ -17,7 +18,6 @@ const PERSIST_CODE_KEY = 'persistCode'
|
|||||||
|
|
||||||
const codeManagerUpdateAnnotation = Annotation.define<boolean>()
|
const codeManagerUpdateAnnotation = Annotation.define<boolean>()
|
||||||
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(true)
|
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(true)
|
||||||
export const codeManagerHistoryCompartment = new Compartment()
|
|
||||||
|
|
||||||
export default class CodeManager {
|
export default class CodeManager {
|
||||||
private _code: string = bracket
|
private _code: string = bracket
|
||||||
@ -103,25 +103,24 @@ export default class CodeManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the code in the editor.
|
* Update the code in the editor.
|
||||||
|
* This is invoked when a segment is being dragged on the canvas, among other things.
|
||||||
*/
|
*/
|
||||||
updateCodeEditor(code: string, clearHistory?: boolean): void {
|
updateCodeEditor(code: string, clearHistory?: boolean): void {
|
||||||
this.code = code
|
this.code = code
|
||||||
if (editorManager.editorView) {
|
if (clearHistory) {
|
||||||
if (clearHistory) {
|
clearCodeMirrorHistory()
|
||||||
clearCodeMirrorHistory(editorManager.editorView)
|
|
||||||
}
|
|
||||||
editorManager.editorView.dispatch({
|
|
||||||
changes: {
|
|
||||||
from: 0,
|
|
||||||
to: editorManager.editorView.state.doc.length,
|
|
||||||
insert: code,
|
|
||||||
},
|
|
||||||
annotations: [
|
|
||||||
codeManagerUpdateEvent,
|
|
||||||
Transaction.addToHistory.of(!clearHistory),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
editorManager.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: editorManager.editorState?.doc.length || 0,
|
||||||
|
insert: code,
|
||||||
|
},
|
||||||
|
annotations: [
|
||||||
|
codeManagerUpdateEvent,
|
||||||
|
Transaction.addToHistory.of(!clearHistory),
|
||||||
|
],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -213,16 +212,16 @@ function safeLSSetItem(key: string, value: string) {
|
|||||||
localStorage?.setItem(key, value)
|
localStorage?.setItem(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearCodeMirrorHistory(view: EditorView) {
|
function clearCodeMirrorHistory() {
|
||||||
// Clear history
|
// Clear history
|
||||||
view.dispatch({
|
editorManager.dispatch({
|
||||||
effects: [codeManagerHistoryCompartment.reconfigure([])],
|
effects: [historyCompartment.reconfigure([])],
|
||||||
annotations: [codeManagerUpdateEvent],
|
annotations: [codeManagerUpdateEvent],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add history back
|
// Add history back
|
||||||
view.dispatch({
|
editorManager.dispatch({
|
||||||
effects: [codeManagerHistoryCompartment.reconfigure([history()])],
|
effects: [historyCompartment.reconfigure([history()])],
|
||||||
annotations: [codeManagerUpdateEvent],
|
annotations: [codeManagerUpdateEvent],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -509,7 +509,7 @@ export async function promptToEditFlow({
|
|||||||
const ranges: SelectionRange[] = diff.insertRanges.map((range) =>
|
const ranges: SelectionRange[] = diff.insertRanges.map((range) =>
|
||||||
EditorSelection.range(range[0], range[1])
|
EditorSelection.range(range[0], range[1])
|
||||||
)
|
)
|
||||||
editorManager?.editorView?.dispatch({
|
editorManager?.getEditorView()?.dispatch({
|
||||||
selection: EditorSelection.create(
|
selection: EditorSelection.create(
|
||||||
ranges,
|
ranges,
|
||||||
selections.graphSelections.length - 1
|
selections.graphSelections.length - 1
|
||||||
|
@ -69,6 +69,7 @@ export const kclManager = new KclManager(engineCommandManager, {
|
|||||||
// method requires it for the current ast.
|
// method requires it for the current ast.
|
||||||
// CYCLIC REF
|
// CYCLIC REF
|
||||||
editorManager.kclManager = kclManager
|
editorManager.kclManager = kclManager
|
||||||
|
editorManager.codeManager = codeManager
|
||||||
|
|
||||||
// These are all late binding because of their circular dependency.
|
// These are all late binding because of their circular dependency.
|
||||||
// TODO: proper dependency injection.
|
// TODO: proper dependency injection.
|
||||||
|
@ -7,6 +7,8 @@ export type MenuLabels =
|
|||||||
| 'Help.Command Palette...'
|
| 'Help.Command Palette...'
|
||||||
| 'Help.Report a bug'
|
| 'Help.Report a bug'
|
||||||
| 'Help.Replay onboarding tutorial'
|
| 'Help.Replay onboarding tutorial'
|
||||||
|
| 'Edit.Undo'
|
||||||
|
| 'Edit.Redo'
|
||||||
| 'Edit.Rename project'
|
| 'Edit.Rename project'
|
||||||
| 'Edit.Delete project'
|
| 'Edit.Delete project'
|
||||||
| 'Edit.Change project directory'
|
| 'Edit.Change project directory'
|
||||||
|
@ -149,8 +149,24 @@ export const modelingEditRole = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'undo' },
|
{
|
||||||
{ role: 'redo' },
|
label: 'Undo',
|
||||||
|
accelerator: 'CmdOrCtrl+Z',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'Edit.Undo',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Redo',
|
||||||
|
accelerator: 'Shift+CmdOrCtrl+Z',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'Edit.Redo',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'cut' },
|
{ role: 'cut' },
|
||||||
{ role: 'copy' },
|
{ role: 'copy' },
|
||||||
|
@ -5,8 +5,12 @@ import type { SettingsType } from '@src/lib/settings/initialSettings'
|
|||||||
import { engineCommandManager, sceneInfra } from '@src/lib/singletons'
|
import { engineCommandManager, sceneInfra } from '@src/lib/singletons'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { uuidv4 } from '@src/lib/utils'
|
import { uuidv4 } from '@src/lib/utils'
|
||||||
import { authActor, settingsActor } from '@src/lib/singletons'
|
import {
|
||||||
import { commandBarActor } from '@src/lib/singletons'
|
authActor,
|
||||||
|
commandBarActor,
|
||||||
|
editorManager,
|
||||||
|
settingsActor,
|
||||||
|
} from '@src/lib/singletons'
|
||||||
import type { WebContentSendPayload } from '@src/menu/channels'
|
import type { WebContentSendPayload } from '@src/menu/channels'
|
||||||
import type { NavigateFunction } from 'react-router-dom'
|
import type { NavigateFunction } from 'react-router-dom'
|
||||||
|
|
||||||
@ -119,6 +123,10 @@ export function modelingMenuCallbackMostActions(
|
|||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'format-code', groupId: 'code' },
|
data: { name: 'format-code', groupId: 'code' },
|
||||||
})
|
})
|
||||||
|
} else if (data.menuLabel === 'Edit.Undo') {
|
||||||
|
editorManager.undo()
|
||||||
|
} else if (data.menuLabel === 'Edit.Redo') {
|
||||||
|
editorManager.redo()
|
||||||
} else if (data.menuLabel === 'View.Orthographic view') {
|
} else if (data.menuLabel === 'View.Orthographic view') {
|
||||||
settingsActor.send({
|
settingsActor.send({
|
||||||
type: 'set.modeling.cameraProjection',
|
type: 'set.modeling.cameraProjection',
|
||||||
|
@ -33,6 +33,8 @@ type EditRoleLabel =
|
|||||||
| 'Rename project'
|
| 'Rename project'
|
||||||
| 'Delete project'
|
| 'Delete project'
|
||||||
| 'Change project directory'
|
| 'Change project directory'
|
||||||
|
| 'Undo'
|
||||||
|
| 'Redo'
|
||||||
| 'Speech'
|
| 'Speech'
|
||||||
| 'Edit parameter'
|
| 'Edit parameter'
|
||||||
| 'Modify with Zoo Text-To-CAD'
|
| 'Modify with Zoo Text-To-CAD'
|
||||||
|
Reference in New Issue
Block a user