From 5093b094f6e1cd7e337fec5590afa10c7244b054 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 8 Oct 2021 23:01:36 +0300 Subject: [PATCH] monaco editor value validators -- part 1 Signed-off-by: Roman --- .../components/dock/dock-tab-content.tsx | 23 ++++++--- .../components/monaco-editor/index.ts | 1 + .../monaco-editor/monaco-editor.tsx | 51 +++++++++++++++++-- .../monaco-editor/monaco-validators.ts | 48 +++++++++++++++++ 4 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 src/renderer/components/monaco-editor/monaco-validators.ts diff --git a/src/renderer/components/dock/dock-tab-content.tsx b/src/renderer/components/dock/dock-tab-content.tsx index b7c01f1cf0..e35cb1defa 100644 --- a/src/renderer/components/dock/dock-tab-content.tsx +++ b/src/renderer/components/dock/dock-tab-content.tsx @@ -26,7 +26,7 @@ import { computed, makeObservable } from "mobx"; import type { DockTab, TabId } from "./dock.store"; import { cssNames } from "../../utils"; import { DockTabComponents, dockViewsManager } from "./dock.views-manager"; -import { MonacoEditor } from "../monaco-editor"; +import { MonacoEditor, onMonacoContentChangeCallback } from "../monaco-editor"; export interface DockTabContentProps extends React.HTMLAttributes { className?: string; @@ -55,20 +55,29 @@ export class DockTabContent extends React.Component { * while switching between different tab.kind-s (e.g. "terminal" tab doesn't have editor) */ renderEditor() { - const { tabId } = this; - const { editor } = this.tabComponents; + const { tabId, tabComponents } = this; + const isHidden = !tabComponents.editor; return ( editor?.setValue(tabId, value)} - onError={error => editor?.onError?.(tabId, error)} + className={cssNames({ hidden: isHidden })} + value={tabComponents.editor?.getValue(tabId)} + onChange={this.onEditorChange} + onError={this.onEditorError} /> ); } + onEditorChange: onMonacoContentChangeCallback = (value) => { + this.tabComponents.editor?.setValue(this.tabId, value); + }; + + onEditorError = (error: string) => { + this.tabComponents.editor?.onError?.(this.tabId, error); + }; + + // TODO: provide error to dock's info-panel render() { const { children: bottomContent, bindContainerRef, className, tab } = this.props; diff --git a/src/renderer/components/monaco-editor/index.ts b/src/renderer/components/monaco-editor/index.ts index fb0a7e1183..5f9e47d46b 100644 --- a/src/renderer/components/monaco-editor/index.ts +++ b/src/renderer/components/monaco-editor/index.ts @@ -20,3 +20,4 @@ */ export * from "./monaco-editor"; +export * from "./monaco-validators"; diff --git a/src/renderer/components/monaco-editor/monaco-editor.tsx b/src/renderer/components/monaco-editor/monaco-editor.tsx index 6c28026c16..ba22f6c0af 100644 --- a/src/renderer/components/monaco-editor/monaco-editor.tsx +++ b/src/renderer/components/monaco-editor/monaco-editor.tsx @@ -27,6 +27,8 @@ import { editor, Uri } from "monaco-editor"; import { ThemeStore } from "../../theme.store"; import { UserStore } from "../../../common/user-store"; import { cssNames, disposer, toJS } from "../../utils"; +import { MonacoValidatorKey, monacoValidators } from "./monaco-validators"; +import debounce from "lodash/debounce"; export interface MonacoEditorProps { id?: string; // associating editor's ID with created model.uri @@ -37,25 +39,30 @@ export interface MonacoEditorProps { theme?: "vs" /* default, light theme */ | "vs-dark" | "hc-black" | string; language?: "yaml" | "json"; // configure bundled list of languages in via MonacoWebpackPlugin({languages: []}) options?: Partial; // customize editor's initialization options - onChange?: onChangeCallback; - onError?: onErrorCallback; + onChange?: onMonacoContentChangeCallback; onDidLayoutChange?(info: editor.EditorLayoutInfo): void; onDidContentSizeChange?(evt: editor.IContentSizeChangedEvent): void; + validators?: MonacoValidatorKey[], // validate changes with validators and catch error in `props.onError` + validateOnChange?: boolean; // run validators each time when editor's content/model changes (default: true) + onError?: onMonacoErrorCallback; // caught errors with `props.validators` } -export interface onChangeCallback { +// `props.onChange` called via editor's api value changes / user input, but when `props.value` changes +export interface onMonacoContentChangeCallback { (value: string, data: { model: editor.ITextModel, // current model event: editor.IModelContentChangedEvent; }): void; } -export interface onErrorCallback { +export interface onMonacoErrorCallback { (error: string): void; // TODO: provide validation or some another occurred error } export const defaultEditorProps: Partial = { language: "yaml", + validators: [], + validateOnChange: true, get theme(): MonacoEditorProps["theme"] { // theme for monaco-editor defined in `src/renderer/themes/lens-*.json` return ThemeStore.getInstance().activeTheme.monacoTheme; @@ -74,6 +81,7 @@ export class MonacoEditor extends React.Component { @observable.ref editor: editor.IStandaloneCodeEditor; @observable dimensions: { width?: number, height?: number } = {}; @observable unmounting = false; + validationErrors = observable.array(); // last validation errors (if any) constructor(props: MonacoEditorProps) { super(props); @@ -168,6 +176,7 @@ export class MonacoEditor extends React.Component { const value = this.editor.getValue(); // console.info("[MONACO]: value changed", { value, event }); + this.validateOnChange(value); this.props.onChange?.(value, { model: this.model, event, @@ -239,12 +248,46 @@ export class MonacoEditor extends React.Component { if (value == this.getValue()) return; this.editor.setValue(value); + this.validateOnChange(value); } getValue(opts?: { preserveBOM: boolean; lineEnding: string; }) { return this.editor.getValue(opts); } + // avoid excessive validations during typing + validateLazy = debounce((value: string) => this.validate(value), 250); + + protected validateOnChange(value: string) { + if (!this.props.validateOnChange) return; + + this.validateLazy(value); + } + + @action + async validate(value: string = this.getValue()): Promise { + const { validators } = this.props; + + if (!validators) return; + + this.validationErrors.clear(); // reset previous if any + + for (const validatorKey of validators) { + try { + await monacoValidators[validatorKey](value); // validate editor's content + } catch (error) { + this.validationErrors.push(String(error)); + } + } + + // emit error via props.onError()-callback if provided + if (this.props.onError && this.validationErrors.length) { + this.validationErrors.forEach(error => { + this.props.onError(error); + }); + } + } + bindRef = (elem: HTMLElement) => this.containerElem = elem; render() { diff --git a/src/renderer/components/monaco-editor/monaco-validators.ts b/src/renderer/components/monaco-editor/monaco-validators.ts new file mode 100644 index 0000000000..3a7c2e3392 --- /dev/null +++ b/src/renderer/components/monaco-editor/monaco-validators.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import yaml, { YAMLException } from "js-yaml"; + +export interface MonacoValidator { + (value: string): Promise; +} + +export const yamlValidator: MonacoValidator = async (value: string) => { + try { + await yaml.safeLoad(value); + } catch (error) { + throw String(error as YAMLException); + } +}; + +export const jsonValidator: MonacoValidator = async (value: string) => { + try { + await yaml.safeLoad(value, { json: true }); + } catch (error) { + throw String(error); + } +}; + +export const monacoValidators = { + yaml: yamlValidator, + json: jsonValidator, +}; + +export type MonacoValidatorKey = keyof typeof monacoValidators; // "json", "yaml"