From a0f3e7100014ebb972ec441cd10c6b0bf8a820f8 Mon Sep 17 00:00:00 2001 From: Misode Date: Wed, 23 Oct 2024 05:44:20 +0200 Subject: [PATCH] Refactor spyglass service and context --- src/app/components/ErrorPanel.tsx | 15 +- .../components/generator/SchemaGenerator.tsx | 36 ++-- src/app/components/generator/SourcePanel.tsx | 16 +- src/app/components/generator/Tree.tsx | 12 +- src/app/contexts/Spyglass.tsx | 36 +++- src/app/services/Spyglass.ts | 162 +++++++++--------- 6 files changed, 157 insertions(+), 120 deletions(-) diff --git a/src/app/components/ErrorPanel.tsx b/src/app/components/ErrorPanel.tsx index 3b8beca1..689158bc 100644 --- a/src/app/components/ErrorPanel.tsx +++ b/src/app/components/ErrorPanel.tsx @@ -18,18 +18,23 @@ type ErrorPanelProps = { } export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_, children }: ErrorPanelProps) { const { version } = useVersion() - const { spyglass } = useSpyglass() + const { service } = useSpyglass() const [stackVisible, setStackVisible] = useState(false) const [stack, setStack] = useState(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)) + if (!service || !gen) { + return undefined } - return undefined - }, [spyglass, version, gen]) + // TODO: read project file if open + const uri = service.getUnsavedFileUri(gen) + if (!uri) { + return undefined + } + return await service.readFile(uri) + }, [service, version, gen]) useEffect(() => { if (error instanceof Error) { diff --git a/src/app/components/generator/SchemaGenerator.tsx b/src/app/components/generator/SchemaGenerator.tsx index 2ba62dd8..94c4ea49 100644 --- a/src/app/components/generator/SchemaGenerator.tsx +++ b/src/app/components/generator/SchemaGenerator.tsx @@ -21,7 +21,7 @@ interface Props { export function SchemaGenerator({ gen, allowedVersions }: Props) { const { locale } = useLocale() const { version, changeVersion, changeTargetVersion } = useVersion() - const { spyglass } = useSpyglass() + const { service } = useSpyglass() const { projects, project, file, updateProject, updateFile, closeFile } = useProject() const [error, setError] = useState(null) const [errorBoundary, errorRetry] = useErrorBoundary() @@ -34,8 +34,8 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { const uri = useMemo(() => { // TODO: return different uri when project file is open - return spyglass?.getUnsavedFileUri(version, gen) - }, [spyglass, version, gen]) + return service?.getUnsavedFileUri(gen) + }, [service, version, gen]) const [currentPreset, setCurrentPreset] = useSearchParam('preset') const [sharedSnippetId, setSharedSnippetId] = useSearchParam(SHARE_KEY) @@ -82,14 +82,17 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { ignoreChange.current = true data = file.data } + if (!service || !uri) { + return AsyncCancel + } if (data) { - await spyglass.writeFile(version, uri, JSON.stringify(data)) + await service.writeFile(uri, JSON.stringify(data)) } // TODO: if data is undefined, set to generator's default - const docAndNode = await spyglass.getFile(version, uri, () => '{}') + const docAndNode = await service.getFile(uri, () => '{}') Analytics.setGenerator(gen.id) return docAndNode - }, [gen.id, version, sharedSnippetId, currentPreset, project.name, file?.id, spyglass]) + }, [gen.id, version, sharedSnippetId, currentPreset, project.name, file?.id, service]) const { doc } = docAndNode ?? {} @@ -112,23 +115,32 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { } const undo = async (e: MouseEvent) => { e.stopPropagation() + if (!service || !uri) { + return + } Analytics.undoGenerator(gen.id, 1, 'menu') - await spyglass.undoEdits(version, uri) + await service.undoEdits(uri) } const redo = async (e: MouseEvent) => { e.stopPropagation() + if (!service || !uri) { + return + } Analytics.redoGenerator(gen.id, 1, 'menu') - await spyglass.redoEdits(version, uri) + await service?.redoEdits(uri) } useEffect(() => { const onKeyUp = async (e: KeyboardEvent) => { + if (!service || !uri) { + return + } if (e.ctrlKey && e.key === 'z') { Analytics.undoGenerator(gen.id, 1, 'hotkey') - await spyglass.undoEdits(version, uri) + await service.undoEdits(uri) } else if (e.ctrlKey && e.key === 'y') { Analytics.redoGenerator(gen.id, 1, 'hotkey') - await spyglass.redoEdits(version, uri) + await service.redoEdits(uri) } } const onKeyDown = (e: KeyboardEvent) => { @@ -145,7 +157,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { document.removeEventListener('keyup', onKeyUp) document.removeEventListener('keydown', onKeyDown) } - }, [gen.id, spyglass, version, uri]) + }, [gen.id, service, uri]) const { value: presets } = useAsync(async () => { const registries = await fetchRegistries(version) @@ -338,7 +350,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
- +
diff --git a/src/app/components/generator/SourcePanel.tsx b/src/app/components/generator/SourcePanel.tsx index db415621..4e5887f8 100644 --- a/src/app/components/generator/SourcePanel.tsx +++ b/src/app/components/generator/SourcePanel.tsx @@ -1,11 +1,10 @@ import type { DocAndNode } from '@spyglassmc/core' import { fileUtil } from '@spyglassmc/core' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' -import { useLocale, useVersion } from '../../contexts/index.js' -import { useDocAndNode } from '../../contexts/Spyglass.jsx' +import { useLocale } from '../../contexts/index.js' +import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx' import { useLocalStorage } from '../../hooks/index.js' import { getSourceFormats, getSourceIndent, getSourceIndents, parseSource, sortData, stringifySource } from '../../services/index.js' -import type { Spyglass } from '../../services/Spyglass.js' import { Store } from '../../Store.js' import { message } from '../../Utils.js' import { Btn, BtnMenu } from '../index.js' @@ -18,7 +17,6 @@ interface Editor { } type SourcePanelProps = { - spyglass: Spyglass | undefined, docAndNode: DocAndNode | undefined, doCopy?: number, doDownload?: number, @@ -26,9 +24,9 @@ type SourcePanelProps = { copySuccess: () => unknown, onError: (message: string | Error) => unknown, } -export function SourcePanel({ spyglass, docAndNode, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) { +export function SourcePanel({ docAndNode, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) { const { locale } = useLocale() - const { version } = useVersion() + const { service } = useSpyglass() const [indent, setIndent] = useState(Store.getIndent()) const [format, setFormat] = useState(Store.getFormat()) const [sort, setSort] = useLocalStorage('misode_output_sort', 'schema') @@ -75,10 +73,10 @@ export function SourcePanel({ spyglass, docAndNode, doCopy, doDownload, doImport if (!editor.current) return const value = editor.current.getValue() if (value.length === 0) return - if (!spyglass || !docAndNode) return + if (!service || !docAndNode) return try { const data = await parseSource(value, format) - await spyglass.writeFile(version, docAndNode.doc.uri, JSON.stringify(data)) + await service.writeFile(docAndNode.doc.uri, JSON.stringify(data)) } catch (e) { if (e instanceof Error) { e.message = `Error importing: ${e.message}` @@ -89,7 +87,7 @@ export function SourcePanel({ spyglass, docAndNode, doCopy, doDownload, doImport console.error(e) } } - }, [spyglass, version, docAndNode, text, indent, format, sort, highlighting]) + }, [service, docAndNode, text, indent, format, sort, highlighting]) useEffect(() => { if (highlighting) { diff --git a/src/app/components/generator/Tree.tsx b/src/app/components/generator/Tree.tsx index bc8bb060..99f86580 100644 --- a/src/app/components/generator/Tree.tsx +++ b/src/app/components/generator/Tree.tsx @@ -1,7 +1,7 @@ import type { DocAndNode } from '@spyglassmc/core' import { JsonFileNode } from '@spyglassmc/json' import { useCallback, useErrorBoundary } from 'preact/hooks' -import { useLocale, useVersion } from '../../contexts/index.js' +import { useLocale } from '../../contexts/index.js' import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx' import type { Edit } from '../../services/Spyglass.js' import { McdocRoot } from './McdocRenderer.jsx' @@ -12,8 +12,7 @@ type TreePanelProps = { } export function Tree({ docAndNode, onError }: TreePanelProps) { const { lang } = useLocale() - const { version } = useVersion() - const { spyglass } = useSpyglass() + const { service } = useSpyglass() if (lang === 'none') return <> @@ -29,8 +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]) + if (!service) { + return + } + service.applyEdits(docAndNode.doc.uri, edits) + }, [service, docAndNode]) return
diff --git a/src/app/contexts/Spyglass.tsx b/src/app/contexts/Spyglass.tsx index 3a8d2e66..6d85602c 100644 --- a/src/app/contexts/Spyglass.tsx +++ b/src/app/contexts/Spyglass.tsx @@ -3,10 +3,14 @@ import type { ComponentChildren } from 'preact' import { createContext } from 'preact' import type { Inputs } from 'preact/hooks' import { useContext, useEffect, useState } from 'preact/hooks' -import { Spyglass } from '../services/Spyglass.js' +import { useAsync } from '../hooks/useAsync.js' +import type { SpyglassService } from '../services/Spyglass.js' +import { SpyglassClient } from '../services/Spyglass.js' +import { useVersion } from './Version.jsx' interface SpyglassContext { - spyglass: Spyglass, + client: SpyglassClient + service: SpyglassService | undefined } const SpyglassContext = createContext(undefined) @@ -24,15 +28,15 @@ export function watchSpyglassUri( handler: (docAndNode: DocAndNode) => void, inputs: Inputs = [], ) { - const { spyglass } = useSpyglass() + const { service } = useSpyglass() useEffect(() => { - if (!uri || !spyglass) { + if (!uri || !service) { return } - spyglass.watchFile(uri, handler) - return () => spyglass.unwatchFile(uri, handler) - }, [spyglass, uri, handler, ...inputs]) + service.watchFile(uri, handler) + return () => service.unwatchFile(uri, handler) + }, [service, uri, handler, ...inputs]) } export function useDocAndNode(original: DocAndNode, inputs?: Inputs): DocAndNode @@ -52,10 +56,24 @@ export function useDocAndNode(original: DocAndNode | undefined, inputs: Inputs = } export function SpyglassProvider({ children }: { children: ComponentChildren }) { - const [spyglass] = useState(new Spyglass()) + const { version } = useVersion() + const [client] = useState(new SpyglassClient()) + + const { value: service, error } = useAsync(() => { + return client.createService(version) + }, [client, version]) + + useEffect(() => { + if (error) { + console.warn(error) + } + }, [error]) + + console.log('->', service) const value: SpyglassContext = { - spyglass, + client, + service, } return diff --git a/src/app/services/Spyglass.ts b/src/app/services/Spyglass.ts index 2fdea40b..a464af8d 100644 --- a/src/app/services/Spyglass.ts +++ b/src/app/services/Spyglass.ts @@ -22,15 +22,14 @@ export interface Edit { text: string } -interface DocumentData { +interface ClientDocument { doc: TextDocument undoStack: string[] redoStack: string[] } -export class Spyglass { - private static readonly LOGGER: core.Logger = console - private static readonly EXTERNALS: core.Externals = { +export class SpyglassClient { + public readonly externals: core.Externals = { ...BrowserExternals, archive: { ...BrowserExternals.archive, @@ -38,65 +37,81 @@ export class Spyglass { }, } - private readonly instances = new Map>() - private readonly documents = new Map() + public readonly documents = new Map() + + public async createService(version: VersionId) { + return SpyglassService.create(version, this) + } +} + +export class SpyglassService { private readonly watchers = new Map void)[]>() - public async getFile(version: VersionId, uri: string, emptyContent?: () => string) { - const service = await this.getService(version) - const document = this.documents.get(uri) - let docAndNode: core.DocAndNode | undefined - if (document === undefined) { - Spyglass.LOGGER.info(`[Spyglass#openFile] Opening file with content from fs: ${uri}`) + private constructor ( + public readonly version: VersionId, + private readonly service: core.Service, + private readonly client: SpyglassClient, + ) { + service.project.on('documentUpdated', (e) => { + const uriWatchers = this.watchers.get(e.doc.uri) ?? [] + for (const handler of uriWatchers) { + handler(e) + } + }) + } + + public async getFile(uri: string, emptyContent?: () => string) { + let docAndNode = this.service.project.getClientManaged(uri) + if (docAndNode === undefined) { + console.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 { - Spyglass.LOGGER.info(`[Spyglass#openFile] Opening already open file: ${uri}`) - docAndNode = service.project.getClientManaged(uri) + await this.service.project.onDidOpen(doc.uri, doc.languageId, doc.version, doc.getText()) + docAndNode = await this.service.project.ensureClientManagedChecked(uri) } if (!docAndNode) { throw new Error(`[Spyglass#openFile] Cannot get doc and node: ${uri}`) } + const document = this.client.documents.get(uri) + if (document === undefined) { + this.client.documents.set(uri, { doc: docAndNode.doc, undoStack: [], redoStack: [] }) + } return docAndNode } public async readFile(uri: string): Promise { try { - const buffer = await Spyglass.EXTERNALS.fs.readFile(uri) + const buffer = await this.service.project.externals.fs.readFile(uri) return new TextDecoder().decode(buffer) } catch (e) { return undefined } } - 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) + private async notifyChange(doc: TextDocument) { + await this.service.project.onDidChange(doc.uri, [{ text: doc.getText() }], doc.version + 1) + const docAndNode = this.service.project.getClientManaged(doc.uri) if (docAndNode) { - service.project.emit('documentUpdated', docAndNode) + this.service.project.emit('documentUpdated', docAndNode) } return docAndNode } - public async writeFile(versionId: VersionId, uri: string, content: string) { - const document = this.documents.get(uri) + public async writeFile(uri: string, content: string) { + const document = this.client.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) + await this.service.project.externals.fs.writeFile(uri, content) if (document) { - await this.notifyChange(versionId, document.doc) + await this.notifyChange(document.doc) } } - public async applyEdits(versionId: VersionId, uri: string, edits: Edit[]) { - const document = this.documents.get(uri) + public async applyEdits(uri: string, edits: Edit[]) { + const document = this.client.documents.get(uri) if (document !== undefined) { document.undoStack.push(document.doc.getText()) document.redoStack = [] @@ -104,13 +119,13 @@ export class Spyglass { 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) + await this.service.project.externals.fs.writeFile(uri, document.doc.getText()) + await this.notifyChange(document.doc) } } - public async undoEdits(versionId: VersionId, uri: string) { - const document = this.documents.get(uri) + public async undoEdits(uri: string) { + const document = this.client.documents.get(uri) if (document === undefined) { throw new Error(`[Spyglass#undoEdits] Document doesn't exist: ${uri}`) } @@ -120,12 +135,12 @@ export class Spyglass { } 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) + await this.service.project.externals.fs.writeFile(uri, document.doc.getText()) + await this.notifyChange(document.doc) } - public async redoEdits(versionId: VersionId, uri: string) { - const document = this.documents.get(uri) + public async redoEdits(uri: string) { + const document = this.client.documents.get(uri) if (document === undefined) { throw new Error(`[Spyglass#redoEdits] Document doesn't exist: ${uri}`) } @@ -135,15 +150,15 @@ export class Spyglass { } 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) + await this.service.project.externals.fs.writeFile(uri, document.doc.getText()) + await this.notifyChange(document.doc) } - public getUnsavedFileUri(versionId: VersionId, gen: ConfigGenerator) { + public getUnsavedFileUri(gen: ConfigGenerator) { if (gen.id === 'pack_mcmeta') { return 'file:///project/pack.mcmeta' } - return `file:///project/data/draft/${genPath(gen, versionId)}/unsaved.json` + return `file:///project/data/draft/${genPath(gen, this.version)}/unsaved.json` } public watchFile(uri: string, handler: (docAndNode: core.DocAndNode) => void) { @@ -157,44 +172,31 @@ export class Spyglass { uriWatchers.splice(index, 1) } - private async getService(versionId: VersionId) { - const instance = this.instances.get(versionId) - if (instance) { - return instance - } - const promise = (async () => { - const version = siteConfig.versions.find(v => v.id === versionId)! - const service = new core.Service({ - logger: Spyglass.LOGGER, - profilers: new core.ProfilerFactory(Spyglass.LOGGER, [ - 'project#init', - 'project#ready', - ]), - project: { - cacheRoot: 'file:///cache/', - projectRoots: ['file:///project/'], - externals: Spyglass.EXTERNALS, - defaultConfig: core.ConfigService.merge(core.VanillaConfig, { - env: { - gameVersion: version.ref ?? version.id, - dependencies: ['@vanilla-mcdoc'], - }, - }), - initializers: [mcdoc.initialize, initialize], - }, - }) - await service.project.ready() - await service.project.cacheService.save() - service.project.on('documentUpdated', (e) => { - const uriWatchers = this.watchers.get(e.doc.uri) ?? [] - for (const handler of uriWatchers) { - handler(e) - } - }) - return service - })() - this.instances.set(versionId, promise) - return promise + public static async create(versionId: VersionId, client: SpyglassClient) { + const version = siteConfig.versions.find(v => v.id === versionId)! + const logger = console + const service = new core.Service({ + logger, + profilers: new core.ProfilerFactory(logger, [ + 'project#init', + 'project#ready', + ]), + project: { + cacheRoot: 'file:///cache/', + projectRoots: ['file:///project/'], + externals: client.externals, + defaultConfig: core.ConfigService.merge(core.VanillaConfig, { + env: { + gameVersion: version.ref ?? version.id, + dependencies: ['@vanilla-mcdoc'], + }, + }), + initializers: [mcdoc.initialize, initialize], + }, + }) + await service.project.ready() + await service.project.cacheService.save() + return new SpyglassService(versionId, service, client) } }