Spaces:
Running
Running
import { atom, map, type MapStore, type ReadableAtom, type WritableAtom } from 'nanostores'; | |
import type { EditorDocument, ScrollPosition } from '~/components/editor/codemirror/CodeMirrorEditor'; | |
import { ActionRunner } from '~/lib/runtime/action-runner'; | |
import type { ActionCallbackData, ArtifactCallbackData } from '~/lib/runtime/message-parser'; | |
import { webcontainer } from '~/lib/webcontainer'; | |
import type { ITerminal } from '~/types/terminal'; | |
import { unreachable } from '~/utils/unreachable'; | |
import { EditorStore } from './editor'; | |
import { FilesStore, type FileMap } from './files'; | |
import { PreviewsStore } from './previews'; | |
import { TerminalStore } from './terminal'; | |
export interface ArtifactState { | |
id: string; | |
title: string; | |
closed: boolean; | |
runner: ActionRunner; | |
} | |
export type ArtifactUpdateState = Pick<ArtifactState, 'title' | 'closed'>; | |
type Artifacts = MapStore<Record<string, ArtifactState>>; | |
export type WorkbenchViewType = 'code' | 'preview'; | |
export class WorkbenchStore { | |
#previewsStore = new PreviewsStore(webcontainer); | |
#filesStore = new FilesStore(webcontainer); | |
#editorStore = new EditorStore(this.#filesStore); | |
#terminalStore = new TerminalStore(webcontainer); | |
artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({}); | |
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false); | |
currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code'); | |
unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>()); | |
modifiedFiles = new Set<string>(); | |
artifactIdList: string[] = []; | |
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; | |
import.meta.hot.data.currentView = this.currentView; | |
} | |
} | |
get previews() { | |
return this.#previewsStore.previews; | |
} | |
get files() { | |
return this.#filesStore.files; | |
} | |
get currentDocument(): ReadableAtom<EditorDocument | undefined> { | |
return this.#editorStore.currentDocument; | |
} | |
get selectedFile(): ReadableAtom<string | undefined> { | |
return this.#editorStore.selectedFile; | |
} | |
get firstArtifact(): ArtifactState | undefined { | |
return this.#getArtifact(this.artifactIdList[0]); | |
} | |
get filesCount(): number { | |
return this.#filesStore.filesCount; | |
} | |
get showTerminal() { | |
return this.#terminalStore.showTerminal; | |
} | |
toggleTerminal(value?: boolean) { | |
this.#terminalStore.toggleTerminal(value); | |
} | |
attachTerminal(terminal: ITerminal) { | |
this.#terminalStore.attachTerminal(terminal); | |
} | |
onTerminalResize(cols: number, rows: number) { | |
this.#terminalStore.onTerminalResize(cols, rows); | |
} | |
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) { | |
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 saveFile(filePath: string) { | |
const documents = this.#editorStore.documents.get(); | |
const document = documents[filePath]; | |
if (document === undefined) { | |
return; | |
} | |
await this.#filesStore.saveFile(filePath, document.value); | |
const newUnsavedFiles = new Set(this.unsavedFiles.get()); | |
newUnsavedFiles.delete(filePath); | |
this.unsavedFiles.set(newUnsavedFiles); | |
} | |
async saveCurrentDocument() { | |
const currentDocument = this.currentDocument.get(); | |
if (currentDocument === undefined) { | |
return; | |
} | |
await this.saveFile(currentDocument.filePath); | |
} | |
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); | |
} | |
async saveAllFiles() { | |
for (const filePath of this.unsavedFiles.get()) { | |
await this.saveFile(filePath); | |
} | |
} | |
getFileModifcations() { | |
return this.#filesStore.getFileModifications(); | |
} | |
resetAllFileModifications() { | |
this.#filesStore.resetFileModifications(); | |
} | |
abortAllActions() { | |
// TODO: what do we wanna do and how do we wanna recover from this? | |
} | |
addArtifact({ messageId, title, id }: ArtifactCallbackData) { | |
const artifact = this.#getArtifact(messageId); | |
if (artifact) { | |
return; | |
} | |
if (!this.artifactIdList.includes(messageId)) { | |
this.artifactIdList.push(messageId); | |
} | |
this.artifacts.setKey(messageId, { | |
id, | |
title, | |
closed: false, | |
runner: new ActionRunner(webcontainer), | |
}); | |
} | |
updateArtifact({ messageId }: ArtifactCallbackData, state: Partial<ArtifactUpdateState>) { | |
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(); | |