diff options
Diffstat (limited to 'subprojects/frontend/src/editor/EditorStore.ts')
-rw-r--r-- | subprojects/frontend/src/editor/EditorStore.ts | 124 |
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 | ||
28 | import type PWAStore from '../PWAStore'; | 28 | import type PWAStore from '../PWAStore'; |
29 | import GraphStore from '../graph/GraphStore'; | 29 | import GraphStore from '../graph/GraphStore'; |
30 | import { | ||
31 | type OpenResult, | ||
32 | type OpenTextFileResult, | ||
33 | openTextFile, | ||
34 | saveTextFile, | ||
35 | saveBlob, | ||
36 | } from '../utils/fileIO'; | ||
30 | import getLogger from '../utils/getLogger'; | 37 | import getLogger from '../utils/getLogger'; |
31 | import type XtextClient from '../xtext/XtextClient'; | 38 | import type XtextClient from '../xtext/XtextClient'; |
32 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | 39 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; |
@@ -35,7 +42,10 @@ import EditorErrors from './EditorErrors'; | |||
35 | import GeneratedModelStore from './GeneratedModelStore'; | 42 | import GeneratedModelStore from './GeneratedModelStore'; |
36 | import LintPanelStore from './LintPanelStore'; | 43 | import LintPanelStore from './LintPanelStore'; |
37 | import SearchPanelStore from './SearchPanelStore'; | 44 | import SearchPanelStore from './SearchPanelStore'; |
38 | import createEditorState from './createEditorState'; | 45 | import createEditorState, { |
46 | createHistoryExtension, | ||
47 | historyCompartment, | ||
48 | } from './createEditorState'; | ||
39 | import { countDiagnostics } from './exposeDiagnostics'; | 49 | import { countDiagnostics } from './exposeDiagnostics'; |
40 | import { type IOccurrence, setOccurrences } from './findOccurrences'; | 50 | import { type IOccurrence, setOccurrences } from './findOccurrences'; |
41 | import { | 51 | import { |
@@ -45,6 +55,25 @@ import { | |||
45 | 55 | ||
46 | const log = getLogger('editor.EditorStore'); | 56 | const log = getLogger('editor.EditorStore'); |
47 | 57 | ||
58 | const REFINERY_CONTENT_TYPE = 'text/x-refinery'; | ||
59 | |||
60 | const 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 | |||
48 | export default class EditorStore { | 77 | export 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 | } |