Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions src/less/components/editors.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
134 changes: 67 additions & 67 deletions src/renderer/components/editors.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -184,81 +183,82 @@ export const Editors = observer(
}
}

/**
* Renders the little tool bar on top of each panel
*/
public renderToolbar(
{ title }: MosaicWindowProps<EditorId>,
id: EditorId,
): JSX.Element {
const { appState } = this.props;

return (
<div role="toolbar">
{/* Left */}
<div>
<h5>{title}</h5>
</div>
{/* Middle */}
<div />
{/* Right */}
<div className="mosaic-controls">
<MaximizeButton id={id} appState={appState} />
<RemoveButton id={id} appState={appState} />
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: can we just have const severityLevel = editorMosaic.editorSeverityMap here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toJS call here is to force a re-render when the values change.

Due to the way renderToolbar() is only called deep in the third-party Mosaic component, I don't think React + MobX is able to pick up the changes and re-render without this hack.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment indicating this

/**
* Renders the little toolbar on top of each panel
*/
const renderToolbar = (
{ title }: MosaicWindowProps<EditorId>,
id: EditorId,
) => {
const { appState } = this.props;
return (
<div role="toolbar">
{/* Left */}
<div>
<h5
className={`mosaic-toolbar-severity-level-${severityLevel.get(id)}`}
>
{title}
</h5>
</div>
{/* Middle */}
<div />
{/* Right */}
<div className="mosaic-controls">
<MaximizeButton id={id} appState={appState} />
<RemoveButton id={id} appState={appState} />
</div>
</div>
</div>
);
}

/**
* Renders a Mosaic tile
*/
public renderTile(id: EditorId, path: Array<MosaicBranch>): JSX.Element {
const content = this.renderEditor(id);
const title = getEditorTitle(id as EditorId);

return (
<MosaicWindow<EditorId>
className={id}
path={path}
title={title}
renderToolbar={(props: MosaicWindowProps<EditorId>) =>
this.renderToolbar(props, id)
}
>
{content}
</MosaicWindow>
);
}

/**
* Render an editor
*/
public renderEditor(id: EditorId): JSX.Element | null {
const { appState } = this.props;
const { monaco } = this.state;
);
};

return (
<Editor
id={id}
monaco={monaco}
appState={appState}
monacoOptions={defaultMonacoOptions}
setFocused={this.setFocused}
/>
);
}
/**
* Renders a Mosaic tile
*/
const renderTile = (id: EditorId, path: Array<MosaicBranch>) => {
const content = renderEditor(id);
const title = getEditorTitle(id as EditorId);

return (
<MosaicWindow<EditorId>
className={id}
path={path}
title={title}
renderToolbar={(props: MosaicWindowProps<EditorId>) =>
renderToolbar(props, id)
}
>
{content}
</MosaicWindow>
);
};

public render() {
const { editorMosaic } = this.props.appState;
const renderEditor = (id: EditorId) => {
const { appState } = this.props;
const { monaco } = this.state;

return (
<Editor
id={id}
monaco={monaco}
appState={appState}
monacoOptions={defaultMonacoOptions}
setFocused={this.setFocused}
/>
);
};

return (
<Mosaic<EditorId>
className={`focused__${this.state.focused}`}
onChange={this.onChange}
value={editorMosaic.mosaic}
zeroStateView={<RenderNonIdealState editorMosaic={editorMosaic} />}
renderTile={this.renderTile}
renderTile={renderTile}
/>
);
}
Expand Down
53 changes: 51 additions & 2 deletions src/renderer/editor-mosaic.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -84,6 +91,7 @@ export class EditorMosaic {
setEditorFromBackup: action,
addNewFile: action,
renameFile: action,
editorSeverityMap: observable,
});

// whenever the mosaics are changed,
Expand All @@ -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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: does this binding need to be cleaned up on destroy/ in the constructor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good callout. After looking a bit more, I don't think there's an easy way of disposing of this callback since JavaScript classes don't provide a lifecycle hook for when the class gets destroyed (in the same way that React does when a component gets unmounted).

Since EditorMosaic never gets destroyed in a Fiddle instance (we keep the same instance and recycle editors whenever possible), I think the potential for a large memory leak is reduced (but I could be wrong).

If this proves to be a problem, we might want to look into the FinalizationRegistry API to manually call the dispose() function when the EditorMosaic object gets GCed.

The whole "Avoid where possible" section in that API makes me just a bit wary for now.

Happy for pushback on this assessment, though.

}

/** File is visible, focus file content */
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -342,6 +363,10 @@ export class EditorMosaic {
}
};

public getAllEditorIds(): EditorId[] {
return [...this.editors.keys()];
}

public getAllEditors(): Editor[] {
return [...this.editors.values()];
}
Expand Down Expand Up @@ -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);
}
});
}
}
12 changes: 12 additions & 0 deletions tests/mocks/monaco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(),
Expand All @@ -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(),
};
Expand Down
Loading