import { atom, map, type MapStore, type ReadableAtom, type WritableAtom } from 'nanostores'; import type { EditorDocument, ScrollPosition } from '../../components/editor/codemirror/CodeMirrorEditor'; import { unreachable } from '../../utils/unreachable'; import { ActionRunner } from '../runtime/action-runner'; import type { ActionCallbackData, ArtifactCallbackData } from '../runtime/message-parser'; import { webcontainer } from '../webcontainer'; import { EditorStore } from './editor'; import { FilesStore, type FileMap } from './files'; import { PreviewsStore } from './previews'; export interface ArtifactState { title: string; closed: boolean; runner: ActionRunner; } export type ArtifactUpdateState = Pick; type Artifacts = MapStore>; export class WorkbenchStore { #previewsStore = new PreviewsStore(webcontainer); #filesStore = new FilesStore(webcontainer); #editorStore = new EditorStore(this.#filesStore); artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({}); showWorkbench: WritableAtom = import.meta.hot?.data.showWorkbench ?? atom(false); unsavedFiles: WritableAtom> = import.meta.hot?.data.unsavedFiles ?? atom(new Set()); modifiedFiles = new Set(); constructor() { if (import.meta.hot) { import.meta.hot.data.artifacts = this.artifacts; import.meta.hot.data.unsavedFiles = this.unsavedFiles; import.meta.hot.data.showWorkbench = this.showWorkbench; } } get previews() { return this.#previewsStore.previews; } get files() { return this.#filesStore.files; } get currentDocument(): ReadableAtom { return this.#editorStore.currentDocument; } get selectedFile(): ReadableAtom { return this.#editorStore.selectedFile; } setDocuments(files: FileMap) { this.#editorStore.setDocuments(files); if (this.#filesStore.filesCount > 0 && this.currentDocument.get() === undefined) { // we find the first file and select it for (const [filePath, dirent] of Object.entries(files)) { if (dirent?.type === 'file') { this.setSelectedFile(filePath); break; } } } } setShowWorkbench(show: boolean) { this.showWorkbench.set(show); } setCurrentDocumentContent(newContent: string | Uint8Array) { const filePath = this.currentDocument.get()?.filePath; if (!filePath) { return; } const originalContent = this.#filesStore.getFile(filePath)?.content; const unsavedChanges = originalContent !== undefined && originalContent !== newContent; this.#editorStore.updateFile(filePath, newContent); const currentDocument = this.currentDocument.get(); if (currentDocument) { const previousUnsavedFiles = this.unsavedFiles.get(); if (unsavedChanges && previousUnsavedFiles.has(currentDocument.filePath)) { return; } const newUnsavedFiles = new Set(previousUnsavedFiles); if (unsavedChanges) { newUnsavedFiles.add(currentDocument.filePath); } else { newUnsavedFiles.delete(currentDocument.filePath); } this.unsavedFiles.set(newUnsavedFiles); } } setCurrentDocumentScrollPosition(position: ScrollPosition) { const editorDocument = this.currentDocument.get(); if (!editorDocument) { return; } const { filePath } = editorDocument; this.#editorStore.updateScrollPosition(filePath, position); } setSelectedFile(filePath: string | undefined) { this.#editorStore.setSelectedFile(filePath); } async saveCurrentDocument() { const currentDocument = this.currentDocument.get(); if (currentDocument === undefined) { return; } const { filePath } = currentDocument; await this.#filesStore.saveFile(filePath, currentDocument.value); const newUnsavedFiles = new Set(this.unsavedFiles.get()); newUnsavedFiles.delete(filePath); this.unsavedFiles.set(newUnsavedFiles); } resetCurrentDocument() { const currentDocument = this.currentDocument.get(); if (currentDocument === undefined) { return; } const { filePath } = currentDocument; const file = this.#filesStore.getFile(filePath); if (!file) { return; } this.setCurrentDocumentContent(file.content); } abortAllActions() { // TODO: what do we wanna do and how do we wanna recover from this? } addArtifact({ messageId, title }: ArtifactCallbackData) { const artifact = this.#getArtifact(messageId); if (artifact) { return; } this.artifacts.setKey(messageId, { title, closed: false, runner: new ActionRunner(webcontainer), }); } updateArtifact({ messageId }: ArtifactCallbackData, state: Partial) { const artifact = this.#getArtifact(messageId); if (!artifact) { return; } this.artifacts.setKey(messageId, { ...artifact, ...state }); } async addAction(data: ActionCallbackData) { const { messageId } = data; const artifact = this.#getArtifact(messageId); if (!artifact) { unreachable('Artifact not found'); } artifact.runner.addAction(data); } async runAction(data: ActionCallbackData) { const { messageId } = data; const artifact = this.#getArtifact(messageId); if (!artifact) { unreachable('Artifact not found'); } artifact.runner.runAction(data); } #getArtifact(id: string) { const artifacts = this.artifacts.get(); return artifacts[id]; } } export const workbenchStore = new WorkbenchStore();