fix: remove monorepo
This commit is contained in:
7
app/lib/stores/chat.ts
Normal file
7
app/lib/stores/chat.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { map } from 'nanostores';
|
||||
|
||||
export const chatStore = map({
|
||||
started: false,
|
||||
aborted: false,
|
||||
showChat: true,
|
||||
});
|
||||
95
app/lib/stores/editor.ts
Normal file
95
app/lib/stores/editor.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { atom, computed, map, type MapStore, type WritableAtom } from 'nanostores';
|
||||
import type { EditorDocument, ScrollPosition } from '~/components/editor/codemirror/CodeMirrorEditor';
|
||||
import type { FileMap, FilesStore } from './files';
|
||||
|
||||
export type EditorDocuments = Record<string, EditorDocument>;
|
||||
|
||||
type SelectedFile = WritableAtom<string | undefined>;
|
||||
|
||||
export class EditorStore {
|
||||
#filesStore: FilesStore;
|
||||
|
||||
selectedFile: SelectedFile = import.meta.hot?.data.selectedFile ?? atom<string | undefined>();
|
||||
documents: MapStore<EditorDocuments> = import.meta.hot?.data.documents ?? map({});
|
||||
|
||||
currentDocument = computed([this.documents, this.selectedFile], (documents, selectedFile) => {
|
||||
if (!selectedFile) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return documents[selectedFile];
|
||||
});
|
||||
|
||||
constructor(filesStore: FilesStore) {
|
||||
this.#filesStore = filesStore;
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.documents = this.documents;
|
||||
import.meta.hot.data.selectedFile = this.selectedFile;
|
||||
}
|
||||
}
|
||||
|
||||
setDocuments(files: FileMap) {
|
||||
const previousDocuments = this.documents.value;
|
||||
|
||||
this.documents.set(
|
||||
Object.fromEntries<EditorDocument>(
|
||||
Object.entries(files)
|
||||
.map(([filePath, dirent]) => {
|
||||
if (dirent === undefined || dirent.type === 'folder') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const previousDocument = previousDocuments?.[filePath];
|
||||
|
||||
return [
|
||||
filePath,
|
||||
{
|
||||
value: dirent.content,
|
||||
filePath,
|
||||
scroll: previousDocument?.scroll,
|
||||
},
|
||||
] as [string, EditorDocument];
|
||||
})
|
||||
.filter(Boolean) as Array<[string, EditorDocument]>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedFile(filePath: string | undefined) {
|
||||
this.selectedFile.set(filePath);
|
||||
}
|
||||
|
||||
updateScrollPosition(filePath: string, position: ScrollPosition) {
|
||||
const documents = this.documents.get();
|
||||
const documentState = documents[filePath];
|
||||
|
||||
if (!documentState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.documents.setKey(filePath, {
|
||||
...documentState,
|
||||
scroll: position,
|
||||
});
|
||||
}
|
||||
|
||||
updateFile(filePath: string, newContent: string) {
|
||||
const documents = this.documents.get();
|
||||
const documentState = documents[filePath];
|
||||
|
||||
if (!documentState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentContent = documentState.value;
|
||||
const contentChanged = currentContent !== newContent;
|
||||
|
||||
if (contentChanged) {
|
||||
this.documents.setKey(filePath, {
|
||||
...documentState,
|
||||
value: newContent,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
220
app/lib/stores/files.ts
Normal file
220
app/lib/stores/files.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';
|
||||
import { getEncoding } from 'istextorbinary';
|
||||
import { map, type MapStore } from 'nanostores';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import * as nodePath from 'node:path';
|
||||
import { bufferWatchEvents } from '~/utils/buffer';
|
||||
import { WORK_DIR } from '~/utils/constants';
|
||||
import { computeFileModifications } from '~/utils/diff';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { unreachable } from '~/utils/unreachable';
|
||||
|
||||
const logger = createScopedLogger('FilesStore');
|
||||
|
||||
const utf8TextDecoder = new TextDecoder('utf8', { fatal: true });
|
||||
|
||||
export interface File {
|
||||
type: 'file';
|
||||
content: string;
|
||||
isBinary: boolean;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
type: 'folder';
|
||||
}
|
||||
|
||||
type Dirent = File | Folder;
|
||||
|
||||
export type FileMap = Record<string, Dirent | undefined>;
|
||||
|
||||
export class FilesStore {
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
|
||||
/**
|
||||
* Tracks the number of files without folders.
|
||||
*/
|
||||
#size = 0;
|
||||
|
||||
/**
|
||||
* @note Keeps track all modified files with their original content since the last user message.
|
||||
* Needs to be reset when the user sends another message and all changes have to be submitted
|
||||
* for the model to be aware of the changes.
|
||||
*/
|
||||
#modifiedFiles: Map<string, string> = import.meta.hot?.data.modifiedFiles ?? new Map();
|
||||
|
||||
/**
|
||||
* Map of files that matches the state of WebContainer.
|
||||
*/
|
||||
files: MapStore<FileMap> = import.meta.hot?.data.files ?? map({});
|
||||
|
||||
get filesCount() {
|
||||
return this.#size;
|
||||
}
|
||||
|
||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.files = this.files;
|
||||
import.meta.hot.data.modifiedFiles = this.#modifiedFiles;
|
||||
}
|
||||
|
||||
this.#init();
|
||||
}
|
||||
|
||||
getFile(filePath: string) {
|
||||
const dirent = this.files.get()[filePath];
|
||||
|
||||
if (dirent?.type !== 'file') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return dirent;
|
||||
}
|
||||
|
||||
getFileModifications() {
|
||||
return computeFileModifications(this.files.get(), this.#modifiedFiles);
|
||||
}
|
||||
|
||||
resetFileModifications() {
|
||||
this.#modifiedFiles.clear();
|
||||
}
|
||||
|
||||
async saveFile(filePath: string, content: string) {
|
||||
const webcontainer = await this.#webcontainer;
|
||||
|
||||
try {
|
||||
const relativePath = nodePath.relative(webcontainer.workdir, filePath);
|
||||
|
||||
if (!relativePath) {
|
||||
throw new Error(`EINVAL: invalid file path, write '${relativePath}'`);
|
||||
}
|
||||
|
||||
const oldContent = this.getFile(filePath)?.content;
|
||||
|
||||
if (!oldContent) {
|
||||
unreachable('Expected content to be defined');
|
||||
}
|
||||
|
||||
await webcontainer.fs.writeFile(relativePath, content);
|
||||
|
||||
if (!this.#modifiedFiles.has(filePath)) {
|
||||
this.#modifiedFiles.set(filePath, oldContent);
|
||||
}
|
||||
|
||||
// we immediately update the file and don't rely on the `change` event coming from the watcher
|
||||
this.files.setKey(filePath, { type: 'file', content, isBinary: false });
|
||||
|
||||
logger.info('File updated');
|
||||
} catch (error) {
|
||||
logger.error('Failed to update file content\n\n', error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async #init() {
|
||||
const webcontainer = await this.#webcontainer;
|
||||
|
||||
webcontainer.watchPaths(
|
||||
{ include: [`${WORK_DIR}/**`], exclude: ['**/node_modules', '.git'], includeContent: true },
|
||||
bufferWatchEvents(100, this.#processEventBuffer.bind(this)),
|
||||
);
|
||||
}
|
||||
|
||||
#processEventBuffer(events: Array<[events: PathWatcherEvent[]]>) {
|
||||
const watchEvents = events.flat(2);
|
||||
|
||||
for (const { type, path, buffer } of watchEvents) {
|
||||
// remove any trailing slashes
|
||||
const sanitizedPath = path.replace(/\/+$/g, '');
|
||||
|
||||
switch (type) {
|
||||
case 'add_dir': {
|
||||
// we intentionally add a trailing slash so we can distinguish files from folders in the file tree
|
||||
this.files.setKey(sanitizedPath, { type: 'folder' });
|
||||
break;
|
||||
}
|
||||
case 'remove_dir': {
|
||||
this.files.setKey(sanitizedPath, undefined);
|
||||
|
||||
for (const [direntPath] of Object.entries(this.files)) {
|
||||
if (direntPath.startsWith(sanitizedPath)) {
|
||||
this.files.setKey(direntPath, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'add_file':
|
||||
case 'change': {
|
||||
if (type === 'add_file') {
|
||||
this.#size++;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
|
||||
/**
|
||||
* @note This check is purely for the editor. The way we detect this is not
|
||||
* bullet-proof and it's a best guess so there might be false-positives.
|
||||
* The reason we do this is because we don't want to display binary files
|
||||
* in the editor nor allow to edit them.
|
||||
*/
|
||||
const isBinary = isBinaryFile(buffer);
|
||||
|
||||
if (!isBinary) {
|
||||
content = this.#decodeFileContent(buffer);
|
||||
}
|
||||
|
||||
this.files.setKey(sanitizedPath, { type: 'file', content, isBinary });
|
||||
|
||||
break;
|
||||
}
|
||||
case 'remove_file': {
|
||||
this.#size--;
|
||||
this.files.setKey(sanitizedPath, undefined);
|
||||
break;
|
||||
}
|
||||
case 'update_directory': {
|
||||
// we don't care about these events
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#decodeFileContent(buffer?: Uint8Array) {
|
||||
if (!buffer || buffer.byteLength === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return utf8TextDecoder.decode(buffer);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isBinaryFile(buffer: Uint8Array | undefined) {
|
||||
if (buffer === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getEncoding(convertToBuffer(buffer), { chunkLength: 100 }) === 'binary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a `Uint8Array` into a Node.js `Buffer` by copying the prototype.
|
||||
* The goal is to avoid expensive copies. It does create a new typed array
|
||||
* but that's generally cheap as long as it uses the same underlying
|
||||
* array buffer.
|
||||
*/
|
||||
function convertToBuffer(view: Uint8Array): Buffer {
|
||||
const buffer = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
||||
|
||||
Object.setPrototypeOf(buffer, Buffer.prototype);
|
||||
|
||||
return buffer as Buffer;
|
||||
}
|
||||
49
app/lib/stores/previews.ts
Normal file
49
app/lib/stores/previews.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { WebContainer } from '@webcontainer/api';
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
export interface PreviewInfo {
|
||||
port: number;
|
||||
ready: boolean;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export class PreviewsStore {
|
||||
#availablePreviews = new Map<number, PreviewInfo>();
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
|
||||
previews = atom<PreviewInfo[]>([]);
|
||||
|
||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
|
||||
this.#init();
|
||||
}
|
||||
|
||||
async #init() {
|
||||
const webcontainer = await this.#webcontainer;
|
||||
|
||||
webcontainer.on('port', (port, type, url) => {
|
||||
let previewInfo = this.#availablePreviews.get(port);
|
||||
|
||||
if (type === 'close' && previewInfo) {
|
||||
this.#availablePreviews.delete(port);
|
||||
this.previews.set(this.previews.get().filter((preview) => preview.port !== port));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const previews = this.previews.get();
|
||||
|
||||
if (!previewInfo) {
|
||||
previewInfo = { port, ready: type === 'open', baseUrl: url };
|
||||
this.#availablePreviews.set(port, previewInfo);
|
||||
previews.push(previewInfo);
|
||||
}
|
||||
|
||||
previewInfo.ready = type === 'open';
|
||||
previewInfo.baseUrl = url;
|
||||
|
||||
this.previews.set([...previews]);
|
||||
});
|
||||
}
|
||||
}
|
||||
39
app/lib/stores/settings.ts
Normal file
39
app/lib/stores/settings.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { map } from 'nanostores';
|
||||
import { workbenchStore } from './workbench';
|
||||
|
||||
export interface Shortcut {
|
||||
key: string;
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
altKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
ctrlOrMetaKey?: boolean;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
export interface Shortcuts {
|
||||
toggleTerminal: Shortcut;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
shortcuts: Shortcuts;
|
||||
}
|
||||
|
||||
export const shortcutsStore = map<Shortcuts>({
|
||||
toggleTerminal: {
|
||||
key: 'j',
|
||||
ctrlOrMetaKey: true,
|
||||
action: () => workbenchStore.toggleTerminal(),
|
||||
},
|
||||
});
|
||||
|
||||
export const settingsStore = map<Settings>({
|
||||
shortcuts: shortcutsStore.get(),
|
||||
});
|
||||
|
||||
shortcutsStore.subscribe((shortcuts) => {
|
||||
settingsStore.set({
|
||||
...settingsStore.get(),
|
||||
shortcuts,
|
||||
});
|
||||
});
|
||||
40
app/lib/stores/terminal.ts
Normal file
40
app/lib/stores/terminal.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
|
||||
import { atom, type WritableAtom } from 'nanostores';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import { newShellProcess } from '~/utils/shell';
|
||||
import { coloredText } from '~/utils/terminal';
|
||||
|
||||
export class TerminalStore {
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
#terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
|
||||
|
||||
showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(false);
|
||||
|
||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.showTerminal = this.showTerminal;
|
||||
}
|
||||
}
|
||||
|
||||
toggleTerminal(value?: boolean) {
|
||||
this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
|
||||
}
|
||||
|
||||
async attachTerminal(terminal: ITerminal) {
|
||||
try {
|
||||
const shellProcess = await newShellProcess(await this.#webcontainer, terminal);
|
||||
this.#terminals.push({ terminal, process: shellProcess });
|
||||
} catch (error: any) {
|
||||
terminal.write(coloredText.red('Failed to spawn shell\n\n') + error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onTerminalResize(cols: number, rows: number) {
|
||||
for (const { process } of this.#terminals) {
|
||||
process.resize({ cols, rows });
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/lib/stores/theme.ts
Normal file
35
app/lib/stores/theme.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
export type Theme = 'dark' | 'light';
|
||||
|
||||
export const kTheme = 'bolt_theme';
|
||||
|
||||
export function themeIsDark() {
|
||||
return themeStore.get() === 'dark';
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME = 'light';
|
||||
|
||||
export const themeStore = atom<Theme>(initStore());
|
||||
|
||||
function initStore() {
|
||||
if (!import.meta.env.SSR) {
|
||||
const persistedTheme = localStorage.getItem(kTheme) as Theme | undefined;
|
||||
const themeAttribute = document.querySelector('html')?.getAttribute('data-theme');
|
||||
|
||||
return persistedTheme ?? (themeAttribute as Theme) ?? DEFAULT_THEME;
|
||||
}
|
||||
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
|
||||
export function toggleTheme() {
|
||||
const currentTheme = themeStore.get();
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
themeStore.set(newTheme);
|
||||
|
||||
localStorage.setItem(kTheme, newTheme);
|
||||
|
||||
document.querySelector('html')?.setAttribute('data-theme', newTheme);
|
||||
}
|
||||
276
app/lib/stores/workbench.ts
Normal file
276
app/lib/stores/workbench.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user