2026-07-06 17:28:59 +00:00
|
|
|
import { EventSlot } from '../library-ts/browser/events/EventSlot.js';
|
2026-07-12 11:05:53 +00:00
|
|
|
import { FileEditorRegistry } from './FileEditorRegistry.js';
|
2026-07-10 18:18:15 +00:00
|
|
|
|
2026-07-06 17:28:59 +00:00
|
|
|
|
|
|
|
|
export interface DocumentOpenedEvent
|
|
|
|
|
{
|
|
|
|
|
path: string;
|
|
|
|
|
content: string;
|
2026-07-12 11:05:53 +00:00
|
|
|
editorTag: string;
|
Editor: auth redirect, targeted file open, Godot file types
- editor-shell: redirect to / if not authenticated on load
- file-tree: open file in first clean matching panel, create new panel if all dirty
- Editor: add openDocumentIn() with targetElement for precise panel targeting
- panels: skip onDocumentOpened if targeted at a different panel
- FileEditorRegistry: add Godot extensions (gd, gdshader, gdshaderinc, gdinclude, glsl, tscn, tres, cs, gdextension)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 20:22:42 +00:00
|
|
|
targetElement?: HTMLElement;
|
2026-07-06 17:28:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DocumentPathEvent
|
|
|
|
|
{
|
|
|
|
|
path: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class Editor
|
|
|
|
|
{
|
|
|
|
|
static _instance: Editor = null;
|
|
|
|
|
|
|
|
|
|
static get(): Editor
|
|
|
|
|
{
|
|
|
|
|
if ( ! this._instance )
|
|
|
|
|
{
|
|
|
|
|
this._instance = new Editor();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this._instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
projectId: string = '';
|
|
|
|
|
projectName: string = '';
|
|
|
|
|
openDocs: Map<string, { content: string; dirty: boolean }> = new Map();
|
|
|
|
|
activeDoc: string | null = null;
|
2026-07-12 11:05:53 +00:00
|
|
|
fileEditorRegistry: FileEditorRegistry = new FileEditorRegistry();
|
2026-07-06 17:28:59 +00:00
|
|
|
|
|
|
|
|
readonly onDocumentOpened: EventSlot<DocumentOpenedEvent> = new EventSlot();
|
|
|
|
|
readonly onDocumentDirty: EventSlot<DocumentPathEvent> = new EventSlot();
|
|
|
|
|
readonly onDocumentSaved: EventSlot<DocumentPathEvent> = new EventSlot();
|
|
|
|
|
readonly onFilesChanged: EventSlot<void> = new EventSlot();
|
2026-07-12 11:05:53 +00:00
|
|
|
readonly onFileTypeUnknown: EventSlot<DocumentPathEvent> = new EventSlot();
|
2026-07-06 17:28:59 +00:00
|
|
|
|
|
|
|
|
async openDocument( filePath: string ): Promise<void>
|
|
|
|
|
{
|
2026-07-12 11:05:53 +00:00
|
|
|
await this.fileEditorRegistry.load( this.projectId );
|
|
|
|
|
|
|
|
|
|
const editorTag = this.fileEditorRegistry.resolve( filePath );
|
|
|
|
|
|
|
|
|
|
if ( editorTag === null )
|
|
|
|
|
{
|
|
|
|
|
this.onFileTypeUnknown.dispatch( { path: filePath } );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 17:28:59 +00:00
|
|
|
if ( ! this.openDocs.has( filePath ) )
|
|
|
|
|
{
|
|
|
|
|
const res = await fetch( `/api/files/${this.projectId}/${filePath}` );
|
|
|
|
|
const content = await res.text();
|
|
|
|
|
this.openDocs.set( filePath, { content, dirty: false } );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.activeDoc = filePath;
|
|
|
|
|
const entry = this.openDocs.get( filePath )!;
|
2026-07-12 11:05:53 +00:00
|
|
|
this.onDocumentOpened.dispatch( { path: filePath, content: entry.content, editorTag } );
|
2026-07-06 17:28:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
markDirty( filePath: string, content: string ): void
|
|
|
|
|
{
|
|
|
|
|
const doc = this.openDocs.get( filePath );
|
|
|
|
|
|
|
|
|
|
if ( ! doc )
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
doc.content = content;
|
|
|
|
|
doc.dirty = true;
|
|
|
|
|
this.onDocumentDirty.dispatch( { path: filePath } );
|
|
|
|
|
}
|
|
|
|
|
|
Editor: auth redirect, targeted file open, Godot file types
- editor-shell: redirect to / if not authenticated on load
- file-tree: open file in first clean matching panel, create new panel if all dirty
- Editor: add openDocumentIn() with targetElement for precise panel targeting
- panels: skip onDocumentOpened if targeted at a different panel
- FileEditorRegistry: add Godot extensions (gd, gdshader, gdshaderinc, gdinclude, glsl, tscn, tres, cs, gdextension)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 20:22:42 +00:00
|
|
|
async openDocumentIn( filePath: string, panelElement: HTMLElement ): Promise<void>
|
|
|
|
|
{
|
|
|
|
|
await this.fileEditorRegistry.load( this.projectId );
|
|
|
|
|
const editorTag = this.fileEditorRegistry.resolve( filePath );
|
|
|
|
|
|
|
|
|
|
if ( editorTag === null )
|
|
|
|
|
{
|
|
|
|
|
this.onFileTypeUnknown.dispatch( { path: filePath } );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !this.openDocs.has( filePath ) )
|
|
|
|
|
{
|
|
|
|
|
const res = await fetch( `/api/files/${this.projectId}/${filePath}` );
|
|
|
|
|
const content = await res.text();
|
|
|
|
|
this.openDocs.set( filePath, { content, dirty: false } );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.activeDoc = filePath;
|
|
|
|
|
const entry = this.openDocs.get( filePath )!;
|
|
|
|
|
this.onDocumentOpened.dispatch( { path: filePath, content: entry.content, editorTag, targetElement: panelElement } );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 17:28:59 +00:00
|
|
|
async save( filePath: string ): Promise<void>
|
|
|
|
|
{
|
|
|
|
|
const doc = this.openDocs.get( filePath );
|
|
|
|
|
|
|
|
|
|
if ( ! doc || ! doc.dirty )
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await fetch(
|
|
|
|
|
`/api/files/${this.projectId}/${filePath}`,
|
|
|
|
|
{
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: { 'Content-Type': 'text/plain' },
|
|
|
|
|
body: doc.content
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
doc.dirty = false;
|
|
|
|
|
this.onDocumentSaved.dispatch( { path: filePath } );
|
|
|
|
|
}
|
|
|
|
|
}
|