From 9c2531a0346589551827e16b05ed8658be2925fa Mon Sep 17 00:00:00 2001 From: vato007 Date: Sat, 22 Jun 2024 14:19:23 +0930 Subject: [PATCH] Start adding editor, fix up sidebar styling --- angular.json | 18 +-- bun.lockb | Bin 269910 -> 270259 bytes package.json | 2 + src-tauri/src/main.rs | 2 +- src/app/app.component.css | 0 src/app/app.component.html | 43 +++++- src/app/app.component.scss | 10 ++ src/app/app.component.ts | 37 ++++- src/app/app.config.ts | 29 ++++ src/app/editor/editor.component.css | 3 - src/app/editor/editor.component.html | 4 +- src/app/editor/editor.component.scss | 3 - src/app/editor/editor.component.ts | 26 ++-- src/app/file-tree/file-tree.component.css | 3 - src/app/file-tree/file-tree.component.html | 23 ++++ src/app/file-tree/file-tree.component.ts | 25 +--- src/app/model/proto-message.model.ts | 19 +++ .../proto-definition-selector.component.css | 16 +++ .../proto-definition-selector.component.ts | 127 ++++++++++++++++++ src/app/proto-definition.service.spec.ts | 57 ++++++++ src/app/proto-definition.service.ts | 70 ++++++++++ src/index.html | 30 +++-- src/styles.scss | 11 +- test.proto | 12 ++ 24 files changed, 489 insertions(+), 81 deletions(-) delete mode 100644 src/app/app.component.css delete mode 100644 src/app/editor/editor.component.css delete mode 100644 src/app/file-tree/file-tree.component.css create mode 100644 src/app/file-tree/file-tree.component.html create mode 100644 src/app/model/proto-message.model.ts create mode 100644 src/app/proto-definition-selector/proto-definition-selector.component.css create mode 100644 src/app/proto-definition-selector/proto-definition-selector.component.ts create mode 100644 src/app/proto-definition.service.spec.ts create mode 100644 src/app/proto-definition.service.ts create mode 100644 test.proto diff --git a/angular.json b/angular.json index dec4bcf..845d330 100644 --- a/angular.json +++ b/angular.json @@ -18,18 +18,21 @@ "outputPath": "dist/proto-creator", "index": "src/index.html", "browser": "src/main.ts", - "polyfills": [ - ], + "polyfills": [], "tsConfig": "tsconfig.app.json", "assets": ["src/assets"], - "styles": ["src/styles.scss"] + "styles": [ + "src/styles.scss", + "highlight.js/styles/atom-one-dark.min.css" + ], + "allowedCommonJsDependencies": ["protobufjs"] }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "500kb", + "maximumWarning": "700kb", "maximumError": "1mb" }, { @@ -69,8 +72,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "polyfills": [ - ], + "polyfills": [], "tsConfig": "tsconfig.spec.json", "inlineStyleLanguage": "scss", "assets": [ @@ -79,9 +81,7 @@ "input": "public" } ], - "styles": [ - "src/styles.scss" - ], + "styles": ["src/styles.scss"], "scripts": [] } } diff --git a/bun.lockb b/bun.lockb index 8d07c1e1d2a1b61046e182b838daa3347ad037be..3c185b6f6392b321d2dbab4815eefddc87b483fd 100755 GIT binary patch delta 57025 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

0G5aFW_bb=lBKUmfVOtjmNh8M?vf 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 diff --git a/package.json b/package.json index 607a8cb..1420d33 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "ng": "ng", "start": "ng serve", + "test": "ng test", "build": "ng build", "watch": "ng build --watch --configuration development", "tauri": "tauri" @@ -21,6 +22,7 @@ "@angular/platform-browser-dynamic": "^18.0.0", "@angular/router": "^18.0.0", "@tauri-apps/api": "^1.5.6", + "highlight.js": "^11.9.0", "protobufjs": "~7.3.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5726066..f78b8d6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -46,7 +46,7 @@ fn build_menu(app_name: &str) -> Menu { } let open_folder = CustomMenuItem::new("openfolder".to_string(), "Open Folder").accelerator("cmd+shift+O"); - let file_submenu = Menu::new() + let mut file_submenu = Menu::new() .add_item(open_folder) .add_native_item(MenuItem::CloseWindow); #[cfg(not(target_os = "macos"))] diff --git a/src/app/app.component.css b/src/app/app.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/app.component.html b/src/app/app.component.html index 7fa7c18..1144aaa 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,12 +1,42 @@ -

BufPiv

+ +

BufPiv

+ + + + +
@if(selectedDirectory()) { - + } + + + @if (!selectedDirectory()) {
- }@else { } + }@else { + + }
-
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 5011c3d..d80ee9d 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -8,8 +8,18 @@ mat-toolbar { flex: 0 0 auto; padding-top: 24px; --mat-toolbar-standard-height: 70px; + display: flex; + justify-content: space-between; } mat-sidenav-container { width: 100%; height: 100%; } + +.mat-sidenav.mat-drawer-end { + padding: var(--mat-sidenav-container-shape); +} + +.mat-icon.filled { + font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 0, "opsz" 24; +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4045cb9..648421f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,19 +1,25 @@ +import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { CommonModule } from '@angular/common'; import { Component, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; import { MatSidenavModule } from '@angular/material/sidenav'; -import { RouterOutlet } from '@angular/router'; -import { message, open } from '@tauri-apps/api/dialog'; -import { FileEntry, readDir } from '@tauri-apps/api/fs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTreeModule } from '@angular/material/tree'; +import { RouterOutlet } from '@angular/router'; +import { message, open } from '@tauri-apps/api/dialog'; +import { UnlistenFn, listen } from '@tauri-apps/api/event'; +import { FileEntry, readDir } from '@tauri-apps/api/fs'; +import { EditorComponent } from './editor/editor.component'; import { FileOrFolder, FileTreeComponent, } from './file-tree/file-tree.component'; -import { MatIconModule } from '@angular/material/icon'; -import { listen, UnlistenFn } from '@tauri-apps/api/event'; import { OpenFolderMessage } from './messages/openfolder.message'; +import { ProtoMessage } from './model/proto-message.model'; +import { ProtoDefinitionSelectorComponent } from './proto-definition-selector/proto-definition-selector.component'; +const mobileBreakpoints = [Breakpoints.Handset, Breakpoints.TabletPortrait]; @Component({ selector: 'app-root', @@ -29,14 +35,30 @@ import { OpenFolderMessage } from './messages/openfolder.message'; MatTreeModule, FileTreeComponent, MatIconModule, + EditorComponent, + ProtoDefinitionSelectorComponent, ], }) export class AppComponent { protected selectedDirectory = signal(null); protected files = signal([]); + protected selectedMessage = signal(undefined); + protected rightSideOpen = signal(true); + protected leftSideOpen = signal(true); private unlisten?: UnlistenFn; + protected isMobile = signal( + this.breakpointObserver.isMatched(mobileBreakpoints) + ); + + constructor(private breakpointObserver: BreakpointObserver) { + breakpointObserver + .observe(mobileBreakpoints) + .pipe(takeUntilDestroyed()) + .subscribe((matches) => this.isMobile.set(matches.matches)); + } + async ngOnInit() { this.unlisten = await listen( 'openfolder', @@ -83,9 +105,14 @@ export class AppComponent { children: entry.children ?.sort(this.sortFiles(splitNumbers)) .map((entry) => this.mapEntry(entry, splitNumbers)), + path: entry.path, }; } private sortFiles = (splitNumbers: RegExp) => (a: FileEntry, b: FileEntry) => a.name?.localeCompare(b.name ?? '') ?? 0; + + protected selectMessage(message: ProtoMessage) { + this.selectedMessage.set(message); + } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index d5506d4..7262144 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,4 +1,5 @@ import { + APP_INITIALIZER, ApplicationConfig, provideExperimentalZonelessChangeDetection, } from '@angular/core'; @@ -6,11 +7,39 @@ import { provideRouter } from '@angular/router'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { routes } from './app.routes'; +import hljs from 'highlight.js/lib/core'; +import json from 'highlight.js/lib/languages/json'; +import { + MAT_ICON_DEFAULT_OPTIONS, + MatIconRegistry, +} from '@angular/material/icon'; export const appConfig: ApplicationConfig = { providers: [ provideExperimentalZonelessChangeDetection(), provideRouter(routes), provideAnimationsAsync(), + { + provide: APP_INITIALIZER, + useValue: () => { + hljs.registerLanguage('json', json); + }, + multi: true, + }, + { + provide: APP_INITIALIZER, + useFactory: (iconRegistry: MatIconRegistry) => () => { + iconRegistry.registerFontClassAlias( + 'material-symbols-rounded', + 'material-symbols-rounded' + ); + }, + deps: [MatIconRegistry], + multi: true, + }, + { + provide: MAT_ICON_DEFAULT_OPTIONS, + useValue: { fontSet: 'material-symbols-rounded' }, + }, ], }; diff --git a/src/app/editor/editor.component.css b/src/app/editor/editor.component.css deleted file mode 100644 index 5d4e87f..0000000 --- a/src/app/editor/editor.component.css +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} diff --git a/src/app/editor/editor.component.html b/src/app/editor/editor.component.html index 2858489..0cbf154 100644 --- a/src/app/editor/editor.component.html +++ b/src/app/editor/editor.component.html @@ -1 +1,3 @@ - + + +
{{fileContents()}}
diff --git a/src/app/editor/editor.component.scss b/src/app/editor/editor.component.scss index 5d4e87f..e69de29 100644 --- a/src/app/editor/editor.component.scss +++ b/src/app/editor/editor.component.scss @@ -1,3 +0,0 @@ -:host { - display: block; -} diff --git a/src/app/editor/editor.component.ts b/src/app/editor/editor.component.ts index 6cf201d..822469b 100644 --- a/src/app/editor/editor.component.ts +++ b/src/app/editor/editor.component.ts @@ -1,8 +1,9 @@ import { CommonModule } from '@angular/common'; -import { Component, input } from '@angular/core'; +import { Component, ElementRef, effect, input, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { readTextFile } from '@tauri-apps/api/fs'; -import { parse } from 'protobufjs'; +import hljs from 'highlight.js/lib/core'; +import { ProtoMessage } from '../model/proto-message.model'; +import { ProtoDefinitionService } from '../proto-definition.service'; @Component({ selector: 'app-editor', @@ -13,18 +14,15 @@ import { parse } from 'protobufjs'; }) export class EditorComponent { fileContents = input(); - selectedProtoPath = input(); + selectedMessage = input(); + private code = viewChild>('code'); - async loadProtoDefinition() { - try { - const protoPath = this.selectedProtoPath(); - if (protoPath) { - const protoContents = await readTextFile(protoPath); - const definition = await parse(protoContents); - console.log(definition); + constructor(private protoDefinitionService: ProtoDefinitionService) { + effect(() => { + const element = this.code()?.nativeElement; + if (element) { + hljs.highlightElement(element); } - } catch (err) { - console.error(err); - } + }); } } diff --git a/src/app/file-tree/file-tree.component.css b/src/app/file-tree/file-tree.component.css deleted file mode 100644 index 5d4e87f..0000000 --- a/src/app/file-tree/file-tree.component.css +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} diff --git a/src/app/file-tree/file-tree.component.html b/src/app/file-tree/file-tree.component.html new file mode 100644 index 0000000..16ebc35 --- /dev/null +++ b/src/app/file-tree/file-tree.component.html @@ -0,0 +1,23 @@ + + + + {{ node.file.name }} + + + + {{ node.file.name }} + + diff --git a/src/app/file-tree/file-tree.component.ts b/src/app/file-tree/file-tree.component.ts index dc4a908..984e7cc 100644 --- a/src/app/file-tree/file-tree.component.ts +++ b/src/app/file-tree/file-tree.component.ts @@ -19,6 +19,7 @@ export interface FileOrFolder { isDirectory: boolean; name: string; children?: FileOrFolder[]; + path: string; } interface FileNode { @@ -31,29 +32,7 @@ interface FileNode { selector: 'app-file-tree', standalone: true, imports: [CommonModule, MatTreeModule, MatIconModule, MatButtonModule], - template: ` - - - {{ node.file.name }} - - - - {{ node.file.name }} - - `, + templateUrl: './file-tree.component.html', styleUrl: './file-tree.component.scss', }) export class FileTreeComponent { diff --git a/src/app/model/proto-message.model.ts b/src/app/model/proto-message.model.ts new file mode 100644 index 0000000..bd516c8 --- /dev/null +++ b/src/app/model/proto-message.model.ts @@ -0,0 +1,19 @@ +export enum MessageType { + String, + Boolean, + Numeric, + List, + Map, + Object, +} + +export interface ProtoMessageField { + name: string; + type: MessageType; + value: any; +} + +export interface ProtoMessage { + name: string; + values: ProtoMessageField[]; +} diff --git a/src/app/proto-definition-selector/proto-definition-selector.component.css b/src/app/proto-definition-selector/proto-definition-selector.component.css new file mode 100644 index 0000000..6ead0bc --- /dev/null +++ b/src/app/proto-definition-selector/proto-definition-selector.component.css @@ -0,0 +1,16 @@ +:host { + display: block; + border-radius: var(--mat-sidenav-container-shape); + padding: var(--mat-sidenav-container-shape); + height: 100%; + box-sizing: border-box; + transition: background-color 100ms linear; +} + +:host.droppable * { + pointer-events: none; +} + +input { + display: none; +} diff --git a/src/app/proto-definition-selector/proto-definition-selector.component.ts b/src/app/proto-definition-selector/proto-definition-selector.component.ts new file mode 100644 index 0000000..38c18c0 --- /dev/null +++ b/src/app/proto-definition-selector/proto-definition-selector.component.ts @@ -0,0 +1,127 @@ +import { MediaMatcher } from '@angular/cdk/layout'; +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + HostListener, + OnDestroy, + output, + signal, + viewChild, +} from '@angular/core'; +import { MatListModule } from '@angular/material/list'; +import { ProtoMessage } from '../model/proto-message.model'; +import { ProtoDefinitionService } from '../proto-definition.service'; +import { MatButtonModule } from '@angular/material/button'; + +@Component({ + selector: 'app-proto-definition-selector', + standalone: true, + imports: [CommonModule, MatListModule, MatButtonModule], + template: ` +

Protobuf Definitions

+ + @for (item of definitionFiles(); track $index) { + {{ + item.name + }} + } + + + + + @for (item of selectedDefinition(); track $index) { + {{ + item.name + }} + } + + + `, + styleUrl: './proto-definition-selector.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProtoDefinitionSelectorComponent { + protoSelector = viewChild>('protoSelector'); + messageSelected = output(); + + protected definitionFiles = signal([]); + protected selectedDefinition = signal([]); + protected isDragging = signal(false); + + private currentFiles: string[] = []; + + @HostBinding('class.droppable') + get droppable() { + return this.isDragging(); + } + + constructor( + private protoDefinitionService: ProtoDefinitionService, + private elementRef: ElementRef + ) {} + + protected async addDefinitionFiles() { + const files = this.protoSelector()?.nativeElement.files; + if (files) { + const definitionFiles = this.definitionFiles(); + for (let i = 0; i < files.length; i++) { + if (!this.currentFiles.includes(files[i].name)) { + definitionFiles.push(files[i]); + this.currentFiles.push(files[i].name); + } + } + this.definitionFiles.set(definitionFiles); + } + } + + protected async selectProtoDefinition(file: File) { + try { + const protoContents = await file.text(); + const messageObjects = + await this.protoDefinitionService.parseProtoDefinition(protoContents); + this.selectedDefinition.set(messageObjects); + } catch (err) { + console.error(err); + alert( + "Failed to parse protobuf definition, please check it's a valid file." + ); + } + } + + @HostListener('dragover', ['$event']) + onDrag(event: DragEvent) { + event.preventDefault(); + } + + @HostListener('drop', ['$event']) + onDrop(event: DragEvent) { + event.preventDefault(); + const protoSelector = this.protoSelector(); + if (protoSelector) { + protoSelector.nativeElement.files = event.dataTransfer?.files ?? null; + } + this.isDragging.set(false); + this.addDefinitionFiles(); + } + + @HostListener('dragenter') + onDragEnter() { + this.isDragging.set(true); + } + + @HostListener('dragleave', ['$event']) + onDragLeave(event: DragEvent) { + if (event.target) this.isDragging.set(false); + } +} diff --git a/src/app/proto-definition.service.spec.ts b/src/app/proto-definition.service.spec.ts new file mode 100644 index 0000000..70b54b3 --- /dev/null +++ b/src/app/proto-definition.service.spec.ts @@ -0,0 +1,57 @@ +import { TestBed } from '@angular/core/testing'; + +import { provideExperimentalZonelessChangeDetection } from '@angular/core'; +import { MessageType, ProtoMessage } from './model/proto-message.model'; +import { ProtoDefinitionService } from './proto-definition.service'; + +let testProto = ` +syntax="proto3"; + +message Test { + string hello = 1; + int32 hello2 = 2; + int64 hello3 = 3; + float hello4 = 4; + double hello5 = 5; + bool hello6 = 6; + repeated string hello7 = 7; + map hello8 = 8; +} +`; + +describe('TestService', () => { + let service: ProtoDefinitionService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideExperimentalZonelessChangeDetection()], + }); + service = TestBed.inject(ProtoDefinitionService); + }); + + it('should convert parse protobuf correctly', async () => { + const testMessages = await service.parseProtoDefinition(testProto); + const converted = testMessages[0]; + + expect(converted.name).toBe('Test'); + + expect(converted.values.length).toBe(8); + checkNameAndType(converted, 'hello', MessageType.String); + checkNameAndType(converted, 'hello2', MessageType.Numeric); + checkNameAndType(converted, 'hello3', MessageType.Numeric); + checkNameAndType(converted, 'hello4', MessageType.Numeric); + checkNameAndType(converted, 'hello5', MessageType.Numeric); + checkNameAndType(converted, 'hello6', MessageType.Boolean); + checkNameAndType(converted, 'hello7', MessageType.List); + checkNameAndType(converted, 'hello8', MessageType.Map); + }); +}); + +const checkNameAndType = ( + converted: ProtoMessage, + name: string, + type: MessageType +) => { + const field = converted.values.find((value) => value.name === name); + expect(field?.type).toBe(type); +}; diff --git a/src/app/proto-definition.service.ts b/src/app/proto-definition.service.ts new file mode 100644 index 0000000..0b38f4c --- /dev/null +++ b/src/app/proto-definition.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; +import { FieldBase, MapField, ReflectionObject, parse } from 'protobufjs'; +import { MessageType, ProtoMessage } from './model/proto-message.model'; + +@Injectable({ + providedIn: 'root', +}) +export class ProtoDefinitionService { + constructor() {} + + async parseProtoDefinition(protoContents: string): Promise { + const definition = await parse(protoContents); + const messages = definition.root.nested; + if (messages) { + const objectDefinitions = Object.values(messages); + const messageObjects = []; + for (const objectDefinition of objectDefinitions) { + // TODO: Better way to check for message type (i.e. not enum)? + if ('fieldsArray' in objectDefinition) { + messageObjects.push( + this.convertProtoDefinitionToModel(objectDefinition) + ); + } + } + return messageObjects; + } + return []; + } + + private convertProtoDefinitionToModel( + messageDefinition?: ReflectionObject + ): ProtoMessage { + if (messageDefinition && 'fieldsArray' in messageDefinition) { + const convertedFields: ProtoMessage = { + name: messageDefinition.name, + values: [], + }; + const fields = messageDefinition.fieldsArray as FieldBase[]; + // TODO: Needs to be a visitor/tree search since messages/enums may be nested + for (const field of fields) { + // TODO: Better way to check this? + if ('rule' in field && field.rule === 'repeated') { + // Repeated/list field + } else if (field instanceof MapField) { + // Map + } else { + switch (field.type) { + case 'string': + break; + case 'int32': + case 'int64': + case 'float': + case 'double': + break; + case 'bool': + break; + // TODO: bytes as well, though that's pretty useless (can't really represent/edit it?) + } + } + convertedFields.values.push({ + name: field.name, + type: MessageType.String, + value: '', + }); + } + return convertedFields; + } + throw 'ReflectionObject is not a message'; + } +} diff --git a/src/index.html b/src/index.html index 645c4d4..3590418 100644 --- a/src/index.html +++ b/src/index.html @@ -1,15 +1,21 @@ - - - Test - - - - - - - - - + + + Test + + + + + + + + + diff --git a/src/styles.scss b/src/styles.scss index 591a754..c841266 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -5,7 +5,16 @@ @mixin custom-colours($theme) { .mat-toolbar { - background-color: mat.get-theme-color($theme, primary-container); + background-color: mat.get-theme-color($theme, tertiary-container); + } + + app-proto-definition-selector { + background-color: mat.get-theme-color($theme, secondary-container); + transition: background-color 100ms linear; + } + + .droppable { + background-color: mat.get-theme-color($theme, secondary, 40); } } diff --git a/test.proto b/test.proto new file mode 100644 index 0000000..268b235 --- /dev/null +++ b/test.proto @@ -0,0 +1,12 @@ +syntax="proto3"; + +message Test { + string hello = 1; + int32 hello2 = 2; + int64 hello3 = 3; + float hello4 = 4; + double hello5 = 5; + bool hello6 = 6; + repeated string hello7 = 7; + map hello8 = 8; +} \ No newline at end of file