diff --git a/package.json b/package.json index e5aaf8ca79..bc720bd0a7 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "getos": "^3.2.1", "mobx": "^6.5.0", "mobx-react": "^7.3.0", - "monaco-editor": "^0.21.3", + "monaco-editor": "^0.22.0", "namor": "^2.0.2", "node-watch": "^0.7.3", "p-debounce": "^2.0.0", @@ -134,7 +134,7 @@ "log-symbols": "^6.0.0", "markdownlint-cli2": "^0.18.0", "mini-css-extract-plugin": "^2.6.1", - "monaco-editor-webpack-plugin": "2.1.0", + "monaco-editor-webpack-plugin": "^3.0.0", "npm-run-all2": "^7.0.1", "postcss": "^8.4.25", "postcss-less": "^6.0.0", diff --git a/src/less/components/editors.less b/src/less/components/editors.less index fba46ab238..40a430549a 100644 --- a/src/less/components/editors.less +++ b/src/less/components/editors.less @@ -24,6 +24,20 @@ } } +// Set the toolbar text colour based on the mosaic's +// current severity level. +// Hint=1, +// Info=2, +// Warning=4, +// Error=8 +.mosaic-toolbar-severity-level-8 { + color: @red4; +} + +.mosaic-toolbar-severity-level-4 { + color: @orange4; +} + // TODO: support new file // update if list of Editor ID changes @editor-ids: main\.js, renderer\.js, index\.html, preload\.js, styles\.css; diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index f5e077b82b..0674c616b2 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -68,6 +68,7 @@ export class App { } this.state.editorMosaic.set(editorValues); + this.state.editorMosaic.editorSeverityMap.clear(); this.state.gistId = gistId || ''; this.state.localPath = localFiddle?.filePath; diff --git a/src/renderer/components/editors.tsx b/src/renderer/components/editors.tsx index 24f498cacd..3310a1bc35 100644 --- a/src/renderer/components/editors.tsx +++ b/src/renderer/components/editors.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; +import { toJS } from 'mobx'; import { observer } from 'mobx-react'; import type * as MonacoType from 'monaco-editor'; import { @@ -42,8 +43,6 @@ export const Editors = observer( super(props); this.onChange = this.onChange.bind(this); - this.renderEditor = this.renderEditor.bind(this); - this.renderTile = this.renderTile.bind(this); this.setFocused = this.setFocused.bind(this); this.state = { @@ -184,73 +183,74 @@ export const Editors = observer( } } - /** - * Renders the little tool bar on top of each panel - */ - public renderToolbar( - { title }: MosaicWindowProps, - id: EditorId, - ): JSX.Element { - const { appState } = this.props; - - return ( -
- {/* Left */} -
-
{title}
-
- {/* Middle */} -
- {/* Right */} -
- - + public render() { + const { editorMosaic } = this.props.appState; + // HACK: we use this to force re-renders of the toolbar when severity changes + const severityLevel = toJS(editorMosaic.editorSeverityMap); + /** + * Renders the little toolbar on top of each panel + */ + const renderToolbar = ( + { title }: MosaicWindowProps, + id: EditorId, + ) => { + const { appState } = this.props; + return ( +
+ {/* Left */} +
+
+ {title} +
+
+ {/* Middle */} +
+ {/* Right */} +
+ + +
-
- ); - } - - /** - * Renders a Mosaic tile - */ - public renderTile(id: EditorId, path: Array): JSX.Element { - const content = this.renderEditor(id); - const title = getEditorTitle(id as EditorId); - - return ( - - className={id} - path={path} - title={title} - renderToolbar={(props: MosaicWindowProps) => - this.renderToolbar(props, id) - } - > - {content} - - ); - } - - /** - * Render an editor - */ - public renderEditor(id: EditorId): JSX.Element | null { - const { appState } = this.props; - const { monaco } = this.state; + ); + }; - return ( - - ); - } + /** + * Renders a Mosaic tile + */ + const renderTile = (id: EditorId, path: Array) => { + const content = renderEditor(id); + const title = getEditorTitle(id as EditorId); + + return ( + + className={id} + path={path} + title={title} + renderToolbar={(props: MosaicWindowProps) => + renderToolbar(props, id) + } + > + {content} + + ); + }; - public render() { - const { editorMosaic } = this.props.appState; + const renderEditor = (id: EditorId) => { + const { appState } = this.props; + const { monaco } = this.state; + + return ( + + ); + }; return ( @@ -258,7 +258,7 @@ export const Editors = observer( onChange={this.onChange} value={editorMosaic.mosaic} zeroStateView={} - renderTile={this.renderTile} + renderTile={renderTile} /> ); } diff --git a/src/renderer/editor-mosaic.ts b/src/renderer/editor-mosaic.ts index 652a7c2ddb..ac7e9f4005 100644 --- a/src/renderer/editor-mosaic.ts +++ b/src/renderer/editor-mosaic.ts @@ -1,4 +1,11 @@ -import { action, computed, makeObservable, observable, reaction } from 'mobx'; +import { + action, + computed, + makeObservable, + observable, + reaction, + runInAction, +} from 'mobx'; import type * as MonacoType from 'monaco-editor'; import { MosaicDirection, MosaicNode, getLeaves } from 'react-mosaic-component'; @@ -84,6 +91,7 @@ export class EditorMosaic { setEditorFromBackup: action, addNewFile: action, renameFile: action, + editorSeverityMap: observable, }); // whenever the mosaics are changed, @@ -104,6 +112,9 @@ export class EditorMosaic { } }, ); + // TODO: evaluate if we need to dispose of the listener when this class is + // destroyed via FinalizationRegistry + window.monaco.editor.onDidChangeMarkers(this.setSeverityLevels.bind(this)); } /** File is visible, focus file content */ @@ -160,8 +171,17 @@ export class EditorMosaic { // create a monaco model with the file's contents const { monaco } = window; const language = monacoLanguage(id); - const model = monaco.editor.createModel(value, language); + // set a URI for each editor for stable identification for monaco features + const uri = monaco.Uri.parse(`inmemory://fiddle/${id}`); + let model: MonacoType.editor.ITextModel; + const maybeModel = monaco.editor.getModel(uri); + if (maybeModel) { + model = maybeModel; + model.setValue(value); + } else { + model = monaco.editor.createModel(value, language, uri); + } // if we have an editor available, use the monaco model now. // otherwise, save the file in `this.backups` for future use. const backup: EditorBackup = { model }; @@ -263,6 +283,7 @@ export class EditorMosaic { this.backups.delete(id); this.editors.set(id, editor); + this.editorSeverityMap.set(id, window.monaco.MarkerSeverity.Hint); this.setEditorFromBackup(editor, backup); } @@ -342,6 +363,10 @@ export class EditorMosaic { } }; + public getAllEditorIds(): EditorId[] { + return [...this.editors.keys()]; + } + public getAllEditors(): Editor[] { return [...this.editors.values()]; } @@ -380,4 +405,28 @@ export class EditorMosaic { disposable.dispose(); }); } + + public editorSeverityMap = observable.map< + EditorId, + MonacoType.MarkerSeverity + >(); + + public setSeverityLevels() { + runInAction(() => { + for (const id of this.getAllEditorIds()) { + const markers = window.monaco.editor.getModelMarkers({ + resource: window.monaco.Uri.parse(`inmemory://fiddle/${id}`), + }); + + const maxSeverity: MonacoType.MarkerSeverity = markers.reduce( + (max, marker) => { + return Math.max(max, marker.severity); + }, + window.monaco.MarkerSeverity.Hint, + ); + + this.editorSeverityMap.set(id, maxSeverity); + } + }); + } } diff --git a/tests/mocks/monaco.ts b/tests/mocks/monaco.ts index 00334e5d90..7bb8e216f4 100644 --- a/tests/mocks/monaco.ts +++ b/tests/mocks/monaco.ts @@ -26,6 +26,7 @@ export class MonacoMock { public latestModel: any; public editor = { create: vi.fn(() => (this.latestEditor = new MonacoEditorMock())), + getModel: vi.fn(), createModel: vi.fn((value: string, language: string) => { const model = new MonacoModelMock(value, language); this.latestModel = model; @@ -35,6 +36,8 @@ export class MonacoMock { onDidFocusEditorText: vi.fn(), revealLine: vi.fn(), setTheme: vi.fn(), + onDidChangeMarkers: vi.fn(), + getModelMarkers: vi.fn(() => []), }; public languages = { register: vi.fn(), @@ -46,6 +49,15 @@ export class MonacoMock { }, }, }; + public Uri = { + parse: vi.fn((uri: string) => ({ toString: () => uri })), + }; + public MarkerSeverity = { + Hint: 1, + Info: 2, + Warning: 4, + Error: 8, + }; public KeyMod = { CtrlCmd: vi.fn(), }; diff --git a/tests/renderer/editor-mosaic-spec.ts b/tests/renderer/editor-mosaic-spec.ts index 53a8e3bf81..5ac191b889 100644 --- a/tests/renderer/editor-mosaic-spec.ts +++ b/tests/renderer/editor-mosaic-spec.ts @@ -101,6 +101,7 @@ describe('EditorMosaic', () => { expect(monaco.editor.createModel).toHaveBeenCalledWith( content, expect.anything(), + expect.anything(), ); }); }); @@ -303,6 +304,9 @@ describe('EditorMosaic', () => { // now call set again, same filename DIFFERENT content content = '// second content'; + vi.mocked(monaco.editor.getModel).mockReturnValueOnce( + monaco.latestModel, + ); editorMosaic.set({ [id]: content }); // test that editorMosaic set the editor to the new content expect(editor.getValue()).toBe(content); @@ -310,6 +314,9 @@ describe('EditorMosaic', () => { // test that the editor still responds to edits content = '// third content'; + vi.mocked(monaco.editor.getModel).mockReturnValueOnce( + monaco.latestModel, + ); editor.setValue(content); expect(editorMosaic.isEdited).toBe(true); @@ -319,6 +326,9 @@ describe('EditorMosaic', () => { // test that the editor still responds to edits content = '// fourth content'; + vi.mocked(monaco.editor.getModel).mockReturnValueOnce( + monaco.latestModel, + ); editor.setValue(content); expect(editorMosaic.isEdited).toBe(true); }); @@ -541,4 +551,52 @@ describe('EditorMosaic', () => { dispose(); }); }); + + describe('setSeverityLevels', () => { + it.each([ + { + markers: [ + { + severity: window.monaco.MarkerSeverity.Error, + message: 'Error message', + }, + { + severity: window.monaco.MarkerSeverity.Warning, + message: 'Warning message', + }, + ], + expectedSeverity: window.monaco.MarkerSeverity.Error, + }, + { + markers: [ + { + severity: window.monaco.MarkerSeverity.Warning, + message: 'Warning message', + }, + ], + expectedSeverity: window.monaco.MarkerSeverity.Warning, + }, + { + markers: [], + expectedSeverity: window.monaco.MarkerSeverity.Hint, + }, + ])( + 'updates severity levels based on Monaco markers', + ({ markers, expectedSeverity }) => { + const id = MAIN_JS; + const editor = new MonacoEditorMock() as unknown as Editor; + editorMosaic.set({ [id]: '// content' }); + editorMosaic.addEditor(id, editor); + + vi.mocked(monaco.editor.getModelMarkers).mockReturnValueOnce( + markers as any, + ); + + editorMosaic.setSeverityLevels(); + + const severityLevels = editorMosaic.editorSeverityMap; + expect(severityLevels.get(id)).toBe(expectedSeverity); + }, + ); + }); }); diff --git a/tests/setup.ts b/tests/setup.ts index a8dea90ca8..1fb6653141 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -95,8 +95,8 @@ window.navigator = window.navigator ?? {}; * code called in individual tests. */ (window.ElectronFiddle as any) = new ElectronFiddleMock(); -(window.app as any) = new AppMock(); (window.monaco as any) = new MonacoMock(); +(window.app as any) = new AppMock(); window.localStorage.setItem = vi.fn(); window.localStorage.getItem = vi.fn(); window.localStorage.removeItem = vi.fn(); @@ -115,8 +115,8 @@ beforeEach(() => { document.body.innerHTML = '
'; (window.ElectronFiddle as any) = new ElectronFiddleMock(); - (window.app as any) = new AppMock(); (window.monaco as any) = new MonacoMock(); + (window.app as any) = new AppMock(); vi.mocked(window.localStorage.setItem).mockReset(); vi.mocked(window.localStorage.getItem).mockReset(); vi.mocked(window.localStorage.removeItem).mockReset(); diff --git a/yarn.lock b/yarn.lock index 8c4c2dbe66..a14746762c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6192,8 +6192,8 @@ __metadata: mini-css-extract-plugin: "npm:^2.6.1" mobx: "npm:^6.5.0" mobx-react: "npm:^7.3.0" - monaco-editor: "npm:^0.21.3" - monaco-editor-webpack-plugin: "npm:2.1.0" + monaco-editor: "npm:^0.22.0" + monaco-editor-webpack-plugin: "npm:^3.0.0" namor: "npm:^2.0.2" node-watch: "npm:^0.7.3" npm-run-all2: "npm:^7.0.1" @@ -10886,22 +10886,22 @@ __metadata: languageName: node linkType: hard -"monaco-editor-webpack-plugin@npm:2.1.0": - version: 2.1.0 - resolution: "monaco-editor-webpack-plugin@npm:2.1.0" +"monaco-editor-webpack-plugin@npm:^3.0.0": + version: 3.1.0 + resolution: "monaco-editor-webpack-plugin@npm:3.1.0" dependencies: loader-utils: "npm:^2.0.0" peerDependencies: - monaco-editor: 0.21.x + monaco-editor: 0.22.x || 0.23.x || 0.24.x webpack: ^4.5.0 || 5.x - checksum: 10c0/47d891383b46c0fcbcdf6732b0d5e8886bbe28c4ad2054f9a45750efafd8267c3116e6adfb66bf8996c26eeb8b1609cbb6f9dbb3d741a15cbd03a909f8ec20f9 + checksum: 10c0/c8e094968eeb63a02c764e60bc38e56e1fbad9be9551785bff291da65196bc0061e13ff507c3f786bde92bf270a0cdfc653a99fc88034305a7b6ceeebcb6e20f languageName: node linkType: hard -"monaco-editor@npm:^0.21.3": - version: 0.21.3 - resolution: "monaco-editor@npm:0.21.3" - checksum: 10c0/846a5007fee92eb64bbbe162fc2d06ea908caac9a9072da6903724c3e3959a3c9f8c15eedff2b9f98ce8a11bf7781305b16ad8125bcfe39a13a6aecc1ad9c0d3 +"monaco-editor@npm:^0.22.0": + version: 0.22.3 + resolution: "monaco-editor@npm:0.22.3" + checksum: 10c0/a95f82f7788617d1f418e42d478b278ba83adfb3a4659cc813378867ad0d6b7a7dc8e1395afac5ec00b2064cef083b1fe75f77b4edd7aae57de5ed6fa49bc9a4 languageName: node linkType: hard