aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/editor/EditorStore.ts
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/editor/EditorStore.ts')
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts124
1 files changed, 123 insertions, 1 deletions
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index 5e7d05e1..33bca382 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -27,6 +27,13 @@ import { nanoid } from 'nanoid';
27 27
28import type PWAStore from '../PWAStore'; 28import type PWAStore from '../PWAStore';
29import GraphStore from '../graph/GraphStore'; 29import GraphStore from '../graph/GraphStore';
30import {
31 type OpenResult,
32 type OpenTextFileResult,
33 openTextFile,
34 saveTextFile,
35 saveBlob,
36} from '../utils/fileIO';
30import getLogger from '../utils/getLogger'; 37import getLogger from '../utils/getLogger';
31import type XtextClient from '../xtext/XtextClient'; 38import type XtextClient from '../xtext/XtextClient';
32import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; 39import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
@@ -35,7 +42,10 @@ import EditorErrors from './EditorErrors';
35import GeneratedModelStore from './GeneratedModelStore'; 42import GeneratedModelStore from './GeneratedModelStore';
36import LintPanelStore from './LintPanelStore'; 43import LintPanelStore from './LintPanelStore';
37import SearchPanelStore from './SearchPanelStore'; 44import SearchPanelStore from './SearchPanelStore';
38import createEditorState from './createEditorState'; 45import createEditorState, {
46 createHistoryExtension,
47 historyCompartment,
48} from './createEditorState';
39import { countDiagnostics } from './exposeDiagnostics'; 49import { countDiagnostics } from './exposeDiagnostics';
40import { type IOccurrence, setOccurrences } from './findOccurrences'; 50import { type IOccurrence, setOccurrences } from './findOccurrences';
41import { 51import {
@@ -45,6 +55,25 @@ import {
45 55
46const log = getLogger('editor.EditorStore'); 56const log = getLogger('editor.EditorStore');
47 57
58const REFINERY_CONTENT_TYPE = 'text/x-refinery';
59
60const FILE_PICKER_OPTIONS: FilePickerOptions = {
61 id: 'problem',
62 types: [
63 {
64 description: 'Refinery files',
65 accept: {
66 [REFINERY_CONTENT_TYPE]: [
67 '.problem',
68 '.PROBLEM',
69 '.refinery',
70 '.REFINERY',
71 ],
72 },
73 },
74 ],
75};
76
48export default class EditorStore { 77export default class EditorStore {
49 readonly id: string; 78 readonly id: string;
50 79
@@ -76,6 +105,12 @@ export default class EditorStore {
76 105
77 selectedGeneratedModel: string | undefined; 106 selectedGeneratedModel: string | undefined;
78 107
108 fileName: string | undefined;
109
110 private fileHandle: FileSystemFileHandle | undefined;
111
112 unsavedChanges = false;
113
79 constructor( 114 constructor(
80 initialValue: string, 115 initialValue: string,
81 pwaStore: PWAStore, 116 pwaStore: PWAStore,
@@ -201,6 +236,9 @@ export default class EditorStore {
201 log.trace('Editor transaction', tr); 236 log.trace('Editor transaction', tr);
202 this.state = tr.state; 237 this.state = tr.state;
203 this.client?.onTransaction(tr); 238 this.client?.onTransaction(tr);
239 if (tr.docChanged) {
240 this.unsavedChanges = true;
241 }
204 } 242 }
205 243
206 doCommand(command: Command): boolean { 244 doCommand(command: Command): boolean {
@@ -403,4 +441,88 @@ export default class EditorStore {
403 }); 441 });
404 return generating; 442 return generating;
405 } 443 }
444
445 openFile(): boolean {
446 openTextFile(FILE_PICKER_OPTIONS)
447 .then((result) => this.fileOpened(result))
448 .catch((error) => log.error('Failed to open file', error));
449 return true;
450 }
451
452 private clearUnsavedChanges(): void {
453 this.unsavedChanges = false;
454 }
455
456 private setFile({ name, handle }: OpenResult): void {
457 log.info('Opened file', name);
458 this.fileName = name;
459 this.fileHandle = handle;
460 }
461
462 private fileOpened(result: OpenTextFileResult): void {
463 this.dispatch({
464 changes: [
465 {
466 from: 0,
467 to: this.state.doc.length,
468 insert: result.text,
469 },
470 ],
471 effects: [historyCompartment.reconfigure([])],
472 });
473 // Clear history by removing and re-adding the history extension. See
474 // https://stackoverflow.com/a/77943295 and
475 // https://discuss.codemirror.net/t/codemirror-6-cm-clearhistory-equivalent/2851/10
476 this.dispatch({
477 effects: [historyCompartment.reconfigure([createHistoryExtension()])],
478 });
479 this.setFile(result);
480 this.clearUnsavedChanges();
481 }
482
483 saveFile(): boolean {
484 if (!this.unsavedChanges) {
485 return false;
486 }
487 if (this.fileHandle === undefined) {
488 return this.saveFileAs();
489 }
490 saveTextFile(this.fileHandle, this.state.sliceDoc())
491 .then(() => this.clearUnsavedChanges())
492 .catch((error) => log.error('Failed to save file', error));
493 return true;
494 }
495
496 saveFileAs(): boolean {
497 const blob = new Blob([this.state.sliceDoc()], {
498 type: REFINERY_CONTENT_TYPE,
499 });
500 saveBlob(blob, this.fileName ?? 'graph.problem', FILE_PICKER_OPTIONS)
501 .then((result) => this.fileSavedAs(result))
502 .catch((error) => log.error('Failed to save file', error));
503 return true;
504 }
505
506 private fileSavedAs(result: OpenResult | undefined) {
507 if (result !== undefined) {
508 this.setFile(result);
509 }
510 this.clearUnsavedChanges();
511 }
512
513 get simpleName(): string | undefined {
514 const { fileName } = this;
515 if (fileName === undefined) {
516 return undefined;
517 }
518 const index = fileName.lastIndexOf('.');
519 if (index < 0) {
520 return fileName;
521 }
522 return fileName.substring(0, index);
523 }
524
525 get simpleNameOrFallback(): string {
526 return this.simpleName ?? 'graph';
527 }
406} 528}