diff --git a/angular.json b/angular.json index 9644e41..8fe5af1 100644 --- a/angular.json +++ b/angular.json @@ -20,7 +20,14 @@ "browser": "src/main.ts", "polyfills": [], "tsConfig": "tsconfig.app.json", - "assets": ["src/assets"], + "assets": [ + "src/assets", + { + "glob": "**/*", + "input": "node_modules/monaco-editor", + "output": "/assets/monaco/" + } + ], "styles": [ "src/styles.scss", "highlight.js/styles/atom-one-dark.min.css" diff --git a/bun.lockb b/bun.lockb index 9546164..2ff8378 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index c9995b0..c17eb63 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "@angular/router": "^18.1.0", "@tauri-apps/api": "^1.6.0", "highlight.js": "^11.10.0", + "monaco-editor": "^0.50.0", + "ngx-monaco-editor-v2": "^18.0.1", "protobufjs": "^7.3.2", "rxjs": "^7.8.1", "tslib": "^2.6.3", diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 2fa6f54..63431fe 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -22,6 +22,7 @@ mat-sidenav-content { app-editor { flex: 1; + width: 100%; } .mat-sidenav.mat-drawer-end, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e69a5de..828cbe6 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,6 +1,6 @@ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { CommonModule } from '@angular/common'; -import { Component, computed, signal } from '@angular/core'; +import { Component, computed, effect, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 7262144..08229d7 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -13,6 +13,7 @@ import { MAT_ICON_DEFAULT_OPTIONS, MatIconRegistry, } from '@angular/material/icon'; +import { provideMonacoEditor } from 'ngx-monaco-editor-v2'; export const appConfig: ApplicationConfig = { providers: [ @@ -41,5 +42,6 @@ export const appConfig: ApplicationConfig = { provide: MAT_ICON_DEFAULT_OPTIONS, useValue: { fontSet: 'material-symbols-rounded' }, }, + provideMonacoEditor(), ], }; diff --git a/src/app/editor/editor.component.html b/src/app/editor/editor.component.html index a8772d5..5e58df2 100644 --- a/src/app/editor/editor.component.html +++ b/src/app/editor/editor.component.html @@ -14,13 +14,36 @@ > } } - @if(showRaw()) { -

Preview

-
- -
-
-} +
+

+ Preview + + Raw + Edit + @if(selectedFile()) { + Diff + } + + +

+ @switch (previewType()) { @case ('raw') { +
+ } @case ('edit') { + + } @case ('diff') { + + } } +
+} diff --git a/src/app/editor/editor.component.scss b/src/app/editor/editor.component.scss index e708522..e386a09 100644 --- a/src/app/editor/editor.component.scss +++ b/src/app/editor/editor.component.scss @@ -2,10 +2,11 @@ padding: var(--mat-sidenav-container-shape); display: flex; flex-direction: column; + box-sizing: border-box; } -pre, -.editor-items { +.editor-items, +.preview { flex: 1; overflow: auto; } @@ -14,11 +15,16 @@ pre, padding: 16px; } -.actions { - flex: 0 0 auto; -} - .proto-title { display: flex; justify-content: space-between; } + +mat-button-toggle-group { + width: fit-content; +} + +ngx-monaco-editor, +ngx-monaco-diff-editor { + height: 100%; +} diff --git a/src/app/editor/editor.component.ts b/src/app/editor/editor.component.ts index a44cc77..aea79c5 100644 --- a/src/app/editor/editor.component.ts +++ b/src/app/editor/editor.component.ts @@ -18,13 +18,26 @@ import hljs from 'highlight.js/lib/core'; import { ProtoMessage } from '../model/proto-message.model'; import { ProtoFieldComponent } from './proto-field/proto-field.component'; import { save } from '@tauri-apps/api/dialog'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MonacoEditorModule } from 'ngx-monaco-editor-v2'; +import { FormsModule } from '@angular/forms'; declare const __TAURI__: any; +type PreviewType = 'raw' | 'edit' | 'diff'; + @Component({ selector: 'app-editor', standalone: true, - imports: [CommonModule, ProtoFieldComponent, MatButtonModule, MatIconModule], + imports: [ + CommonModule, + ProtoFieldComponent, + MatButtonModule, + MatButtonToggleModule, + MatIconModule, + MonacoEditorModule, + FormsModule, + ], templateUrl: './editor.component.html', styleUrl: './editor.component.scss', }) @@ -34,11 +47,16 @@ export class EditorComponent { indentSize = input(2); showRaw = input(true); + protected editorOptions = { + theme: 'vs-dark', + language: 'json', + automaticLayout: true, + }; protected values = signal(undefined); protected downloadName = computed( () => this.selectedFile() ?? `${this.selectedMessage().name}.json` ); - private serialisedValues = computed(() => + protected serialisedValues = computed(() => JSON.stringify(this.values(), undefined, this.indentSize()) ); protected saveHref = computed(() => { @@ -48,6 +66,16 @@ export class EditorComponent { return URL.createObjectURL(blob); }); protected downloader = viewChild>('downloader'); + protected previewType = signal('raw'); + protected currentFileContents = signal(''); + protected originalModel = computed(() => ({ + code: this.currentFileContents(), + language: 'json', + })); + protected currentModel = computed(() => ({ + code: this.serialisedValues(), + language: 'json', + })); private code = viewChild>('code'); @@ -85,24 +113,23 @@ export class EditorComponent { const selectedFile = this.selectedFile(); if (selectedFile) { const fileContents = await readTextFile(selectedFile); - try { - const parsed = JSON.parse(fileContents); - this.values.set(parsed); - } catch (err) { - console.error( - `Failed to parse contents of file ${selectedFile}`, - err - ); - this.snackBar.open( - `Failed to parse contents of file ${selectedFile}, please check the file is correctly formatted`, - 'Dismiss', - { duration: 5000 } - ); - } + this.currentFileContents.set(fileContents); + this.updateValuesFromText(fileContents, selectedFile); } }); } + protected updateValuesFromText(text: string, selectedFile?: string) { + try { + const parsed = JSON.parse(text); + this.values.set(parsed); + } catch (err) { + if (selectedFile) { + console.error(`Failed to parse contents of file ${selectedFile}`, err); + } + } + } + protected updateValue(key: string, value: any) { const existingValue = { ...this.values() }; existingValue[key] = value; diff --git a/src/styles.scss b/src/styles.scss index b730084..9878d19 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -30,6 +30,7 @@ html { @include mat.list-theme(theme.$rose-theme); @include mat.select-theme(theme.$rose-theme); @include mat.snack-bar-theme(theme.$rose-theme); + @include mat.button-toggle-theme(theme.$rose-theme); @include custom-colours(theme.$rose-theme); }