Editor opens in native windows (one per file)
Real BrowserWindow per edited file with its own RPC instance, deduped by sessionId+path. The same Angular bundle boots an editor-only root via preload globals. Native close isn't cancellable in electrobun, so the editor mirrors its draft to bun; traffic-light closes get a native Save/Discard prompt after the fact, and failed saves park the draft in the edit-leftovers dir. In-window closes (Cmd+W, Close) use a styled prompt; the quit guard now also counts unsaved editors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,158 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
afterNextRender,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { DialogsService, type EditorRequest } from '../dialogs.service';
|
||||
import { PanelsService } from '../panels.service';
|
||||
import { RpcService } from '../rpc.service';
|
||||
import { ToastService } from '../toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-editor-dialog',
|
||||
imports: [NgIcon],
|
||||
template: `
|
||||
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40">
|
||||
<div
|
||||
class="flex h-[80vh] w-[85vw] max-w-5xl flex-col rounded-lg border border-border bg-card text-card-foreground shadow-xl"
|
||||
(keydown)="onKeydown($event)"
|
||||
>
|
||||
<div class="flex items-center gap-2 border-b border-border px-4 py-2.5">
|
||||
<ng-icon name="mynaFileText" size="0.95rem" class="shrink-0 text-muted-foreground" />
|
||||
<h2 class="min-w-0 truncate font-mono text-sm font-semibold">
|
||||
{{ request().name }}{{ dirty() ? ' •' : '' }}
|
||||
</h2>
|
||||
<span class="shrink-0 text-[11px] text-muted-foreground">
|
||||
{{ request().sessionTitle }}
|
||||
</span>
|
||||
<div class="flex-1"></div>
|
||||
<button
|
||||
(click)="close()"
|
||||
class="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<ng-icon name="mynaX" size="1rem" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
#box
|
||||
[value]="text()"
|
||||
(input)="onInput($any($event.target).value)"
|
||||
spellcheck="false"
|
||||
class="min-h-0 flex-1 resize-none bg-transparent p-4 font-mono text-xs leading-relaxed outline-none"
|
||||
></textarea>
|
||||
|
||||
<div class="flex items-center gap-2 border-t border-border px-4 py-2.5">
|
||||
<span class="text-[11px] text-muted-foreground">⌘S saves · Esc closes</span>
|
||||
<div class="flex-1"></div>
|
||||
<button
|
||||
(click)="close()"
|
||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
(click)="save()"
|
||||
[disabled]="saving() || !dirty()"
|
||||
class="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
@if (saving()) {
|
||||
<ng-icon name="mynaSpinner" size="0.85rem" class="animate-spin" />
|
||||
}
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class EditorDialog {
|
||||
readonly request = input.required<EditorRequest>();
|
||||
|
||||
readonly text = signal('');
|
||||
readonly dirty = signal(false);
|
||||
readonly saving = signal(false);
|
||||
|
||||
private readonly box = viewChild.required<ElementRef<HTMLTextAreaElement>>('box');
|
||||
private readonly dialogs = inject(DialogsService);
|
||||
private readonly panels = inject(PanelsService);
|
||||
private readonly rpc = inject(RpcService);
|
||||
private readonly toast = inject(ToastService);
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.text.set(this.request().content);
|
||||
this.box().nativeElement.focus();
|
||||
});
|
||||
}
|
||||
|
||||
onInput(value: string): void {
|
||||
this.text.set(value);
|
||||
this.dirty.set(value !== this.request().content);
|
||||
}
|
||||
|
||||
onKeydown(event: KeyboardEvent): void {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
|
||||
event.preventDefault();
|
||||
void this.save();
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
this.close();
|
||||
} else if (event.key === 'Tab' && event.target === this.box().nativeElement) {
|
||||
// keep Tab as an input character inside the editor
|
||||
event.preventDefault();
|
||||
const el = this.box().nativeElement;
|
||||
const { selectionStart, selectionEnd, value } = el;
|
||||
el.value = `${value.slice(0, selectionStart)}\t${value.slice(selectionEnd)}`;
|
||||
el.selectionStart = el.selectionEnd = selectionStart + 1;
|
||||
this.onInput(el.value);
|
||||
}
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
const req = this.request();
|
||||
if (!this.dirty() || this.saving()) return;
|
||||
this.saving.set(true);
|
||||
try {
|
||||
const res = await this.rpc.request.writeFile({
|
||||
sessionId: req.sessionId,
|
||||
path: req.path,
|
||||
content: this.text(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
this.toast.error(res.error);
|
||||
return;
|
||||
}
|
||||
this.toast.success(`Saved ${req.name}`);
|
||||
req.content = this.text();
|
||||
this.dirty.set(false);
|
||||
void req.panel.refresh();
|
||||
} finally {
|
||||
this.saving.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.dirty()) {
|
||||
this.dialogs.confirm.set({
|
||||
title: 'Discard changes?',
|
||||
body: `"${this.request().name}" has unsaved changes.`,
|
||||
confirmLabel: 'Discard',
|
||||
destructive: true,
|
||||
onConfirm: () => this.forceClose(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.forceClose();
|
||||
}
|
||||
|
||||
private forceClose(): void {
|
||||
this.dialogs.editor.set(null);
|
||||
this.panels.refocus();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user