From 9d2a95e87abd8c84c17f5d3c41fbbd1470ef01bc Mon Sep 17 00:00:00 2001 From: CalebJohn Date: Sun, 17 May 2020 17:25:02 -0600 Subject: [PATCH 01/18] add codemirror --- ElectronClient/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/ElectronClient/package.json b/ElectronClient/package.json index 117b7cf9ae3..a88126ad63a 100644 --- a/ElectronClient/package.json +++ b/ElectronClient/package.json @@ -91,6 +91,7 @@ "base64-stream": "^1.0.0", "chokidar": "^3.0.0", "clean-html": "^1.5.0", + "codemirror": "^5.53.2", "color": "^3.1.2", "compare-versions": "^3.2.1", "countable": "^3.0.1", From 289bf23188db68a9548ae43651012c21f9b4fc4a Mon Sep 17 00:00:00 2001 From: CalebJohn Date: Thu, 21 May 2020 14:38:50 -0600 Subject: [PATCH 02/18] Replace AceEditorReact with codemirror, no integration yet --- .eslintignore | 1 + .gitignore | 1 + .../NoteBody/AceEditor/AceEditor.tsx | 83 ++++++++++--------- .../NoteBody/AceEditor/CodeMirror.tsx | 62 ++++++++++++++ ElectronClient/gui/style/theme/aritimDark.js | 2 +- ElectronClient/gui/style/theme/dark.js | 2 +- ElectronClient/gui/style/theme/light.js | 2 +- ElectronClient/gui/style/theme/nord.js | 2 +- .../gui/style/theme/solarizedDark.js | 2 +- .../gui/style/theme/solarizedLight.js | 2 +- ElectronClient/index.html | 8 +- ElectronClient/style.css | 11 ++- 12 files changed, 131 insertions(+), 47 deletions(-) create mode 100644 ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.tsx diff --git a/.eslintignore b/.eslintignore index 74a5e2875b1..84fdf8a512f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -64,6 +64,7 @@ Modules/TinyMCE/langs/ ElectronClient/gui/MultiNoteActions.js ElectronClient/gui/NoteContentPropertiesDialog.js ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js +ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.js ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js diff --git a/.gitignore b/.gitignore index aa028226110..1cc11e8b481 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ Tools/commit_hook.txt ElectronClient/gui/MultiNoteActions.js ElectronClient/gui/NoteContentPropertiesDialog.js ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js +ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.js ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js diff --git a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx index 1df8a495267..e15a8177bd9 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx @@ -5,14 +5,16 @@ import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHand import { EditorCommand, NoteBodyEditorProps } from '../../utils/types'; import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling'; import { ScrollOptions, ScrollOptionTypes } from '../../utils/types'; -import { textOffsetToCursorPosition, useScrollHandler, useRootWidth, usePrevious, lineLeftSpaces, selectionRangeCurrentLine, selectionRangePreviousLine, currentTextOffset, textOffsetSelection, selectedText, useSelectionRange } from './utils'; +import { textOffsetToCursorPosition, useScrollHandler, usePrevious, lineLeftSpaces, selectionRangeCurrentLine, selectionRangePreviousLine, currentTextOffset, textOffsetSelection, selectedText, useSelectionRange } from './utils'; import useListIdent from './utils/useListIdent'; import Toolbar from './Toolbar'; import styles_ from './styles'; import { RenderedBody, defaultRenderedBody } from './utils/types'; +import CodeMirrorEditor from './CodeMirror'; -const AceEditorReact = require('react-ace').default; +// @ts-ignore const { bridge } = require('electron').remote.require('./bridge'); +// @ts-ignore const Note = require('lib/models/Note.js'); const { clipboard } = require('electron'); const Setting = require('lib/models/Setting.js'); @@ -77,7 +79,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { const styles = styles_(props); const [renderedBody, setRenderedBody] = useState(defaultRenderedBody()); // Viewer content - const [editor, setEditor] = useState(null); + const [editor] = useState(null); const [lastKeys, setLastKeys] = useState([]); const [webviewReady, setWebviewReady] = useState(false); @@ -100,9 +102,8 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { const selectionRangeRef = useRef(null); selectionRangeRef.current = useSelectionRange(editor); - const rootWidth = useRootWidth({ rootRef }); - - const { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll } = useScrollHandler(editor, webviewRef, props.onScroll); + // @ts-ignore + const { resetScroll, setEditorPercentScroll, setViewerPercentScroll } = useScrollHandler(editor, webviewRef, props.onScroll); useListIdent({ editor, selectionRangeRef }); @@ -421,10 +422,6 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { menu.popup(bridge().window()); }, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste]); - function aceEditor_load(editor: any) { - setEditor(editor); - } - useEffect(() => { if (!editor) return () => {}; @@ -575,44 +572,52 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { function renderEditor() { // Need to hard-code the editor width, otherwise various bugs pops up - let width = 0; - if (props.visiblePanes.includes('editor')) { - width = !props.visiblePanes.includes('viewer') ? rootWidth : Math.floor(rootWidth / 2); - } + // @ts-ignore + // let width = 0; + // if (props.visiblePanes.includes('editor')) { + // width = !props.visiblePanes.includes('viewer') ? rootWidth : Math.floor(rootWidth / 2); + // } return (
-
+ // ); } diff --git a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.tsx b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.tsx new file mode 100644 index 00000000000..9e2e224071e --- /dev/null +++ b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +// @ts-ignore +import { useImperativeHandle, useState, useCallback, forwardRef } from 'react'; + +const CodeMirror = require('codemirror'); +import 'codemirror/mode/gfm/gfm'; +import 'codemirror/mode/xml/xml'; +import 'codemirror/mode/python/python'; + +// TODO: This needs to be moved to ../../utils/types +export interface EditorProps { + value: string, + mode: string, + style: any, + theme: any, + onChange: any, +} + +function CodeMirrorEditor(props: EditorProps, ref: any) { +// @ts-ignore + const [editor, setEditor] = useState(null); + + useImperativeHandle(ref, () => ({ + focus: () => { + editor.focus(); + }, + })); + + // TODO: use the change deltas rather than getValue (should be faster) + const editor_change = useCallback((cm, change) => { + if (props.onChange && change.origin !== 'setValue') { + props.onChange(cm.getValue()); + } + }, []); + + const divRef = useCallback(node => { + if (node !== null) { + const cmOptions = { + value: props.value, + theme: props.theme, + mode: props.mode, + inputStyle: 'contenteditable', + lineWrapping: true, + }; + const cm = CodeMirror(node, cmOptions); + setEditor(cm); + cm.on('change', editor_change); + console.log('New ref'); + } + }, []); + + // useEffect(() => { + // if (editor !== null) { + // editor.setOption('theme', props.theme); + // } + // }, [editor]); + + // @ts-ignore + return
; +} + +export default forwardRef(CodeMirrorEditor); diff --git a/ElectronClient/gui/style/theme/aritimDark.js b/ElectronClient/gui/style/theme/aritimDark.js index eecaa85cfdb..168940fcd12 100644 --- a/ElectronClient/gui/style/theme/aritimDark.js +++ b/ElectronClient/gui/style/theme/aritimDark.js @@ -34,7 +34,7 @@ const aritimStyle = { htmlCodeBorderColor: '#141a21', // Single line code border, and tables htmlCodeColor: '#005b47', // Single line code text - editorTheme: 'chaos', + editorTheme: 'monokai', codeThemeCss: 'atom-one-dark-reasonable.css', highlightedColor: '#d3dae3', diff --git a/ElectronClient/gui/style/theme/dark.js b/ElectronClient/gui/style/theme/dark.js index ecc2567bc4a..0aec47886df 100644 --- a/ElectronClient/gui/style/theme/dark.js +++ b/ElectronClient/gui/style/theme/dark.js @@ -33,7 +33,7 @@ const darkStyle = { htmlCodeBackgroundColor: 'rgb(47, 48, 49)', htmlCodeBorderColor: 'rgb(70, 70, 70)', - editorTheme: 'twilight', + editorTheme: 'material-darker', codeThemeCss: 'atom-one-dark-reasonable.css', highlightedColor: '#0066C7', diff --git a/ElectronClient/gui/style/theme/light.js b/ElectronClient/gui/style/theme/light.js index 29c43c0fcf6..550a380d7ba 100644 --- a/ElectronClient/gui/style/theme/light.js +++ b/ElectronClient/gui/style/theme/light.js @@ -32,7 +32,7 @@ const lightStyle = { htmlCodeBorderColor: 'rgb(220, 220, 220)', htmlCodeColor: 'rgb(0,0,0)', - editorTheme: 'chrome', + editorTheme: 'default', codeThemeCss: 'atom-one-light.css', }; diff --git a/ElectronClient/gui/style/theme/nord.js b/ElectronClient/gui/style/theme/nord.js index 2189bfb13f0..06a7f1e70c7 100644 --- a/ElectronClient/gui/style/theme/nord.js +++ b/ElectronClient/gui/style/theme/nord.js @@ -79,7 +79,7 @@ const nordStyle = { htmlCodeBorderColor: nord[2], htmlCodeColor: nord[13], - editorTheme: 'terminal', + editorTheme: 'nord', codeThemeCss: 'atom-one-dark-reasonable.css', }; diff --git a/ElectronClient/gui/style/theme/solarizedDark.js b/ElectronClient/gui/style/theme/solarizedDark.js index afe7106cf8a..a15f66365ac 100644 --- a/ElectronClient/gui/style/theme/solarizedDark.js +++ b/ElectronClient/gui/style/theme/solarizedDark.js @@ -33,7 +33,7 @@ const solarizedDarkStyle = { htmlCodeBorderColor: '#696969', htmlCodeColor: '#fdf6e3', - editorTheme: 'twilight', + editorTheme: 'solarized dark', codeThemeCss: 'atom-one-dark-reasonable.css', }; diff --git a/ElectronClient/gui/style/theme/solarizedLight.js b/ElectronClient/gui/style/theme/solarizedLight.js index 042ad3b052a..8cd33b9a394 100644 --- a/ElectronClient/gui/style/theme/solarizedLight.js +++ b/ElectronClient/gui/style/theme/solarizedLight.js @@ -31,7 +31,7 @@ const solarizedLightStyle = { htmlCodeBorderColor: '#eee8d5', htmlCodeColor: '#002b36', - editorTheme: 'tomorrow', + editorTheme: 'solarized light', codeThemeCss: 'atom-one-light.css', }; diff --git a/ElectronClient/index.html b/ElectronClient/index.html index 5a867092a20..db51fdb2dcb 100644 --- a/ElectronClient/index.html +++ b/ElectronClient/index.html @@ -9,9 +9,15 @@ --> Joplin + + + + + + - \ No newline at end of file + diff --git a/ElectronClient/style.css b/ElectronClient/style.css index c409bcc4419..7b06bbf4efe 100644 --- a/ElectronClient/style.css +++ b/ElectronClient/style.css @@ -169,4 +169,13 @@ a { @keyframes rotate { from {transform: rotate(0deg);} to {transform: rotate(360deg);} -} \ No newline at end of file +} + +/* These must be important to prevent the codemirror defaults from taking over*/ +.CodeMirror { + height: 100% !important; + width: 100% !important; + color: inherit !important; + background-color: inherit !important; + position: absolute !important; +} From 4a4cd62942017072ca34a2180669453aa8367946 Mon Sep 17 00:00:00 2001 From: CalebJohn Date: Fri, 22 May 2020 16:13:12 -0600 Subject: [PATCH 03/18] Fix up searching/commenting and list indent --- ElectronClient/app.js | 2 +- .../NoteBody/AceEditor/AceEditor.tsx | 46 +----------- .../NoteBody/AceEditor/CodeMirror.tsx | 55 +++++++++++--- .../NoteBody/AceEditor/utils/useListIdent.ts | 73 +++++++++++-------- ElectronClient/index.html | 13 ++-- ElectronClient/style.css | 1 + 6 files changed, 99 insertions(+), 91 deletions(-) diff --git a/ElectronClient/app.js b/ElectronClient/app.js index d9a1c176ea8..c3b13b0c586 100644 --- a/ElectronClient/app.js +++ b/ElectronClient/app.js @@ -1308,7 +1308,7 @@ class Application extends BaseApplication { // The '*' and '!important' parts are necessary to make sure Russian text is displayed properly // https://github.com/laurent22/joplin/issues/155 - const css = `.ace_editor * { font-family: ${fontFamilies.join(', ')} !important; }`; + const css = `.CodeMirror * { font-family: ${fontFamilies.join(', ')} !important; }`; const styleTag = document.createElement('style'); styleTag.type = 'text/css'; styleTag.appendChild(document.createTextNode(css)); diff --git a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx index cd12428d361..a906ab517a5 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx @@ -6,7 +6,6 @@ import { EditorCommand, NoteBodyEditorProps } from '../../utils/types'; import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling'; import { ScrollOptions, ScrollOptionTypes } from '../../utils/types'; import { textOffsetToCursorPosition, useScrollHandler, usePrevious, lineLeftSpaces, selectionRange, selectionRangeCurrentLine, selectionRangePreviousLine, currentTextOffset, textOffsetSelection, selectedText } from './utils'; -import useListIdent from './utils/useListIdent'; import Toolbar from './Toolbar'; import styles_ from './styles'; import { RenderedBody, defaultRenderedBody } from './utils/types'; @@ -27,20 +26,6 @@ const { _ } = require('lib/locale'); const { reg } = require('lib/registry.js'); const dialogs = require('../../../dialogs'); -require('brace/mode/markdown'); -// https://ace.c9.io/build/kitchen-sink.html -// https://highlightjs.org/static/demo/ -require('brace/theme/chrome'); -require('brace/theme/solarized_light'); -require('brace/theme/solarized_dark'); -require('brace/theme/twilight'); -require('brace/theme/dracula'); -require('brace/theme/chaos'); -require('brace/theme/tomorrow'); -require('brace/keybinding/vim'); -require('brace/keybinding/emacs'); -require('brace/theme/terminal'); - // TODO: Could not get below code to work // @ts-ignore Ace global variable @@ -98,8 +83,6 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { const { resetScroll, setEditorPercentScroll, setViewerPercentScroll } = useScrollHandler(editor, webviewRef, props.onScroll); - useListIdent({ editor }); - const aceEditor_change = useCallback((newBody: string) => { props_onChangeRef.current({ changeId: null, content: newBody }); }, []); @@ -579,37 +562,14 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'xml' : 'gfm'} theme={styles.editor.editorTheme} style={styles.editor} + readOnly={props.visiblePanes.indexOf('editor') < 0} + autoMatchBraces={Setting.value('editor.autoMatchingBraces')} onChange={aceEditor_change} + keyMap={props.keyboardMode} />
// ); diff --git a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.tsx b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.tsx index 9e2e224071e..fc63f68edfa 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/CodeMirror.tsx @@ -1,25 +1,44 @@ import * as React from 'react'; -// @ts-ignore -import { useImperativeHandle, useState, useCallback, forwardRef } from 'react'; +import { useEffect, useImperativeHandle, useState, useCallback, forwardRef } from 'react'; const CodeMirror = require('codemirror'); +import 'codemirror/addon/comment/comment'; +import 'codemirror/addon/dialog/dialog'; +import 'codemirror/addon/edit/closebrackets'; +import 'codemirror/addon/edit/continuelist'; +import 'codemirror/addon/scroll/scrollpastend'; +import 'codemirror/addon/search/searchcursor'; +import 'codemirror/addon/search/search'; + +import useListIdent from './utils/useListIdent'; + +import 'codemirror/keymap/emacs'; +import 'codemirror/keymap/vim'; + import 'codemirror/mode/gfm/gfm'; import 'codemirror/mode/xml/xml'; +// Modes for syntax highlighting inside of code blocks import 'codemirror/mode/python/python'; -// TODO: This needs to be moved to ../../utils/types export interface EditorProps { value: string, mode: string, style: any, theme: any, + readOnly: boolean, + autoMatchBraces: boolean, + keyMap: string, onChange: any, } function CodeMirrorEditor(props: EditorProps, ref: any) { -// @ts-ignore const [editor, setEditor] = useState(null); + // Codemirror plugins add new commands to codemirror (or change it's behavior) + // This command adds the smartListIndent function which will be bound to tab + useListIdent(CodeMirror); + CodeMirror.keyMap.default['Ctrl-G'] = null; + useImperativeHandle(ref, () => ({ focus: () => { editor.focus(); @@ -39,23 +58,35 @@ function CodeMirrorEditor(props: EditorProps, ref: any) { value: props.value, theme: props.theme, mode: props.mode, - inputStyle: 'contenteditable', + readOnly: props.readOnly, + autoCloseBrackets: props.autoMatchBraces, + inputStyle: 'contenteditable', // Has better support for screen readers lineWrapping: true, + lineNumbers: false, + scrollPastEnd: true, + indentWithTabs: true, + indentUnit: 4, + keyMap: props.keyMap ? props.keyMap : 'default', + extraKeys: { 'Enter': 'newlineAndIndentContinueMarkdownList', + 'Ctrl-/': 'toggleComment', + 'Tab': 'smartListIndent', + 'Shift-Tab': 'smartListUnindent' }, }; const cm = CodeMirror(node, cmOptions); setEditor(cm); cm.on('change', editor_change); - console.log('New ref'); + console.log('!!!!!!!!!!!!New ref!!!!!!!!!!!!!!!'); } }, []); - // useEffect(() => { - // if (editor !== null) { - // editor.setOption('theme', props.theme); - // } - // }, [editor]); + useEffect(() => { + if (editor) { + editor.setValue(props.value); + editor.clearHistory(); + console.log('!!!!!!!!!!UPDATE PROPS!!!!!!!!!!!!'); + } + }, [props.value]); - // @ts-ignore return
; } diff --git a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.ts b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.ts index 105e4ce1533..f633a2aa438 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.ts +++ b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.ts @@ -1,40 +1,55 @@ -import { useEffect } from 'react'; -import { selectionRange } from './index'; +// Markdown list indentation. +// If the current line starts with `markup.list` token, +// hitting `Tab` key indents the line instead of inserting tab at cursor. +export default function useListIdent(CodeMirror: any) { -interface HookDependencies { - editor: any, -} + function isSelection(anchor: any, head: any) { + return anchor.line !== head.line || anchor.ch !== head.ch; + } -export default function useListIdent(dependencies:HookDependencies) { - const { editor } = dependencies; + CodeMirror.commands.smartListIndent = function(cm: any) { + if (cm.getOption('disableInput')) return CodeMirror.Pass; - useEffect(() => { - if (!editor) return; + const ranges = cm.listSelections(); + for (let i = 0; i < ranges.length; i++) { + const { anchor, head } = ranges[i]; - // Markdown list indentation. (https://github.com/laurent22/joplin/pull/2713) - // If the current line starts with `markup.list` token, - // hitting `Tab` key indents the line instead of inserting tab at cursor. - const originalEditorIndent = editor.indent; + const tokens = cm.getLineTokens(anchor.line); + console.log(tokens); - editor.indent = function() { - const range = selectionRange(editor); - if (range.isEmpty()) { - const row = range.start.row; - const tokens = this.session.getTokens(row); + // This is an actual selection and we should indent + if (isSelection(anchor, head) || tokens.length == 0 || !tokens[0].state.base.list) { + cm.execCommand('defaultTab'); + } else { + let token: any = tokens[0]; - if (tokens.length > 0 && tokens[0].type == 'markup.list') { - if (tokens[0].value.search(/\d+\./) != -1) { - // Resets numbered list to 1. - this.session.replace({ start: { row, column: 0 }, end: { row, column: tokens[0].value.length } }, - tokens[0].value.replace(/\d+\./, '1.')); - } + if (tokens[0].string.match(/\s/) && tokens.length > 1) { token = tokens[1]; } - this.session.indentRows(row, row, '\t'); - return; + if (token.string.match(/\d+/)) { + // Resets numbered list to 1. + cm.replaceRange('1', { line: anchor.line, ch: token.start }, { line: anchor.line, ch: token.end }); } + + cm.indentLine(anchor.line, 'add'); } + } + }; + + CodeMirror.commands.smartListUnindent = function(cm: any) { + if (cm.getOption('disableInput')) return CodeMirror.Pass; - if (originalEditorIndent) originalEditorIndent.call(this); - }; - }, [editor]); + const ranges = cm.listSelections(); + for (let i = 0; i < ranges.length; i++) { + const { anchor, head } = ranges[i]; + + const tokens = cm.getLineTokens(anchor.line); + + // This is an actual selection and we should indent + if (isSelection(anchor, head) || tokens.length == 0 || !tokens[0].state.base.list) { + cm.execCommand('indentAuto'); + } else { + cm.indentLine(anchor.line, 'subtract'); + } + } + }; } diff --git a/ElectronClient/index.html b/ElectronClient/index.html index db51fdb2dcb..34953114703 100644 --- a/ElectronClient/index.html +++ b/ElectronClient/index.html @@ -9,15 +9,16 @@ --> Joplin - - - - - - + + + + + + +