mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-26 00:16:51 +00:00
Implement undo and redo
This commit is contained in:
@@ -3,6 +3,7 @@ import { getCurrentUrl } from 'preact-router'
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { useSpyglass } from '../contexts/Spyglass.jsx'
|
||||
import { useVersion } from '../contexts/Version.jsx'
|
||||
import { useAsync } from '../hooks/useAsync.js'
|
||||
import { latestVersion } from '../services/DataFetcher.js'
|
||||
import { getGenerator } from '../Utils.js'
|
||||
import { Octicon } from './index.js'
|
||||
@@ -21,9 +22,14 @@ export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_,
|
||||
const [stackVisible, setStackVisible] = useState(false)
|
||||
const [stack, setStack] = useState<string | undefined>(undefined)
|
||||
|
||||
const gen = getGenerator(getCurrentUrl())
|
||||
const source = gen ? spyglass?.getFileContents(spyglass.getUnsavedFileUri(version, gen)) : undefined
|
||||
const name = (prefix ?? '') + (error instanceof Error ? error.message : error)
|
||||
const gen = getGenerator(getCurrentUrl())
|
||||
const { value: source } = useAsync(async () => {
|
||||
if (gen) {
|
||||
return await spyglass?.readFile(spyglass.getUnsavedFileUri(version, gen))
|
||||
}
|
||||
return undefined
|
||||
}, [spyglass, version, gen])
|
||||
|
||||
useEffect(() => {
|
||||
if (error instanceof Error) {
|
||||
@@ -57,7 +63,7 @@ export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_,
|
||||
body += `\n### Stack trace\n\`\`\`\n${fullName}\n${stack}\n\`\`\`\n`
|
||||
}
|
||||
if (source) {
|
||||
body += `\n### Generator JSON\n<details>\n<pre>\n${JSON.stringify(source, null, 2)}\n</pre>\n</details>\n`
|
||||
body += `\n### Generator JSON\n<details>\n<pre>\n${source}\n</pre>\n</details>\n`
|
||||
}
|
||||
if (body_) {
|
||||
body += body_
|
||||
|
||||
@@ -3,23 +3,25 @@ import { JsonArrayNode, JsonBooleanNode, JsonNumberNode, JsonObjectNode, JsonStr
|
||||
import type { ListType, LiteralType, McdocType } from '@spyglassmc/mcdoc'
|
||||
import type { SimplifiedStructType } from '@spyglassmc/mcdoc/lib/runtime/checker/index.js'
|
||||
import { useLocale } from '../../contexts/Locale.jsx'
|
||||
import type { Edit } from '../../services/Spyglass.js'
|
||||
import { Octicon } from '../Octicon.jsx'
|
||||
|
||||
interface Props {
|
||||
node: JsonNode | undefined
|
||||
makeEdits: (edits: Edit[]) => void
|
||||
}
|
||||
export function McdocRoot({ node } : Props) {
|
||||
export function McdocRoot({ node, makeEdits } : Props) {
|
||||
const type = node?.typeDef ?? { kind: 'unsafe' }
|
||||
|
||||
if (type.kind === 'struct') {
|
||||
return <StructBody type={type} node={node} />
|
||||
return <StructBody type={type} node={node} makeEdits={makeEdits} />
|
||||
}
|
||||
|
||||
return <>
|
||||
<div class="node-header">
|
||||
<Head simpleType={type} node={node} />
|
||||
<Head simpleType={type} node={node} makeEdits={makeEdits} />
|
||||
</div>
|
||||
<Body simpleType={type} node={node} />
|
||||
<Body simpleType={type} node={node} makeEdits={makeEdits} />
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -89,19 +91,23 @@ function Head({ simpleType, optional, node }: HeadProps) {
|
||||
interface BodyProps extends Props {
|
||||
simpleType: McdocType
|
||||
}
|
||||
function Body({ simpleType, node }: BodyProps) {
|
||||
function Body({ simpleType, node, makeEdits }: BodyProps) {
|
||||
const type = node?.typeDef ?? simpleType
|
||||
if (node?.typeDef?.kind === 'struct') {
|
||||
if (node.typeDef.fields.length === 0) {
|
||||
return <></>
|
||||
}
|
||||
return <div class="node-body">
|
||||
<StructBody type={node.typeDef} node={node} />
|
||||
<StructBody type={node.typeDef} node={node} makeEdits={makeEdits} />
|
||||
</div>
|
||||
}
|
||||
if (node?.typeDef?.kind === 'list') {
|
||||
const fixedRange = node.typeDef.lengthRange?.min !== undefined && node.typeDef.lengthRange.min === node.typeDef.lengthRange.max
|
||||
if (!fixedRange && node.children?.length === 0) {
|
||||
return <></>
|
||||
}
|
||||
return <div class="node-body">
|
||||
<ListBody type={node.typeDef} node={node} />
|
||||
<ListBody type={node.typeDef} node={node} makeEdits={makeEdits} />
|
||||
</div>
|
||||
}
|
||||
if (type.kind === 'byte' || type.kind === 'short' || type.kind === 'int' || type.kind === 'boolean') {
|
||||
@@ -114,7 +120,7 @@ function Body({ simpleType, node }: BodyProps) {
|
||||
interface StructBodyProps extends Props {
|
||||
type: SimplifiedStructType
|
||||
}
|
||||
function StructBody({ type, node }: StructBodyProps) {
|
||||
function StructBody({ type, node, makeEdits }: StructBodyProps) {
|
||||
if (!JsonObjectNode.is(node)) {
|
||||
return <></>
|
||||
}
|
||||
@@ -133,9 +139,9 @@ function StructBody({ type, node }: StructBodyProps) {
|
||||
return <div class="node">
|
||||
<div class="node-header">
|
||||
<Key label={key} />
|
||||
<Head simpleType={field.type} node={child} optional={field.optional} />
|
||||
<Head simpleType={field.type} node={child} optional={field.optional} makeEdits={makeEdits} />
|
||||
</div>
|
||||
<Body simpleType={field.type} node={child} />
|
||||
<Body simpleType={field.type} node={child} makeEdits={makeEdits} />
|
||||
</div>
|
||||
})}
|
||||
</>
|
||||
@@ -150,7 +156,7 @@ function Key({ label }: { label: string | number | boolean }) {
|
||||
interface ListBodyProps extends Props {
|
||||
type: ListType
|
||||
}
|
||||
function ListBody({ type, node }: ListBodyProps) {
|
||||
function ListBody({ type, node, makeEdits }: ListBodyProps) {
|
||||
const { locale } = useLocale()
|
||||
if (!JsonArrayNode.is(node)) {
|
||||
return <></>
|
||||
@@ -160,7 +166,7 @@ function ListBody({ type, node }: ListBodyProps) {
|
||||
const child = item.value
|
||||
return <div class="node">
|
||||
<div class="node-header">
|
||||
<button class="remove tooltipped tip-se" aria-label={locale('remove')}>
|
||||
<button class="remove tooltipped tip-se" aria-label={locale('remove')} onClick={() => makeEdits([{ range: item.range, text: '' }])}>
|
||||
{Octicon.trashcan}
|
||||
</button>
|
||||
{node.children.length > 1 && <div class="node-move">
|
||||
@@ -172,9 +178,9 @@ function ListBody({ type, node }: ListBodyProps) {
|
||||
</button>
|
||||
</div>}
|
||||
<Key label="entry" />
|
||||
<Head simpleType={type.item} node={child} />
|
||||
<Head simpleType={type.item} node={child} makeEdits={makeEdits} />
|
||||
</div>
|
||||
<Body simpleType={type.item} node={child} />
|
||||
<Body simpleType={type.item} node={child} makeEdits={makeEdits} />
|
||||
</div>
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -110,25 +110,25 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
|
||||
Analytics.resetGenerator(gen.id, 1, 'menu')
|
||||
// TODO
|
||||
}
|
||||
const undo = (e: MouseEvent) => {
|
||||
const undo = async (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
Analytics.undoGenerator(gen.id, 1, 'menu')
|
||||
// TODO
|
||||
await spyglass.undoEdits(version, uri)
|
||||
}
|
||||
const redo = (e: MouseEvent) => {
|
||||
const redo = async (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
Analytics.redoGenerator(gen.id, 1, 'menu')
|
||||
// TODO
|
||||
await spyglass.redoEdits(version, uri)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
const onKeyUp = async (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === 'z') {
|
||||
Analytics.undoGenerator(gen.id, 1, 'hotkey')
|
||||
// TODO
|
||||
await spyglass.undoEdits(version, uri)
|
||||
} else if (e.ctrlKey && e.key === 'y') {
|
||||
Analytics.redoGenerator(gen.id, 1, 'hotkey')
|
||||
// TODO
|
||||
await spyglass.redoEdits(version, uri)
|
||||
}
|
||||
}
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -145,7 +145,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
|
||||
document.removeEventListener('keyup', onKeyUp)
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [gen.id])
|
||||
}, [gen.id, spyglass, version, uri])
|
||||
|
||||
const { value: presets } = useAsync(async () => {
|
||||
const registries = await fetchRegistries(version)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { DocAndNode } from '@spyglassmc/core'
|
||||
import { JsonFileNode } from '@spyglassmc/json'
|
||||
import { useErrorBoundary } from 'preact/hooks'
|
||||
import { useLocale } from '../../contexts/index.js'
|
||||
import { useDocAndNode } from '../../contexts/Spyglass.jsx'
|
||||
import { useCallback, useErrorBoundary } from 'preact/hooks'
|
||||
import { useLocale, useVersion } from '../../contexts/index.js'
|
||||
import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx'
|
||||
import type { Edit } from '../../services/Spyglass.js'
|
||||
import { McdocRoot } from './McdocRenderer.jsx'
|
||||
|
||||
type TreePanelProps = {
|
||||
@@ -11,6 +12,9 @@ type TreePanelProps = {
|
||||
}
|
||||
export function Tree({ docAndNode, onError }: TreePanelProps) {
|
||||
const { lang } = useLocale()
|
||||
const { version } = useVersion()
|
||||
const { spyglass } = useSpyglass()
|
||||
|
||||
if (lang === 'none') return <></>
|
||||
|
||||
const fileChild = useDocAndNode(docAndNode).node.children[0]
|
||||
@@ -24,7 +28,11 @@ export function Tree({ docAndNode, onError }: TreePanelProps) {
|
||||
})
|
||||
if (error) return <></>
|
||||
|
||||
const makeEdits = useCallback((edits: Edit[]) => {
|
||||
spyglass.applyEdits(version, docAndNode.doc.uri, edits)
|
||||
}, [spyglass, version, docAndNode])
|
||||
|
||||
return <div class="tree node-root" data-cy="tree">
|
||||
<McdocRoot node={fileChild.children[0]} />
|
||||
<McdocRoot node={fileChild.children[0]} makeEdits={makeEdits} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { localize } from '@spyglassmc/locales'
|
||||
import * as mcdoc from '@spyglassmc/mcdoc'
|
||||
import * as nbt from '@spyglassmc/nbt'
|
||||
import * as zip from '@zip.js/zip.js'
|
||||
import type { TextEdit } from 'vscode-languageserver-textdocument'
|
||||
import type { Position, Range } from 'vscode-languageserver-textdocument'
|
||||
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||
import type { ConfigGenerator, ConfigVersion } from '../Config.js'
|
||||
import siteConfig from '../Config.js'
|
||||
@@ -17,10 +17,15 @@ import { computeIfAbsent, genPath } from '../Utils.js'
|
||||
import { fetchBlockStates, fetchRegistries, fetchVanillaMcdoc, getVersionChecksum } from './DataFetcher.js'
|
||||
import type { VersionId } from './Versions.js'
|
||||
|
||||
export interface Edit {
|
||||
range?: core.Range
|
||||
text: string
|
||||
}
|
||||
|
||||
interface DocumentData {
|
||||
doc: TextDocument
|
||||
undoStack: { edits: TextEdit[] }[]
|
||||
redoStack: { edits: TextEdit[] }[]
|
||||
undoStack: string[]
|
||||
redoStack: string[]
|
||||
}
|
||||
|
||||
export class Spyglass {
|
||||
@@ -42,22 +47,15 @@ export class Spyglass {
|
||||
const document = this.documents.get(uri)
|
||||
let docAndNode: core.DocAndNode | undefined
|
||||
if (document === undefined) {
|
||||
let doc: TextDocument
|
||||
try {
|
||||
const buffer = await Spyglass.EXTERNALS.fs.readFile(uri)
|
||||
const content = new TextDecoder().decode(buffer)
|
||||
doc = TextDocument.create(uri, 'json', 1, content)
|
||||
Spyglass.LOGGER.info(`[Spyglass#openFile] Opening file with content from fs: ${uri}`)
|
||||
} catch (e) {
|
||||
doc = TextDocument.create(uri, 'json', 1, emptyContent ? emptyContent() : '')
|
||||
Spyglass.LOGGER.info(`[Spyglass#openFile] Opening empty file: ${uri}`)
|
||||
}
|
||||
Spyglass.LOGGER.info(`[Spyglass#openFile] Opening file with content from fs: ${uri}`)
|
||||
const content = await this.readFile(uri)
|
||||
const doc = TextDocument.create(uri, 'json', 1, content ?? (emptyContent ? emptyContent() : ''))
|
||||
this.documents.set(uri, { doc, undoStack: [], redoStack: [] })
|
||||
await service.project.onDidOpen(doc.uri, doc.languageId, doc.version, doc.getText())
|
||||
docAndNode = await service.project.ensureClientManagedChecked(uri)
|
||||
} else {
|
||||
docAndNode = service.project.getClientManaged(uri)
|
||||
Spyglass.LOGGER.info(`[Spyglass#openFile] Opening already open file: ${uri}`)
|
||||
docAndNode = service.project.getClientManaged(uri)
|
||||
}
|
||||
if (!docAndNode) {
|
||||
throw new Error(`[Spyglass#openFile] Cannot get doc and node: ${uri}`)
|
||||
@@ -65,22 +63,80 @@ export class Spyglass {
|
||||
return docAndNode
|
||||
}
|
||||
|
||||
public async writeFile(versionId: VersionId, uri: string, content: string) {
|
||||
await Spyglass.EXTERNALS.fs.writeFile(uri, content)
|
||||
Spyglass.LOGGER.info(`[Spyglass#writeFile] Writing file: ${uri} ${content.substring(0, 50)}`)
|
||||
const doc = this.documents.get(uri)?.doc
|
||||
if (doc !== undefined) {
|
||||
const service = await this.getService(versionId)
|
||||
await service.project.onDidChange(doc.uri, [{ text: content }], doc.version + 1)
|
||||
const docAndNode = service.project.getClientManaged(doc.uri)
|
||||
if (docAndNode) {
|
||||
service.project.emit('documentUpdated', docAndNode)
|
||||
}
|
||||
public async readFile(uri: string): Promise<string | undefined> {
|
||||
try {
|
||||
const buffer = await Spyglass.EXTERNALS.fs.readFile(uri)
|
||||
return new TextDecoder().decode(buffer)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
public getFileContents(_uri: string): string | undefined {
|
||||
return undefined // TODO
|
||||
private async notifyChange(versionId: VersionId, doc: TextDocument) {
|
||||
const service = await this.getService(versionId)
|
||||
await service.project.onDidChange(doc.uri, [{ text: doc.getText() }], doc.version + 1)
|
||||
const docAndNode = service.project.getClientManaged(doc.uri)
|
||||
if (docAndNode) {
|
||||
service.project.emit('documentUpdated', docAndNode)
|
||||
}
|
||||
return docAndNode
|
||||
}
|
||||
|
||||
public async writeFile(versionId: VersionId, uri: string, content: string) {
|
||||
const document = this.documents.get(uri)
|
||||
if (document !== undefined) {
|
||||
document.undoStack.push(document.doc.getText())
|
||||
document.redoStack = []
|
||||
TextDocument.update(document.doc, [{ text: content }], document.doc.version + 1)
|
||||
}
|
||||
await Spyglass.EXTERNALS.fs.writeFile(uri, content)
|
||||
if (document) {
|
||||
await this.notifyChange(versionId, document.doc)
|
||||
}
|
||||
}
|
||||
|
||||
public async applyEdits(versionId: VersionId, uri: string, edits: Edit[]) {
|
||||
const document = this.documents.get(uri)
|
||||
if (document !== undefined) {
|
||||
document.undoStack.push(document.doc.getText())
|
||||
document.redoStack = []
|
||||
TextDocument.update(document.doc, edits.map(e => ({
|
||||
range: e.range ? getLsRange(e.range, document.doc) : undefined,
|
||||
text: e.text,
|
||||
})), document.doc.version + 1)
|
||||
await Spyglass.EXTERNALS.fs.writeFile(uri, document.doc.getText())
|
||||
await this.notifyChange(versionId, document.doc)
|
||||
}
|
||||
}
|
||||
|
||||
public async undoEdits(versionId: VersionId, uri: string) {
|
||||
const document = this.documents.get(uri)
|
||||
if (document === undefined) {
|
||||
throw new Error(`[Spyglass#undoEdits] Document doesn't exist: ${uri}`)
|
||||
}
|
||||
const lastUndo = document.undoStack.pop()
|
||||
if (lastUndo === undefined) {
|
||||
return
|
||||
}
|
||||
document.redoStack.push(document.doc.getText())
|
||||
TextDocument.update(document.doc, [{ text: lastUndo }], document.doc.version + 1)
|
||||
await Spyglass.EXTERNALS.fs.writeFile(uri, document.doc.getText())
|
||||
await this.notifyChange(versionId, document.doc)
|
||||
}
|
||||
|
||||
public async redoEdits(versionId: VersionId, uri: string) {
|
||||
const document = this.documents.get(uri)
|
||||
if (document === undefined) {
|
||||
throw new Error(`[Spyglass#redoEdits] Document doesn't exist: ${uri}`)
|
||||
}
|
||||
const lastRedo = document.redoStack.pop()
|
||||
if (lastRedo === undefined) {
|
||||
return
|
||||
}
|
||||
document.undoStack.push(document.doc.getText())
|
||||
TextDocument.update(document.doc, [{ text: lastRedo }], document.doc.version + 1)
|
||||
await Spyglass.EXTERNALS.fs.writeFile(uri, document.doc.getText())
|
||||
await this.notifyChange(versionId, document.doc)
|
||||
}
|
||||
|
||||
public getUnsavedFileUri(versionId: VersionId, gen: ConfigGenerator) {
|
||||
@@ -272,3 +328,11 @@ function registerAttributes(meta: core.MetaRegistry, release: ReleaseVersion) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function getLsPosition(offset: number, doc: TextDocument): Position {
|
||||
return doc.positionAt(offset)
|
||||
}
|
||||
|
||||
export function getLsRange(range: core.Range, doc: TextDocument): Range {
|
||||
return { start: getLsPosition(range.start, doc), end: getLsPosition(range.end, doc) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user