diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 0ad35409b6..b2d488a17d 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -40,7 +40,6 @@ class Editor extends Plugin { this.renderComponent() }) this.currentTheme = translateTheme(this._deps.themeModule.currentTheme()) - this.models = [] // Init this.event = new EventManager() this.sessions = {} @@ -65,12 +64,14 @@ class Editor extends Plugin { rs: 'rust' } - this.onBreakPointAdded = (file, line) => this.triggerEvent('breakpointAdded', [file, line]) - this.onBreakPointCleared = (file, line) => this.triggerEvent('breakpointCleared', [file, line]) + this.activated = false - this.onDidChangeContent = (file) => this._onChange(file) - - this.onEditorMounted = () => this.triggerEvent('editorMounted', []) + this.events = { + onBreakPointAdded: (file, line) => this.triggerEvent('breakpointAdded', [file, line]), + onBreakPointCleared: (file, line) => this.triggerEvent('breakpointCleared', [file, line]), + onDidChangeContent: (file) => this._onChange(file), + onEditorMounted: () => this.triggerEvent('editorMounted', []) + } // to be implemented by the react component this.api = {} @@ -96,7 +97,15 @@ class Editor extends Plugin { renderComponent () { ReactDOM.render( - + , this.el) } @@ -106,6 +115,7 @@ class Editor extends Plugin { } onActivation () { + this.activated = true this.on('sidePanel', 'focusChanged', (name) => { this.keepDecorationsFor(name, 'sourceAnnotationsPerFile') this.keepDecorationsFor(name, 'markerPerFile') @@ -120,10 +130,6 @@ class Editor extends Plugin { this.off('sidePanel', 'pluginDisabled') } - setTheme (type) { - this.api.setTheme(this._themes[type]) - } - _onChange (file) { const currentFile = this._deps.config.get('currentFile') if (!currentFile) { @@ -180,18 +186,19 @@ class Editor extends Plugin { * @param {string} mode Mode for this file [Default is `text`] */ _createSession (path, content, mode) { - this.api.addModel(content, mode, path, false) + if (!this.activated) return + this.emit('addModel', content, mode, path, false) return { path, language: mode, setValue: (content) => { - this.api.setValue(path, content) + this.emit('setValue', path, content) }, getValue: () => { return this.api.getValue(path, content) }, dispose: () => { - this.api.disposeModel(path) + this.emit('disposeModel', path) } } } @@ -208,9 +215,9 @@ class Editor extends Plugin { * Display an Empty read-only session */ displayEmptyReadOnlySession () { + if (!this.activated) return this.currentFile = null - this.api.addModel('', 'text', '_blank', true) - this.api.setCurrentPath('_blank') + this.emit('addModel', '', 'text', '_blank', true) } /** @@ -324,9 +331,10 @@ class Editor extends Plugin { * @param {number} incr The amount of pixels to add to the font. */ editorFontSize (incr) { + if (!this.activated) return const newSize = this.api.getFontSize() + incr if (newSize >= 6) { - this.api.setFontSize(newSize) + this.emit('setFontSize', newSize) } } @@ -335,7 +343,8 @@ class Editor extends Plugin { * @param {boolean} useWrapMode Enable (or disable) wrap mode */ resize (useWrapMode) { - this.api.setWordWrap(useWrapMode) + if (!this.activated) return + this.emit('setWordWrap', useWrapMode) } /** @@ -344,8 +353,9 @@ class Editor extends Plugin { * @param {number} col */ gotoLine (line, col) { - this.api.focus() - this.api.revealLine(line + 1, col) + if (!this.activated) return + this.emit('focus') + this.emit('revealLine', line + 1, col) } /** @@ -353,7 +363,8 @@ class Editor extends Plugin { * @param {number} line The line to scroll to */ scrollToLine (line) { - this.api.revealLine(line) + if (!this.activated) return + this.emit('revealLine', line, 0) } /** diff --git a/libs/remix-ui/editor/src/lib/actions/editor.ts b/libs/remix-ui/editor/src/lib/actions/editor.ts new file mode 100644 index 0000000000..ee12a1691c --- /dev/null +++ b/libs/remix-ui/editor/src/lib/actions/editor.ts @@ -0,0 +1,125 @@ +export interface Action { + type: string; + payload: Record + monaco: any +} + +export const initialState = {} + +export const reducerActions = (models = initialState, action: Action) => { + const monaco = action.monaco + switch (action.type) { + case 'ADD_MODEL': { + if (!monaco) return models + const uri = action.payload.uri + const value = action.payload.value + const language = action.payload.language + const readOnly = action.payload.readOnly + if (models[uri]) return models // already existing + const model = monaco.editor.createModel(value, language, monaco.Uri.parse(uri)) + model.onDidChangeContent(() => action.payload.onDidChangeContent(uri)) + models[uri] = { language, uri, readOnly, model } + return models + } + case 'DISPOSE_MODEL': { + const uri = action.payload.uri + const model = models[uri]?.model + if (model) model.dispose() + delete models[uri] + return models + } + case 'SET_VALUE': { + if (!monaco.editor) return models + const uri = action.payload.uri + const value = action.payload.value + const model = models[uri]?.model + if (model) { + model.setValue(value) + } + return models + } + case 'REVEAL_LINE': { + if (!monaco.editor) return models + const line = action.payload.line + const column = action.payload.column + monaco.editor.revealLine(line) + monaco.editor.setPosition({ column, lineNumber: line }) + return models + } + case 'FOCUS': { + if (!monaco.editor) return models + monaco.editor.focus() + return models + } + case 'SET_FONTSIZE': { + if (!monaco.editor) return models + const size = action.payload.size + monaco.editor.updateOptions({ fontSize: size }) + return models + } + case 'SET_WORDWRAP': { + if (!monaco.editor) return models + const wrap = action.payload.wrap + monaco.editor.updateOptions({ wordWrap: wrap ? 'on' : 'off' }) + return models + } + } +} + +export const reducerListener = (plugin, dispatch, monaco) => { + plugin.on('editor', 'addModel', (value, language, uri, readOnly) => { + dispatch({ + type: 'ADD_MODEL', + payload: { uri, value, language, readOnly }, + monaco + }) + }) + + plugin.on('editor', 'disposeModel', (uri) => { + dispatch({ + type: 'DISPOSE_MODEL', + payload: { uri }, + monaco + }) + }) + + plugin.on('editor', 'setValue', (uri, value) => { + dispatch({ + type: 'SET_VALUE', + payload: { uri, value }, + monaco + }) + }) + + plugin.on('editor', 'revealLine', (line, column) => { + dispatch({ + type: 'REVEAL_LINE', + payload: { line, column }, + monaco + }) + }) + + plugin.on('editor', 'focus', () => { + dispatch({ + type: 'FOCUS', + payload: {}, + monaco + }) + }) + + plugin.on('editor', 'setFontSize', (size) => { + dispatch({ + type: 'SET_FONTSIZE', + payload: { size }, + monaco + }) + }) + + plugin.on('editor', 'setWordWrap', (wrap) => { + dispatch({ + type: 'SET_WORDWRAP', + payload: { wrap }, + monaco + }) + }) +} diff --git a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx index d85da7c6c9..2729970a68 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -1,5 +1,6 @@ -import React, { useState, useRef, useEffect } from 'react' +import React, { useState, useRef, useEffect, useReducer } from 'react' import Editor from '@monaco-editor/react' +import { reducerActions, reducerListener, initialState } from './actions/editor' import './remix-ui-editor.css' @@ -44,31 +45,29 @@ type sourceMarkerMap = { /* eslint-disable-next-line */ export interface EditorUIProps { + activated: boolean theme: string currentFile: string sourceAnnotationsPerFile: sourceAnnotationMap markerPerFile: sourceMarkerMap - onBreakPointAdded: (file: string, line: number) => void - onBreakPointCleared: (file: string, line: number) => void - onDidChangeContent: (file: string) => void - onEditorMounted: () => void + events: { + onBreakPointAdded: (file: string, line: number) => void + onBreakPointCleared: (file: string, line: number) => void + onDidChangeContent: (file: string) => void + onEditorMounted: () => void + } + plugin: { + on: (plugin: string, event: string, listener: any) => void + } editorAPI:{ findMatches: (uri: string, value: string) => any - addModel: (value: string, language: string, uri: string, readOnly: boolean) => void - disposeModel: (uri: string) => void, - setFontSize: (fontSize: number) => void, getFontSize: () => number, getValue: (uri: string) => string getCursorPosition: () => cursorPosition - revealLine: (line: number, column: number) => void - focus: () => void - setWordWrap: (wrap: boolean) => void - setValue: (uri: string, value: string) => void } } export const EditorUI = (props: EditorUIProps) => { - const [models, setModels] = useState({}) const [, setCurrentBreakpoints] = useState({}) const [currentAnnotations, setCurrentAnnotations] = useState({}) const [currentMarkers, setCurrentMarkers] = useState({}) @@ -76,6 +75,8 @@ export const EditorUI = (props: EditorUIProps) => { const monacoRef = useRef(null) const currentFileRef = useRef('') + const [editorModelsState, dispatch] = useReducer(reducerActions, initialState) + useEffect(() => { if (!monacoRef.current) return monacoRef.current.editor.setTheme(props.theme) @@ -83,7 +84,7 @@ export const EditorUI = (props: EditorUIProps) => { const setAnnotationsbyFile = (uri) => { if (props.sourceAnnotationsPerFile[uri]) { - const model = models[uri]?.model + const model = editorModelsState[uri]?.model const newAnnotations = [] for (const annotation of props.sourceAnnotationsPerFile[uri]) { if (!annotation.hide) { @@ -106,7 +107,7 @@ export const EditorUI = (props: EditorUIProps) => { const setMarkerbyFile = (uri) => { if (props.markerPerFile[uri]) { - const model = models[uri]?.model + const model = editorModelsState[uri]?.model const newMarkers = [] for (const marker of props.markerPerFile[uri]) { if (!marker.hide) { @@ -134,8 +135,8 @@ export const EditorUI = (props: EditorUIProps) => { useEffect(() => { if (!editorRef.current) return currentFileRef.current = props.currentFile - editorRef.current.setModel(models[props.currentFile].model) - editorRef.current.updateOptions({ readOnly: models[props.currentFile].readOnly }) + editorRef.current.setModel(editorModelsState[props.currentFile].model) + editorRef.current.updateOptions({ readOnly: editorModelsState[props.currentFile].readOnly }) setAnnotationsbyFile(props.currentFile) setMarkerbyFile(props.currentFile) }, [props.currentFile]) @@ -150,80 +151,31 @@ export const EditorUI = (props: EditorUIProps) => { props.editorAPI.findMatches = (uri: string, value: string) => { if (!editorRef.current) return - const model = models[uri]?.model + const model = editorModelsState[uri]?.model if (model) return model.findMatches(value) } - props.editorAPI.addModel = (value: string, language: string, uri: string, readOnly: boolean) => { - if (!monacoRef.current) return - if (models[uri]) return // already existing - const model = monacoRef.current.editor.createModel(value, language, monacoRef.current.Uri.parse(uri)) - model.onDidChangeContent(() => props.onDidChangeContent(uri)) - setModels(prevState => { - prevState[uri] = { language, uri, readOnly, model } - return prevState - }) - } - - props.editorAPI.disposeModel = (uri: string) => { - const model = models[uri]?.model - if (model) model.dispose() - setModels(prevState => { - delete prevState[uri] - return prevState - }) - } - props.editorAPI.getValue = (uri: string) => { if (!editorRef.current) return - const model = models[uri]?.model + const model = editorModelsState[uri]?.model if (model) { return model.getValue() } } - props.editorAPI.setValue = (uri: string, value: string) => { - if (!editorRef.current) return - const model = models[uri]?.model - if (model) { - model.setValue(value) - } - } - props.editorAPI.getCursorPosition = () => { if (!monacoRef.current) return - const model = models[currentFileRef.current]?.model + const model = editorModelsState[currentFileRef.current]?.model if (model) { return model.getOffsetAt(editorRef.current.getPosition()) } } - props.editorAPI.revealLine = (line: number, column: number) => { - if (!editorRef.current) return - editorRef.current.revealLine(line) - editorRef.current.setPosition({ column, lineNumber: line }) - } - - props.editorAPI.focus = () => { - if (!editorRef.current) return - editorRef.current.focus() - } - - props.editorAPI.setFontSize = (size: number) => { - if (!editorRef.current) return - editorRef.current.updateOptions({ fontSize: size }) - } - props.editorAPI.getFontSize = () => { if (!editorRef.current) return return editorRef.current.getOption(42).fontSize } - props.editorAPI.setWordWrap = (wrap: boolean) => { - if (!editorRef.current) return - editorRef.current.updateOptions({ wordWrap: wrap ? 'on' : 'off' }) - } - (window as any).addRemixBreakpoint = (position) => { // make it available from e2e testing... const model = editorRef.current.getModel() if (model) { @@ -232,11 +184,11 @@ export const EditorUI = (props: EditorUIProps) => { if (!prevState[currentFile]) prevState[currentFile] = {} const decoration = Object.keys(prevState[currentFile]).filter((line) => parseInt(line) === position.lineNumber) if (decoration.length) { - props.onBreakPointCleared(currentFile, position.lineNumber) + props.events.onBreakPointCleared(currentFile, position.lineNumber) model.deltaDecorations([prevState[currentFile][position.lineNumber]], []) delete prevState[currentFile][position.lineNumber] } else { - props.onBreakPointAdded(currentFile, position.lineNumber) + props.events.onBreakPointAdded(currentFile, position.lineNumber) const decorationIds = model.deltaDecorations([], [{ range: new monacoRef.current.Range(position.lineNumber, 1, position.lineNumber, 1), options: { @@ -254,7 +206,7 @@ export const EditorUI = (props: EditorUIProps) => { function handleEditorDidMount (editor) { editorRef.current = editor monacoRef.current.editor.setTheme(props.theme) - props.onEditorMounted() + props.events.onEditorMounted() editor.onMouseUp((e) => { if (e && e.target && e.target.toString().startsWith('GUTTER')) { (window as any).addRemixBreakpoint(e.target.position) @@ -264,6 +216,7 @@ export const EditorUI = (props: EditorUIProps) => { function handleEditorWillMount (monaco) { monacoRef.current = monaco + reducerListener(props.plugin, dispatch, monacoRef.current) // see https://microsoft.github.io/monaco-editor/playground.html#customizing-the-appearence-exposed-colors const backgroundColor = window.getComputedStyle(document.documentElement).getPropertyValue('--light').trim() const infoColor = window.getComputedStyle(document.documentElement).getPropertyValue('--info').trim() @@ -285,7 +238,7 @@ export const EditorUI = (props: EditorUIProps) => { width="100%" height="100%" path={props.currentFile} - language={models[props.currentFile] ? models[props.currentFile].language : 'text'} + language={editorModelsState[props.currentFile] ? editorModelsState[props.currentFile].language : 'text'} onMount={handleEditorDidMount} beforeMount={handleEditorWillMount} options= { { glyphMargin: true } }