react-diagram替换remix中editor的思路与操作

0. 需求

  • 需要在remix-ide的editor组件中嵌入react-diagram,并且能够交互。

0.1. 需求附图

在这里插入图片描述

  • 右上半部分为Editor。

0.2. React-diagram

在这里插入图片描述

  • React-diagram如上图所示。

1. 难点

  • Remix项目的每一个组件是使用remix-project开发的一个插件库@remixproject/engine制作的,每个插件有互相沟通的通道,每个插件内的函数都被可能被其他的组件调用。
  • Remix项目并不是完全使用react构建的,但是react-diagram完全是依赖于react的。
  • react-diagram使用的是typescript,而Remix整个项目都是使用javascript。

2. 步骤分解

  • 取消editor所有的函数内容以及变量内容,但是保留所有的函数外壳。(关键)
    • 其他的组件调用的时候不会找不到函数和变量。
    • 技巧:逐步注释和编译,避免定位不到前端的报错。
  • 给remix-project安装react-diagram框架。
    • 需要特别理解peer dependencies的内容,需要理解npm报错该怎么处理。
  • 搭建一个react-diagram最简单的demo。
  • 尝试着将demo中的内容迁移到remix-project。
    • 需要把typescript转换成javascript。

3. 实际操作

3.1. 安装remix-project

  • 进入你选定的一个文件夹。
  • sudo npm install -g @nrwl/cli
  • git clone https://github.com/ethereum/remix-project.git
  • cd remix-project
  • npm install

3.2. 给项目嵌入antlr4

  • 如果电脑没有Java环境需要去官网默认安装。
  • npm install --save-dev antlr4-tool
  • npm install -S antlr4@4.8.0
  • 把你的.g4文件放入remix-project/apps/remix-ide/src/app/editor/antlrScript这个目录。(不一定要这个路径,自选即可)
  • pakcage.json中的script中加入字段
"generate-parser":"antlr4-tool -o parser apps/remix-ide/src/app/editor/antlrScript/xxx.g4"
  • 下面这种情况代表成功
    在这里插入图片描述

3.3. 安装react-diagram

  • npm install -S @projectstorm/react-diagrams
  • npm install @projectstorm/react-diagrams@next
  • npm install -S closest lodash react dagre pathfinding paths-js resize-observer-polyfill ml-matrix @emotion/core@10.1.1 @emotion/styled@10.0.3
  • npm install -S @emotion/react
  • npm install -S styled-components
  • 然后启动remix-project
    • nx build remix-ide --with-deps
    • nx serve

3.4. 修改editor.js

  • 用下面的文件替换editor.js
'use strict'
import { Plugin } from '@remixproject/engine'

import * as packageJson from '../../../../../package.json'

const EventManager = require('../../lib/events')
const yo = require('yo-yo')
const csjs = require('csjs-inject')
const ace = require('brace')

const globalRegistry = require('../../global/registry')
const SourceHighlighters = require('./SourceHighlighters')

const Range = ace.acequire('ace/range').Range
require('brace/ext/language_tools')
require('brace/ext/searchbox')
const langTools = ace.acequire('ace/ext/language_tools')
require('ace-mode-solidity/build/remix-ide/mode-solidity')
require('ace-mode-move/build/remix-ide/mode-move')
require('ace-mode-zokrates')
require('ace-mode-lexon')
require('brace/mode/javascript')
require('brace/mode/python')
require('brace/mode/json')
require('brace/mode/rust')
require('brace/theme/chrome') // for all light themes
require('brace/theme/chaos') // for all dark themes
require('../../assets/js/editor/darkTheme') // a custom one for remix 'Dark' theme

const css = csjs`
  .ace-editor {
    width     : 100%;
  }
`
document.head.appendChild(yo`
  <style>
    .ace-tm .ace_gutter,
    .ace-tm .ace_gutter-active-line,
    .ace-tm .ace_marker-layer .ace_active-line {
        background-color: var(--secondary);
    }
    .ace_gutter-cell.ace_breakpoint{
      background-color: var(--secondary);
    }
  </style>
`)

const profile = {
  displayName: 'Editor',
  name: 'editor',
  description: 'service - editor',
  version: packageJson.version,
  methods: ['highlight', 'discardHighlight', 'discardHighlightAt', 'clearAnnotations', 'addAnnotation']
}

class Editor extends Plugin {
  constructor (opts = {}, themeModule) {
    super(profile)
    // Dependancies
    this._components = {}
    this._components.registry = globalRegistry
    this._deps = {
      config: this._components.registry.get('config').api
    }

    this._themes = {
      light: 'chrome',
      dark: 'chaos',
      remixDark: 'remixDark'
    }
    themeModule.events.on('themeChanged', (theme) => {
      this.setTheme(theme.name === 'Dark' ? 'remixDark' : theme.quality)
    })

    // Init
    this.event = new EventManager()
    this.sessions = {}
    this.sourceAnnotationsPerFile = []
    this.readOnlySessions = {}
    this.previousInput = ''
    this.saveTimeout = null
    this.sourceHighlighters = new SourceHighlighters()
    this.emptySession = this._createSession('')
    this.modes = {
      sol: 'ace/mode/solidity',
      yul: 'ace/mode/solidity',
      mvir: 'ace/mode/move',
      js: 'ace/mode/javascript',
      py: 'ace/mode/python',
      vy: 'ace/mode/python',
      zok: 'ace/mode/zokrates',
      lex: 'ace/mode/lexon',
      txt: 'ace/mode/text',
      json: 'ace/mode/json',
      abi: 'ace/mode/json',
      rs: 'ace/mode/rust'
    }
    
    // Editor Setup
    //const el = yo`<div id="input" data-id="editorInput"></div>`
    const el = yo`<div id='myApp'></div>`
    //this.editor = ace.edit(el)
    this.editor =null

    //ace.acequire('ace/ext/language_tools')
    /*
    // Unmap ctrl-l & cmd-l
    this.editor.commands.bindKeys({
      'ctrl-L': null,
      'Command-L': null
    })

    // shortcuts for "Ctrl-"" and "Ctrl+"" to increase/decrease font size of the editor
    this.editor.commands.addCommand({
      name: 'increasefontsizeEqual',
      bindKey: { win: 'Ctrl-=', mac: 'Command-=' },
      exec: (editor) => {
        this.editorFontSize(1)
      },
      readOnly: true
    })

    this.editor.commands.addCommand({
      name: 'increasefontsizePlus',
      bindKey: { win: 'Ctrl-+', mac: 'Command-+' },
      exec: (editor) => {
        this.editorFontSize(1)
      },
      readOnly: true
    })

    this.editor.commands.addCommand({
      name: 'decreasefontsize',
      bindKey: { win: 'Ctrl--', mac: 'Command--' },
      exec: (editor) => {
        this.editorFontSize(-1)
      },
      readOnly: true
    })

    this.editor.setShowPrintMargin(false)
    this.editor.resize(true)

    this.editor.setOptions({
      enableBasicAutocompletion: true,
      enableLiveAutocompletion: true
    })
    */
    //el.className += ' ' + css['ace-editor']
    el.className += ''
    el.editor = this.editor // required to access the editor during tests
    this.render = () => el
    
    /*
    // Completer for editor
    const flowCompleter = {
      getCompletions: (editor, session, pos, prefix, callback) => {
        // @TODO add here other propositions
      }
    }
    langTools.addCompleter(flowCompleter)

    // zoom with Ctrl+wheel
    window.addEventListener('wheel', (e) => {
      if (e.ctrlKey && Math.abs(e.wheelY) > 5) {
        this.editorFontSize(e.wheelY > 0 ? 1 : -1)
      }
    })
    */
    // EVENTS LISTENERS
    
    // Gutter Mouse down
    /*
    this.editor.on('guttermousedown', e => {
      const target = e.domEvent.target
      if (target.className.indexOf('ace_gutter-cell') === -1) {
        return
      }
      const row = e.getDocumentPosition().row
      const breakpoints = e.editor.session.getBreakpoints()
      for (const k in breakpoints) {
        if (k === row.toString()) {
          this.triggerEvent('breakpointCleared', [this.currentSession, row])
          e.editor.session.clearBreakpoint(row)
          e.stop()
          return
        }
      }
      this.setBreakpoint(row)
      this.triggerEvent('breakpointAdded', [this.currentSession, row])
      e.stop()
    })
  
    // Do setup on initialisation here
    this.editor.on('changeSession', () => {
      this._onChange()
      this.triggerEvent('sessionSwitched', [])
      this.editor.getSession().on('change', () => {
        this._onChange()
        this.sourceHighlighters.discardAllHighlights()
        this.triggerEvent('contentChanged', [])
      })
    })
    */
  }
  /*
  triggerEvent (name, params) {
    this.event.trigger(name, params) // internal stack
    this.emit(name, ...params) // plugin stack
  }
  */
  onActivation () {
    /*
    this.on('sidePanel', 'focusChanged', (name) => {
      this.sourceHighlighters.hideHighlightsExcept(name)
      this.keepAnnotationsFor(name)
    })
    this.on('sidePanel', 'pluginDisabled', (name) => {
      this.sourceHighlighters.discardHighlight(name)
      this.clearAllAnnotationsFor(name)
    })
    */
  }

  onDeactivation () {
    //this.off('sidePanel', 'focusChanged')
    //this.off('sidePanel', 'pluginDisabled')
  }

  highlight (position, filePath, hexColor) {
    //const { from } = this.currentRequest
    //this.sourceHighlighters.highlight(position, filePath, hexColor, from)
  }

  discardHighlight () {
    //const { from } = this.currentRequest
    //this.sourceHighlighters.discardHighlight(from)
  }

  discardHighlightAt (line, filePath) {
    //const { from } = this.currentRequest
    //this.sourceHighlighters.discardHighlightAt(line, filePath, from)
  }

  setTheme (type) {
    //this.editor.setTheme('ace/theme/' + this._themes[type])
  }

  _onChange () {
    /*
    const currentFile = this._deps.config.get('currentFile')
    if (!currentFile) {
      return
    }
    const input = this.get(currentFile)
    if (!input) {
      return
    }
    // if there's no change, don't do anything
    if (input === this.previousInput) {
      return
    }
    this.previousInput = input

    // fire storage update
    // NOTE: save at most once per 5 seconds
    if (this.saveTimeout) {
      window.clearTimeout(this.saveTimeout)
    }
    this.saveTimeout = window.setTimeout(() => {
      this.triggerEvent('requiringToSaveCurrentfile', [])
    }, 5000)
    */
  }

  _switchSession (path) {
    //this.currentSession = path
    //this.editor.setSession(this.sessions[this.currentSession])
    //this.editor.setReadOnly(this.readOnlySessions[this.currentSession])
    //this.editor.focus()
  }
  
  /**
   * Get Ace mode base of the extension of the session file
   * @param {string} path Path of the file
   */
  _getMode (path) {
    /*
    if (!path) return this.modes.txt
    const root = path.split('#')[0].split('?')[0]
    let ext = root.indexOf('.') !== -1 ? /[^.]+$/.exec(root) : null
    if (ext) ext = ext[0]
    else ext = 'txt'
    return ext && this.modes[ext] ? this.modes[ext] : this.modes.txt
    */
  }

  /**
   * Create an Ace session
   * @param {string} content Content of the file to open
   * @param {string} mode Ace Mode for this file [Default is `text`]
   */
  _createSession (content, mode) {
    /*
    const s = new ace.EditSession(content)
    s.setMode(mode || 'ace/mode/text')
    s.setUndoManager(new ace.UndoManager())
    s.setTabSize(4)
    s.setUseSoftTabs(true)
    return s
    */
  }

  /**
   * Attempts to find the string in the current document
   * @param {string} string
   */
  find (string) {
    //return this.editor.find(string)
  }

  /**
   * Display an Empty read-only session
   */
  displayEmptyReadOnlySession () {
    /*
    this.currentSession = null
    this.editor.setSession(this.emptySession)
    this.editor.setReadOnly(true)
    */
  }

  /**
   * Sets a breakpoint on the row number
   * @param {number} row Line index of the breakpoint
   * @param {string} className Class of the breakpoint
   */
  setBreakpoint (row, className) {
    //this.editor.session.setBreakpoint(row, className)
  }

  /**
   * Increment the font size (in pixels) for the editor text.
   * @param {number} incr The amount of pixels to add to the font.
   */
  editorFontSize (incr) {
    /*
    const newSize = this.editor.getFontSize() + incr
    if (newSize >= 6) {
      this.editor.setFontSize(newSize)
    }
    */
  }

  /**
   * Set the text in the current session, if any.
   * @param {string} text New text to be place.
   */
  setText (text) {
    /*
    if (this.currentSession && this.sessions[this.currentSession]) {
      this.sessions[this.currentSession].setValue(text)
    }
    */
  }

  /**
   * Upsert and open a session.
   * @param {string} path Path of the session to open.
   * @param {string} content Content of the document or update.
   */
  open (path, content) {
    /*
      we have the following cases:
       - URL prepended with "localhost"
       - URL prepended with "browser"
       - URL not prepended with the file explorer. We assume (as it is in the whole app, that this is a "browser" URL
    */
   /*
    if (!this.sessions[path]) {
      const session = this._createSession(content, this._getMode(path))
      this.sessions[path] = session
      this.readOnlySessions[path] = false
    } else if (this.sessions[path].getValue() !== content) {
      this.sessions[path].setValue(content)
    }
    this._switchSession(path)
    */
  }

  /**
   * Upsert and Open a session and set it as Read-only.
   * @param {string} path Path of the session to open.
   * @param {string} content Content of the document or update.
   */
  openReadOnly (path, content) {
    /*
    if (!this.sessions[path]) {
      const session = this._createSession(content, this._getMode(path))
      this.sessions[path] = session
      this.readOnlySessions[path] = true
    }
    this._switchSession(path)
    */
  }

  /**
   * Content of the current session
   * @return {String} content of the file referenced by @arg path
   */
  currentContent () {
    //return this.get(this.current())
  }

  /**
   * Content of the session targeted by @arg path
   * if @arg path is null, the content of the current session is returned
   * @param {string} path Path of the session to get.
   * @return {String} content of the file referenced by @arg path
   */
  get (path) {
    /*
    if (!path || this.currentSession === path) {
      return this.editor.getValue()
    } else if (this.sessions[path]) {
      return this.sessions[path].getValue()
    }
    */
  }

  /**
   * Path of the currently editing file
   * returns `undefined` if no session is being editer
   * @return {String} path of the current session
   */
  current () {
    /*
    if (this.editor.getSession() === this.emptySession) {
      return
    }
    return this.currentSession
    */
  }

  /**
   * The position of the cursor
   */
  getCursorPosition () {
    /*
    return this.editor.session.doc.positionToIndex(
      this.editor.getCursorPosition(),
      0
    )
    */
  }

  /**
   * Remove the current session from the list of sessions.
   */
  discardCurrentSession () {
    /*
    if (this.sessions[this.currentSession]) {
      delete this.sessions[this.currentSession]
      this.currentSession = null
    }
    */
  }

  /**
   * Remove a session based on its path.
   * @param {string} path
   */
  discard (path) {
    /*
    if (this.sessions[path]) delete this.sessions[path]
    if (this.currentSession === path) this.currentSession = null
    */
  }

  /**
   * Resize the editor, and sets whether or not line wrapping is enabled.
   * @param {boolean} useWrapMode Enable (or disable) wrap mode
   */
  resize (useWrapMode) {
    /*
    this.editor.resize()
    const session = this.editor.getSession()
    session.setUseWrapMode(useWrapMode)
    if (session.getUseWrapMode()) {
      const characterWidth = this.editor.renderer.characterWidth
      const contentWidth = this.editor.container.ownerDocument.getElementsByClassName(
        'ace_scroller'
      )[0].clientWidth

      if (contentWidth > 0) {
        session.setWrapLimit(parseInt(contentWidth / characterWidth, 10))
      }
    }
    */
  }

  /**
   * Adds a new marker to the given `Range`.
   * @param {*} lineColumnPos
   * @param {string} source Path of the session to add the mark on.
   * @param {string} cssClass css to apply to the mark.
   */
  addMarker (lineColumnPos, source, cssClass) {
    /*
    const currentRange = new Range(
      lineColumnPos.start.line,
      lineColumnPos.start.column,
      lineColumnPos.end.line,
      lineColumnPos.end.column
    )
    if (this.sessions[source]) {
      return this.sessions[source].addMarker(currentRange, cssClass)
    }
    return null
    */
  }

  /**
   * Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to).
   * @param {number} line The line to scroll to
   * @param {boolean} center If true
   * @param {boolean} animate If true animates scrolling
   * @param {Function} callback Function to be called when the animation has finished
   */
  scrollToLine (line, center, animate, callback) {
    //this.editor.scrollToLine(line, center, animate, callback)
  }

  /**
   * Remove a marker from the session
   * @param {string} markerId Id of the marker
   * @param {string} source Path of the session
   */
  removeMarker (markerId, source) {
    /*
    if (this.sessions[source]) {
      this.sessions[source].removeMarker(markerId)
    }
    */
  }

  /**
   * Clears all the annotations for the given @arg filePath and @arg plugin, if none is given, the current sesssion is used.
   * An annotation has the following shape:
      column: -1
      row: -1
      text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵"
      type: "warning"
   * @param {String} filePath
   * @param {String} plugin
   */
  clearAnnotationsByPlugin (filePath, plugin) {
    /*
    if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath)
    const session = this.sessions[filePath] || this.editor.getSession()
    const path = filePath || this.currentSession

    const currentAnnotations = this.sourceAnnotationsPerFile[path]
    if (!currentAnnotations) return

    const newAnnotations = []
    for (const annotation of currentAnnotations) {
      if (annotation.from !== plugin) newAnnotations.push(annotation)
    }
    this.sourceAnnotationsPerFile[path] = newAnnotations

    this._setAnnotations(session, path)
    */
  }

  keepAnnotationsFor (name) {
    /*
    if (!this.currentSession) return
    if (!this.sourceAnnotationsPerFile[this.currentSession]) return

    const annotations = this.sourceAnnotationsPerFile[this.currentSession]
    for (const annotation of annotations) {
      annotation.hide = annotation.from !== name
    }

    this._setAnnotations(this.editor.getSession(), this.currentSession)
    */
  }

  /**
   * Clears all the annotations for the given @arg filePath, the plugin name is retrieved from the context, if none is given, the current sesssion is used.
   * An annotation has the following shape:
      column: -1
      row: -1
      text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵"
      type: "warning"
   * @param {String} filePath
   * @param {String} plugin
   */
  clearAnnotations (filePath) {
    /*
    const { from } = this.currentRequest
    this.clearAnnotationsByPlugin(filePath, from)
    */
  }

  /**
   * Clears all the annotations and for all the sessions for the given @arg plugin
   * An annotation has the following shape:
      column: -1
      row: -1
      text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵"
      type: "warning"
   * @param {String} filePath
   */
  clearAllAnnotationsFor (plugin) {
    /*
    for (const session in this.sessions) {
      this.clearAnnotationsByPlugin(session, plugin)
    }
    */
  }

  /**
   * Add an annotation to the current session.
   * An annotation has the following shape:
      column: -1
      row: -1
      text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵"
      type: "warning"
   * @param {Object} annotation
   * @param {String} filePath
   */
  addAnnotation (annotation, filePath) {
    /*
    if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath)
    const session = this.sessions[filePath] || this.editor.getSession()
    const path = filePath || this.currentSession

    const { from } = this.currentRequest
    if (!this.sourceAnnotationsPerFile[path]) this.sourceAnnotationsPerFile[path] = []
    annotation.from = from
    this.sourceAnnotationsPerFile[path].push(annotation)

    this._setAnnotations(session, path)
    */
  }

  _setAnnotations (session, path) {
    //const annotations = this.sourceAnnotationsPerFile[path]
    //session.setAnnotations(annotations.filter((element) => !element.hide))
  }

  /**
   * Moves the cursor and focus to the specified line and column number
   * @param {number} line
   * @param {number} col
   */
  gotoLine (line, col) {
    //this.editor.focus()
    //this.editor.gotoLine(line + 1, col - 1, true)
  }
}


module.exports = Editor

3.5. 创建一个最简单的react-diagram项目

  • git clone https://github.com/MrWater98/Simple_ReactDiagram_Demo.git
  • npm install
  • 安装typescript,把typescript转换成javascript:sudo npm install -g typescript

3.6. 在remix-project项目中嵌入typescript

  • 令人意外的是,remix-project的javascript项目是可以直接嵌入typescript的,因为它添加了下面这几个库:
    在这里插入图片描述
  • 要注意的只是引入文件的时候即可
import MyApp from './app/editor/app.tsx';
  • 如果出现了:
ERROR in ./app/editor/RuleParser.tsx
Module build failed (from /Users/ziyue/ResaechLib/OxHainan/_backup/remix-project/node_modules/@nrwl/web/src/utils/web-babel-loader.js):
  • 则按照这篇来解决问题:

https://stackoverflow.com/questions/35517245/error-missing-class-properties-transform

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值