src/components/common/controls/Editor.js

Summary

Maintainability
A
0 mins
Test Coverage
import React from 'react';
import PropTypes from 'prop-types';
import styled, { css, withTheme } from 'styled-components';
import MonacoEditor from 'react-monaco-editor';
import { registerRulesForLanguage } from 'monaco-ace-tokenizer';
import { toPairs } from 'lodash/fp';

import AdaHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/ada';
import ClojureHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/clojure';
import CobolHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/cobol';
import DHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/d';
import ElixirHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/elixir';
import ErlangHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/erlang';
import FortranHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/fortran';
import GroovyHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/groovy';
import HaskellHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/haskell';
import JuliaHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/julia';
import KotlinHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/kotlin';
import OcamlHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/ocaml';
import PascalHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/pascal';
import PerlHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/perl';
import RacketHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/racket';
import SbclHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/sbcl';
import ScalaHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/scala';
import TclHighlightRules from 'monaco-ace-tokenizer/lib/ace/definitions/tcl';

import themeList from 'monaco-themes/themes/themelist';

import { getSetting, getSession, setSession } from 'utils/settings';
import {
  isAsciiDoc,
  isCSV,
  isGeoJson,
  isMarkDown,
  isPDF,
  isImage,
  isTSV,
  isHTML,
  isLaTex
} from 'utils/files';

import { syntaxMap } from 'constants/editor';

import 'highlight.js/styles/default.css';

import Loading from 'components/common/Loading';
import Anchor from 'components/common/Anchor';

import Markdown from 'components/common/editor/Markdown';
import Asciidoc from 'components/common/editor/Asciidoc';
import Csv from 'components/common/editor/Csv';
import GeoJson from 'components/common/editor/GeoJson';
import Pdf from 'components/common/editor/Pdf';
import Html from 'components/common/editor/Html';
import LaTex from 'components/common/editor/LaTex';

const RenderedComponent = css`
  padding: 20px 30px;
  ${(props) => props.width && `width: calc(${props.width} - 60px);`}
`;

const MarkdownComponent = styled(Markdown)`
  ${RenderedComponent}
`;

const AsciidocComponent = styled(Asciidoc)`
  ${RenderedComponent}
`;

const EditorWrapper = styled.span`
  display: inline-flex;
  flex-direction: row;
  justify-content: space-evenly;
  width: 100%;
`;

const LoadingIndicator = styled.div`
  padding: 20px;
  color: ${(props) => props.theme.baseAppColor};
`;

const Image = styled.div`
  text-align: center;

  img {
    max-width: 100%;
  }
`;

const BigFile = styled.div`
  margin: 20px;
`;

const StyledAnchor = styled(Anchor)`
  text-decoration: underline;
`;

const editorOptions = (options) => ({
  selectOnLineNumbers: Boolean(getSetting('selectOnLineNumbers', false)),
  lineNumbers: getSetting('lineNumbers', true),
  codeLens: getSetting('codeLens', false),
  cursorBlinking: getSetting('cursorBlinking', 'blink'),
  formatOnPaste: Boolean(getSetting('formatOnPaste', false)),
  formatOnType: Boolean(getSetting('settings-editor-formatOnType', false)),
  fontFamily: getSetting('fontFamily', 'monospace'),
  lineHeight: getSetting('lineHeight', 21),
  fontLigatures: getSetting('fontLigatures', false),
  fontSize: getSetting('fontSize', 12),
  roundedSelection: false,
  scrollBeyondLastLine: false,
  wordWrap: getSetting('settings-editor-wordWrap', 'bounded'),
  wordWrapColumn: getSetting('settings-editor-wordWrapColumn', 80),
  minimap: {
    enabled: Boolean(getSetting('minimap', false))
  },
  automaticLayout: true,
  ...options
});

export class Editor extends React.Component {
  editorDidMount = (editor, monaco) => {
    setSession('monaco-extra-langs-registred', true);

    toPairs(themeList).map((theme) =>
      import(`monaco-themes/themes/${theme[1]}`).then((data) =>
        monaco.editor.defineTheme(theme[0], data)
      )
    );

    monaco.editor.setTheme(getSetting('editorTheme', 'vs'));

    monaco.languages.register({ id: 'ada' });
    monaco.languages.register({ id: 'clojure' });
    monaco.languages.register({ id: 'cobol' });
    monaco.languages.register({ id: 'd' });
    monaco.languages.register({ id: 'elixir' });
    monaco.languages.register({ id: 'erlang' });
    monaco.languages.register({ id: 'fortran' });
    monaco.languages.register({ id: 'groovy' });
    monaco.languages.register({ id: 'haskell' });
    monaco.languages.register({ id: 'julia' });
    monaco.languages.register({ id: 'kotlin' });
    monaco.languages.register({ id: 'ocaml' });
    monaco.languages.register({ id: 'pascal' });
    monaco.languages.register({ id: 'perl' });
    monaco.languages.register({ id: 'racket' });
    monaco.languages.register({ id: 'sbcl' });
    monaco.languages.register({ id: 'scala' });
    monaco.languages.register({ id: 'tcl' });

    registerRulesForLanguage('ada', new AdaHighlightRules());
    registerRulesForLanguage('clojure', new ClojureHighlightRules());
    registerRulesForLanguage('cobol', new CobolHighlightRules());
    registerRulesForLanguage('d', new DHighlightRules());
    registerRulesForLanguage('elixir', new ElixirHighlightRules());
    registerRulesForLanguage('erlang', new ErlangHighlightRules());
    registerRulesForLanguage('fortran', new FortranHighlightRules());
    registerRulesForLanguage('groovy', new GroovyHighlightRules());
    registerRulesForLanguage('haskell', new HaskellHighlightRules());
    registerRulesForLanguage('julia', new JuliaHighlightRules());
    registerRulesForLanguage('kotlin', new KotlinHighlightRules());
    registerRulesForLanguage('ocaml', new OcamlHighlightRules());
    registerRulesForLanguage('pascal', new PascalHighlightRules());
    registerRulesForLanguage('perl', new PerlHighlightRules());
    registerRulesForLanguage('racket', new RacketHighlightRules());
    registerRulesForLanguage('sbcl', new SbclHighlightRules());
    registerRulesForLanguage('scala', new ScalaHighlightRules());
    registerRulesForLanguage('tcl', new TclHighlightRules());
  };

  renderMonacoEdito = (width, height, language = this.props.language) => (
    <MonacoEditor
      width={ width }
      height={
        getSetting('editor-fit-to-content', false) && !this.props.isNew && !this.props.edit
          ? false
          : height
      }
      className={ this.props.className }
      language={ language || syntaxMap[this.props.file.language] || 'text' }
      theme={ getSetting('editorTheme', 'vs') }
      name={ this.props.id }
      value={ this.props.file.content }
      options={ editorOptions({ readOnly: !this.props.edit && !this.props.isNew }) }
      // eslint-disable-next-line no-extra-boolean-cast
      editorDidMount={ (editor, monaco) => {
        const fitContentHeight = editor.getModel().getLineCount() * getSetting('lineHeight', 21);

        // eslint-disable-next-line no-param-reassign
        editor.getDomNode().style.height = `${
          getSetting('editor-fit-to-content', false) && !this.props.isNew && !this.props.edit
            ? fitContentHeight
            : height
        }px`;
        editor.layout();

        return getSession('monaco-extra-langs-registred')
          ? null
          : this.editorDidMount(editor, monaco);
      } }
      onChange={ this.props.onChange }/>
  );

  renderEditor = () => {
    const { edit, file, filesCount, isNew, theme } = this.props;

    if (file.collapsed) {
      return null;
    }

    if (!isNew && !file.content && !edit) {
      return (
        <LoadingIndicator>
          <Loading color={ theme.baseAppColor } text=""/>
        </LoadingIndicator>
      );
    }

    if (
      file.content &&
      Boolean(getSetting('settings-editor-preview-image', false)) &&
      isImage(file) &&
      !file.collapsed
    ) {
      return (
        <Image>
          <img
            id="img"
            src={ file.raw_url }
            title={ `File type: ${file.type}` }
            alt={ `File type: ${file.type}` }/>
        </Image>
      );
    }

    if (!file.collapsed && file.truncated) {
      return (
        <BigFile>
          This file has been truncated, it contains {file.size} characters.
          <br/>
          You can view the <StyledAnchor href={ file.raw_url }>full file</StyledAnchor> on web.
        </BigFile>
      );
    }

    if (Boolean(getSetting('settings-editor-preview-csv', false)) && (isCSV(file) || isTSV(file))) {
      if (!edit && !isNew) {
        return <Csv text={ file.content }/>;
      }
    }

    if (Boolean(getSetting('settings-editor-preview-html', false)) && isHTML(file)) {
      if (!edit && !isNew) {
        return <Html file={ file }/>;
      }
    }

    if (Boolean(getSetting('settings-editor-preview-latex', false)) && isLaTex(file)) {
      if (!edit && !isNew) {
        return <LaTex text={ file.content }/>;
      }
    }

    if (
      Boolean(getSetting('settings-editor-preview-pdf', false)) &&
      isPDF(file) &&
      navigator.onLine
    ) {
      if (!isNew) {
        return <Pdf file={ file }/>;
      }
    }

    if (
      Boolean(getSetting('settings-editor-preview-geojson', false)) &&
      isGeoJson(file) &&
      navigator.onLine
    ) {
      if (!edit && !isNew && !file.collapsed) {
        return <GeoJson file={ file }/>;
      }
    }

    if (Boolean(getSetting('settings-editor-preview-asciidoc', false)) && isAsciiDoc(file)) {
      if (!edit && !isNew && !file.collapsed) {
        return <AsciidocComponent text={ file.content }/>;
      }

      const calculatedHeight = filesCount === 1 ? window.outerHeight - 220 : 300;

      return (
        <EditorWrapper>
          {this.renderMonacoEdito('50%', calculatedHeight, 'AsciiDoc')}
          <AsciidocComponent width="50%" text={ file.content }/>
        </EditorWrapper>
      );
    }

    if (Boolean(getSetting('settings-editor-preview-markdown', false)) && isMarkDown(file)) {
      if (!edit && !isNew && !file.collapsed) {
        return <MarkdownComponent text={ file.content }/>;
      }

      const calculatedHeight = filesCount === 1 ? window.outerHeight - 220 : 300;

      return (
        <EditorWrapper>
          {this.renderMonacoEdito('50%', calculatedHeight, 'Markdown')}
          <MarkdownComponent width="50%" text={ file.content }/>
        </EditorWrapper>
      );
    }

    const calculatedHeight = filesCount === 1 ? window.outerHeight - 220 : 'calc(100vh - 235px)';

    return (
      <span style={ { display: file.collapsed ? 'none' : 'inherit' } }>
        {this.renderMonacoEdito('100%', calculatedHeight)}
      </span>
    );
  };

  render() {
    return this.renderEditor();
  }
}

Editor.propTypes = {
  file: PropTypes.object,
  theme: PropTypes.object,
  onChange: PropTypes.func,
  language: PropTypes.string,
  id: PropTypes.string,
  className: PropTypes.string,
  edit: PropTypes.bool,
  filesCount: PropTypes.number,
  isNew: PropTypes.bool
};

export default withTheme(Editor);