diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index f4ce77b9..3e416ab2 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -1,121 +1,110 @@ +import 'codemirror-mode-elixir' + +import {Alignment, options, TableEditor} from '@susisu/mte-kernel' +import consts from 'browser/lib/consts' +import convertModeName from 'browser/lib/convertModeName' +import TextEditorInterface from 'browser/lib/TextEditorInterface' +import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' +import eventEmitter from 'browser/main/lib/eventEmitter' +import CodeMirror from 'codemirror' +import crypto from 'crypto' +import fs from 'fs' +import iconv from 'iconv-lite' +import _ from 'lodash' import PropTypes from 'prop-types' import React from 'react' -import _ from 'lodash' -import CodeMirror from 'codemirror' -import 'codemirror-mode-elixir' -import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' -import convertModeName from 'browser/lib/convertModeName' -import { - options, - TableEditor, - Alignment -} from '@susisu/mte-kernel' -import TextEditorInterface from 'browser/lib/TextEditorInterface' -import eventEmitter from 'browser/main/lib/eventEmitter' -import iconv from 'iconv-lite' -import crypto from 'crypto' -import consts from 'browser/lib/consts' -import fs from 'fs' -const { - ipcRenderer -} = require('electron') + +const {ipcRenderer} = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' import TurndownService from 'turndown' -import { - gfm -} from 'turndown-plugin-gfm' +import{gfm} from 'turndown-plugin-gfm' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' const buildCMRulers = (rulers, enableRulers) => - (enableRulers ? rulers.map(ruler => ({ - column: ruler - })) : []) + (enableRulers ? rulers.map(ruler => ({column: ruler})) : []) export default class CodeEditor extends React.Component { constructor(props) { super(props) - this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, { - leading: false, - trailing: true - }) - this.changeHandler = e => this.handleChange(e) - this.focusHandler = () => { - ipcRenderer.send('editor:focused', true) - } - this.blurHandler = (editor, e) => { - ipcRenderer.send('editor:focused', false) - if (e == null) return null - let el = e.relatedTarget - while (el != null) { - if (el === this.refs.root) { - return - } - el = el.parentNode - } - this.props.onBlur != null && this.props.onBlur(e) + this.scrollHandler = + _.debounce( + this.handleScroll.bind(this), 100, + {leading: false, trailing: true}) this.changeHandler = e => + this.handleChange(e) this.focusHandler = + () => { + ipcRenderer.send('editor:focused', true) + } this.blurHandler = + (editor, e) => { + ipcRenderer.send('editor:focused', false) + if (e == null) return null + let el = e.relatedTarget + while (el != null) { + if (el === this.refs.root) { + return + } + el = el.parentNode + } + this.props.onBlur != null && this.props.onBlur(e) - const { - storageKey, - noteKey - } = this.props - attachmentManagement.deleteAttachmentsNotPresentInNote( - this.editor.getValue(), - storageKey, - noteKey - ) - } - this.pasteHandler = (editor, e) => this.handlePaste(editor, e) - this.loadStyleHandler = e => { - this.editor.refresh() - } - this.searchHandler = (e, msg) => this.handleSearch(msg) - this.searchState = null - this.scrollToLineHandeler = this.scrollToLine.bind(this) + const {storageKey, noteKey} = this.props + attachmentManagement.deleteAttachmentsNotPresentInNote( + this.editor.getValue(), storageKey, noteKey) + } this.pasteHandler = (editor, e) => + this.handlePaste(editor, e) this.loadStyleHandler = + e => { + this.editor.refresh() + } this.searchHandler = (e, msg) => + this.handleSearch(msg) this.searchState = + null this.scrollToLineHandeler = + this.scrollToLine + .bind(this) - this.formatTable = () => this.handleFormatTable() - this.editorActivityHandler = () => this.handleEditorActivity() + this.formatTable = () => + this.handleFormatTable() this + .editorActivityHandler = () => + this.handleEditorActivity() } handleSearch(msg) { const cm = this.editor const component = this - if (component.searchState) cm.removeOverlay(component.searchState) + if (component.searchState) + cm.removeOverlay(component.searchState) if (msg.length < 3) return - cm.operation(function () { - component.searchState = makeOverlay(msg, 'searching') - cm.addOverlay(component.searchState) + cm.operation(function() { + component.searchState = makeOverlay(msg, 'searching') + cm.addOverlay(component.searchState) - function makeOverlay(query, style) { - query = new RegExp( - query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), - 'gi' - ) - return { - token: function (stream) { - query.lastIndex = stream.pos - var match = query.exec(stream.string) - if (match && match.index === stream.pos) { - stream.pos += match[0].length || 1 - return style - } else if (match) { - stream.pos = match.index - } else { - stream.skipToEnd() + function makeOverlay(query, style) { + query = new RegExp( + query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), + 'gi') + return { + token: function(stream) { + query.lastIndex = stream.pos + var match = query.exec(stream.string) + if (match && match.index === stream.pos) { + stream.pos += match[0].length || 1 + return style + } + else if (match) { + stream.pos = match.index + } + else { + stream.skipToEnd() + } + } } } - } - } - }) + }) } handleFormatTable() { - this.tableEditor.formatAll(options({ - textWidthOptions: {} - })) + this.tableEditor.formatAll(options({textWidthOptions: {}})) } handleEditorActivity() { @@ -128,23 +117,23 @@ export default class CodeEditor extends React.Component { const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) if (active) { if (this.extraKeysMode !== 'editor') { - this.extraKeysMode = 'editor' - this.editor.setOption('extraKeys', this.editorKeyMap) + this.extraKeysMode = + 'editor' this.editor.setOption('extraKeys', this.editorKeyMap) } - } else { + } + else { if (this.extraKeysMode !== 'default') { - this.extraKeysMode = 'default' - this.editor.setOption('extraKeys', this.defaultKeyMap) - this.tableEditor.resetSmartCursor() + this.extraKeysMode = + 'default' this.editor + .setOption( + 'extraKeys', + this.defaultKeyMap) this.tableEditor.resetSmartCursor() } } } componentDidMount() { - const { - rulers, - enableRulers - } = this.props + const {rulers, enableRulers} = this.props const expandSnippet = this.expandSnippet.bind(this) eventEmitter.on('line:jump', this.scrollToLineHandeler) @@ -152,109 +141,99 @@ export default class CodeEditor extends React.Component { id: crypto.randomBytes(16).toString('hex'), name: 'Dummy text', prefix: ['lorem', 'ipsum'], - content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' - }] - if (!fs.existsSync(consts.SNIPPET_FILE)) { + content: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + }] if (!fs.existsSync(consts.SNIPPET_FILE)) { fs.writeFileSync( - consts.SNIPPET_FILE, - JSON.stringify(defaultSnippet, null, 4), - 'utf8' - ) + consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8') } - this.defaultKeyMap = CodeMirror.normalizeKeyMap({ - Tab: function (cm) { - const cursor = cm.getCursor() - const line = cm.getLine(cursor.line) - const cursorPosition = cursor.ch - const charBeforeCursor = line.substr(cursorPosition - 1, 1) - if (cm.somethingSelected()) cm.indentSelection('add') - else { - const tabs = cm.getOption('indentWithTabs') - if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) { - cm.execCommand('goLineStart') - if (tabs) { - cm.execCommand('insertTab') - } else { - cm.execCommand('insertSoftTab') - } - cm.execCommand('goLineEnd') - } else if ( - !charBeforeCursor.match(/\t|\s|\r|\n/) && - cursor.ch > 1 - ) { - // text expansion on tab key if the char before is alphabet - const snippets = JSON.parse( - fs.readFileSync(consts.SNIPPET_FILE, 'utf8') - ) - if (expandSnippet(line, cursor, cm, snippets) === false) { - if (tabs) { - cm.execCommand('insertTab') - } else { - cm.execCommand('insertSoftTab') + this.defaultKeyMap = + CodeMirror + .normalizeKeyMap({ + Tab: function(cm) { + const cursor = cm.getCursor() + const line = cm.getLine(cursor.line) + const cursorPosition = cursor.ch + const charBeforeCursor = line.substr(cursorPosition - 1, 1) + if (cm.somethingSelected()) cm.indentSelection('add') else { + const tabs = cm.getOption('indentWithTabs') + if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) { + cm.execCommand('goLineStart') + if (tabs) { + cm.execCommand('insertTab') + } + else {cm.execCommand('insertSoftTab')} cm.execCommand( + 'goLineEnd') + } + else if ( + !charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) { + // text expansion on tab key if the char before is alphabet + const snippets = + JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8')) + if (expandSnippet(line, cursor, cm, snippets) === false) { + if (tabs) { + cm.execCommand('insertTab') + } else { + cm.execCommand('insertSoftTab') + } + } + } + else { + if (tabs) { + cm.execCommand('insertTab') + } else { + cm.execCommand('insertSoftTab') + } + } + } + }, + 'Cmd-T': function(cm) { + // Do nothing + }, + Enter: 'boostNewLineAndIndentContinueMarkdownList', + 'Ctrl-C': cm => { + if (cm.getOption('keyMap').substr(0, 3) === 'vim') { + document.execCommand('copy') + } + return CodeMirror.Pass } - } - } else { - if (tabs) { - cm.execCommand('insertTab') - } else { - cm.execCommand('insertSoftTab') - } - } - } - }, - 'Cmd-T': function (cm) { - // Do nothing - }, - Enter: 'boostNewLineAndIndentContinueMarkdownList', - 'Ctrl-C': cm => { - if (cm.getOption('keyMap').substr(0, 3) === 'vim') { - document.execCommand('copy') - } - return CodeMirror.Pass - } - }) + }) - this.value = this.props.value - this.editor = CodeMirror(this.refs.root, { - rulers: buildCMRulers(rulers, enableRulers), - value: this.props.value, - lineNumbers: this.props.displayLineNumbers, - lineWrapping: true, - theme: this.props.theme, - indentUnit: this.props.indentSize, - tabSize: this.props.indentSize, - indentWithTabs: this.props.indentType !== 'space', - keyMap: this.props.keyMap, - scrollPastEnd: this.props.scrollPastEnd, - inputStyle: 'textarea', - dragDrop: false, - foldGutter: true, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], - autoCloseBrackets: (this.props.enableBracketMatching ? { - pairs: '()[]{}\'\'""$$**``ll', - triples: '```"""\'\'\'', - explode: '[]{}``$$', - override: true - } : { - pairs: '', - triples: '', - explode: '', - override: true - }), - extraKeys: this.defaultKeyMap - }) + this.value = this.props.value this.editor = + CodeMirror(this.refs.root, { + rulers: buildCMRulers(rulers, enableRulers), + value: this.props.value, + lineNumbers: this.props.displayLineNumbers, + lineWrapping: true, + theme: this.props.theme, + indentUnit: this.props.indentSize, + tabSize: this.props.indentSize, + indentWithTabs: this.props.indentType !== 'space', + keyMap: this.props.keyMap, + scrollPastEnd: this.props.scrollPastEnd, + inputStyle: 'textarea', + dragDrop: false, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + autoCloseBrackets: { + pairs: this.props.matchingPairs, + triples: this.props.matchingTriples, + explode: this.props.explodingPairs, + override: true + }, + extraKeys: this.defaultKeyMap + }) - this.setMode(this.props.mode) + this.setMode(this.props.mode) - this.editor.on('focus', this.focusHandler) - this.editor.on('blur', this.blurHandler) - this.editor.on('change', this.changeHandler) - this.editor.on('paste', this.pasteHandler) + this.editor.on('focus', this.focusHandler) this.editor + .on('blur', this.blurHandler) this.editor + .on('change', this.changeHandler) this.editor.on( + 'paste', this.pasteHandler) eventEmitter.on('top:search', this.searchHandler) - eventEmitter.emit('code:init') - this.editor.on('scroll', this.scrollHandler) + eventEmitter.emit('code:init') this.editor.on('scroll', this.scrollHandler) const editorTheme = document.getElementById('editorTheme') editorTheme.addEventListener('load', this.loadStyleHandler) @@ -263,146 +242,151 @@ export default class CodeEditor extends React.Component { CodeMirror.Vim.defineEx('q!', 'q!', this.quitEditor) CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor) CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor) - CodeMirror.Vim.map('ZZ', ':q', 'normal') + CodeMirror.Vim + .map('ZZ', ':q', 'normal') - this.textEditorInterface = new TextEditorInterface(this.editor) - this.tableEditor = new TableEditor(this.textEditorInterface) - eventEmitter.on('code:format-table', this.formatTable) + this.textEditorInterface = + new TextEditorInterface(this.editor) this.tableEditor = + new TableEditor(this.textEditorInterface) + eventEmitter + .on('code:format-table', this.formatTable) - this.tableEditorOptions = options({ - smartCursor: true - }) + this.tableEditorOptions = options({smartCursor: true}) - this.editorKeyMap = CodeMirror.normalizeKeyMap({ - 'Tab': () => { - this.tableEditor.nextCell(this.tableEditorOptions) - }, - 'Shift-Tab': () => { - this.tableEditor.previousCell(this.tableEditorOptions) - }, - 'Enter': () => { - this.tableEditor.nextRow(this.tableEditorOptions) - }, - 'Ctrl-Enter': () => { - this.tableEditor.escape(this.tableEditorOptions) - }, - 'Cmd-Enter': () => { - this.tableEditor.escape(this.tableEditorOptions) - }, - 'Shift-Ctrl-Left': () => { - this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) - }, - 'Shift-Cmd-Left': () => { - this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) - }, - 'Shift-Ctrl-Right': () => { - this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) - }, - 'Shift-Cmd-Right': () => { - this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) - }, - 'Shift-Ctrl-Up': () => { - this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) - }, - 'Shift-Cmd-Up': () => { - this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) - }, - 'Shift-Ctrl-Down': () => { - this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) - }, - 'Shift-Cmd-Down': () => { - this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) - }, - 'Ctrl-Left': () => { - this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) - }, - 'Cmd-Left': () => { - this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) - }, - 'Ctrl-Right': () => { - this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) - }, - 'Cmd-Right': () => { - this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) - }, - 'Ctrl-Up': () => { - this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) - }, - 'Cmd-Up': () => { - this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) - }, - 'Ctrl-Down': () => { - this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) - }, - 'Cmd-Down': () => { - this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) - }, - 'Ctrl-K Ctrl-I': () => { - this.tableEditor.insertRow(this.tableEditorOptions) - }, - 'Cmd-K Cmd-I': () => { - this.tableEditor.insertRow(this.tableEditorOptions) - }, - 'Ctrl-L Ctrl-I': () => { - this.tableEditor.deleteRow(this.tableEditorOptions) - }, - 'Cmd-L Cmd-I': () => { - this.tableEditor.deleteRow(this.tableEditorOptions) - }, - 'Ctrl-K Ctrl-J': () => { - this.tableEditor.insertColumn(this.tableEditorOptions) - }, - 'Cmd-K Cmd-J': () => { - this.tableEditor.insertColumn(this.tableEditorOptions) - }, - 'Ctrl-L Ctrl-J': () => { - this.tableEditor.deleteColumn(this.tableEditorOptions) - }, - 'Cmd-L Cmd-J': () => { - this.tableEditor.deleteColumn(this.tableEditorOptions) - }, - 'Alt-Shift-Ctrl-Left': () => { - this.tableEditor.moveColumn(-1, this.tableEditorOptions) - }, - 'Alt-Shift-Cmd-Left': () => { - this.tableEditor.moveColumn(-1, this.tableEditorOptions) - }, - 'Alt-Shift-Ctrl-Right': () => { - this.tableEditor.moveColumn(1, this.tableEditorOptions) - }, - 'Alt-Shift-Cmd-Right': () => { - this.tableEditor.moveColumn(1, this.tableEditorOptions) - }, - 'Alt-Shift-Ctrl-Up': () => { - this.tableEditor.moveRow(-1, this.tableEditorOptions) - }, - 'Alt-Shift-Cmd-Up': () => { - this.tableEditor.moveRow(-1, this.tableEditorOptions) - }, - 'Alt-Shift-Ctrl-Down': () => { - this.tableEditor.moveRow(1, this.tableEditorOptions) - }, - 'Alt-Shift-Cmd-Down': () => { - this.tableEditor.moveRow(1, this.tableEditorOptions) - } - }) + this.editorKeyMap = + CodeMirror.normalizeKeyMap({ + 'Tab': () => { + this.tableEditor.nextCell(this.tableEditorOptions) + }, + 'Shift-Tab': () => { + this.tableEditor.previousCell(this.tableEditorOptions) + }, + 'Enter': () => { + this.tableEditor.nextRow(this.tableEditorOptions) + }, + 'Ctrl-Enter': () => { + this.tableEditor.escape(this.tableEditorOptions) + }, + 'Cmd-Enter': () => { + this.tableEditor.escape(this.tableEditorOptions) + }, + 'Shift-Ctrl-Left': () => { + this.tableEditor.alignColumn( + Alignment.LEFT, this.tableEditorOptions) + }, + 'Shift-Cmd-Left': () => { + this.tableEditor.alignColumn( + Alignment.LEFT, this.tableEditorOptions) + }, + 'Shift-Ctrl-Right': () => { + this.tableEditor.alignColumn( + Alignment.RIGHT, this.tableEditorOptions) + }, + 'Shift-Cmd-Right': () => { + this.tableEditor.alignColumn( + Alignment.RIGHT, this.tableEditorOptions) + }, + 'Shift-Ctrl-Up': () => { + this.tableEditor.alignColumn( + Alignment.CENTER, this.tableEditorOptions) + }, + 'Shift-Cmd-Up': () => { + this.tableEditor.alignColumn( + Alignment.CENTER, this.tableEditorOptions) + }, + 'Shift-Ctrl-Down': () => { + this.tableEditor.alignColumn( + Alignment.NONE, this.tableEditorOptions) + }, + 'Shift-Cmd-Down': () => { + this.tableEditor.alignColumn( + Alignment.NONE, this.tableEditorOptions) + }, + 'Ctrl-Left': () => { + this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) + }, + 'Cmd-Left': () => { + this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) + }, + 'Ctrl-Right': () => { + this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) + }, + 'Cmd-Right': () => { + this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) + }, + 'Ctrl-Up': () => { + this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) + }, + 'Cmd-Up': () => { + this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) + }, + 'Ctrl-Down': () => { + this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) + }, + 'Cmd-Down': () => { + this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) + }, + 'Ctrl-K Ctrl-I': () => { + this.tableEditor.insertRow(this.tableEditorOptions) + }, + 'Cmd-K Cmd-I': () => { + this.tableEditor.insertRow(this.tableEditorOptions) + }, + 'Ctrl-L Ctrl-I': () => { + this.tableEditor.deleteRow(this.tableEditorOptions) + }, + 'Cmd-L Cmd-I': () => { + this.tableEditor.deleteRow(this.tableEditorOptions) + }, + 'Ctrl-K Ctrl-J': () => { + this.tableEditor.insertColumn(this.tableEditorOptions) + }, + 'Cmd-K Cmd-J': () => { + this.tableEditor.insertColumn(this.tableEditorOptions) + }, + 'Ctrl-L Ctrl-J': () => { + this.tableEditor.deleteColumn(this.tableEditorOptions) + }, + 'Cmd-L Cmd-J': () => { + this.tableEditor.deleteColumn(this.tableEditorOptions) + }, + 'Alt-Shift-Ctrl-Left': () => { + this.tableEditor.moveColumn(-1, this.tableEditorOptions) + }, + 'Alt-Shift-Cmd-Left': () => { + this.tableEditor.moveColumn(-1, this.tableEditorOptions) + }, + 'Alt-Shift-Ctrl-Right': () => { + this.tableEditor.moveColumn(1, this.tableEditorOptions) + }, + 'Alt-Shift-Cmd-Right': () => { + this.tableEditor.moveColumn(1, this.tableEditorOptions) + }, + 'Alt-Shift-Ctrl-Up': () => { + this.tableEditor.moveRow(-1, this.tableEditorOptions) + }, + 'Alt-Shift-Cmd-Up': () => { + this.tableEditor.moveRow(-1, this.tableEditorOptions) + }, + 'Alt-Shift-Ctrl-Down': () => { + this.tableEditor.moveRow(1, this.tableEditorOptions) + }, + 'Alt-Shift-Cmd-Down': () => { + this.tableEditor.moveRow(1, this.tableEditorOptions) + } + }) if (this.props.enableTableEditor) { - this.editor.on('cursorActivity', this.editorActivityHandler) - this.editor.on('changes', this.editorActivityHandler) + this.editor.on('cursorActivity', this.editorActivityHandler) this.editor + .on('changes', this.editorActivityHandler) } - this.setState({ - clientWidth: this.refs.root.clientWidth - }) + this.setState({clientWidth: this.refs.root.clientWidth}) } expandSnippet(line, cursor, cm, snippets) { - const wordBeforeCursor = this.getWordBeforeCursor( - line, - cursor.line, - cursor.ch - ) + const wordBeforeCursor = + this.getWordBeforeCursor(line, cursor.line, cursor.ch) const templateCursorString = ':{}' for (let i = 0; i < snippets.length; i++) { if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) { @@ -416,10 +400,8 @@ export default class CodeEditor extends React.Component { cursorLineNumber = j cursorLinePosition = cursorIndex cm.replaceRange( - snippets[i].content.replace(templateCursorString, ''), - wordBeforeCursor.range.from, - wordBeforeCursor.range.to - ) + snippets[i].content.replace(templateCursorString, ''), + wordBeforeCursor.range.from, wordBeforeCursor.range.to) cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition @@ -428,10 +410,8 @@ export default class CodeEditor extends React.Component { } } else { cm.replaceRange( - snippets[i].content, - wordBeforeCursor.range.from, - wordBeforeCursor.range.to - ) + snippets[i].content, wordBeforeCursor.range.from, + wordBeforeCursor.range.to) } return true } @@ -446,7 +426,8 @@ export default class CodeEditor extends React.Component { const emptyChars = /\t|\s|\r|\n/ // to prevent the word to expand is long that will crash the whole app - // the safeStop is there to stop user to expand words that longer than 20 chars + // the safeStop is there to stop user to expand words that longer than 20 + // chars const safeStop = 20 while (cursorPosition > 0) { @@ -454,25 +435,17 @@ export default class CodeEditor extends React.Component { // if char is not an empty char if (!emptyChars.test(currentChar)) { wordBeforeCursor = currentChar + wordBeforeCursor - } else if (wordBeforeCursor.length >= safeStop) { - throw new Error('Your snippet trigger is too long !') - } else { - break } - cursorPosition-- + else if (wordBeforeCursor.length >= safeStop) { + throw new Error('Your snippet trigger is too long !') + } + else {break} cursorPosition-- } return { - text: wordBeforeCursor, - range: { - from: { - line: lineNumber, - ch: originCursorPosition - }, - to: { - line: lineNumber, - ch: cursorPosition - } + text: wordBeforeCursor, range: { + from: {line: lineNumber, ch: originCursorPosition}, + to: {line: lineNumber, ch: cursorPosition} } } } @@ -482,12 +455,12 @@ export default class CodeEditor extends React.Component { } componentWillUnmount() { - this.editor.off('focus', this.focusHandler) - this.editor.off('blur', this.blurHandler) - this.editor.off('change', this.changeHandler) - this.editor.off('paste', this.pasteHandler) - eventEmitter.off('top:search', this.searchHandler) - this.editor.off('scroll', this.scrollHandler) + this.editor.off('focus', this.focusHandler) this.editor + .off('blur', this.blurHandler) this.editor + .off('change', this.changeHandler) this.editor.off( + 'paste', this.pasteHandler) + eventEmitter.off('top:search', this.searchHandler) this.editor.off( + 'scroll', this.scrollHandler) const editorTheme = document.getElementById('editorTheme') editorTheme.removeEventListener('load', this.loadStyleHandler) @@ -496,10 +469,7 @@ export default class CodeEditor extends React.Component { componentDidUpdate(prevProps, prevState) { let needRefresh = false - const { - rulers, - enableRulers - } = this.props + const {rulers, enableRulers} = this.props if (prevProps.mode !== this.props.mode) { this.setMode(this.props.mode) } @@ -517,16 +487,14 @@ export default class CodeEditor extends React.Component { needRefresh = true } - if ( - prevProps.enableRulers !== enableRulers || - prevProps.rulers !== rulers - ) { + if (prevProps.enableRulers !== enableRulers || + prevProps.rulers !== rulers) { this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers)) } if (prevProps.indentSize !== this.props.indentSize) { - this.editor.setOption('indentUnit', this.props.indentSize) - this.editor.setOption('tabSize', this.props.indentSize) + this.editor.setOption('indentUnit', this.props.indentSize) this.editor + .setOption('tabSize', this.props.indentSize) } if (prevProps.indentType !== this.props.indentType) { this.editor.setOption('indentWithTabs', this.props.indentType !== 'space') @@ -540,38 +508,33 @@ export default class CodeEditor extends React.Component { this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) } - if (prevProps.enableBracketMatching !== this.props.enableBracketMatching) { - const bracketObject = (this.props.enableBracketMatching ? { - pairs: '()[]{}\'\'""$$**``ll', - triples: '```"""\'\'\'', - explode: '[]{}``$$', + if (prevProps.matchingPairs !== this.props.matchingPairs || + prevProps.matchingTriples !== this.props.matchingTriples || + prevProps.explodingPairs !== this.props.explodingPairs) { + const bracketObject = { + pairs: this.props.matchingPairs, + triples: this.props.matchingTriples, + explode: this.props.explodingPairs, override: true - } : { - pairs: '', - triples: '', - explode: '', - override: true - }); + } this.editor.setOption('autoCloseBrackets', bracketObject) } if (prevProps.enableTableEditor !== this.props.enableTableEditor) { if (this.props.enableTableEditor) { - this.editor.on('cursorActivity', this.editorActivityHandler) - this.editor.on('changes', this.editorActivityHandler) + this.editor.on('cursorActivity', this.editorActivityHandler) this.editor + .on('changes', this.editorActivityHandler) } else { - this.editor.off('cursorActivity', this.editorActivityHandler) - this.editor.off('changes', this.editorActivityHandler) + this.editor.off('cursorActivity', this.editorActivityHandler) this + .editor.off('changes', this.editorActivityHandler) } - this.extraKeysMode = 'default' - this.editor.setOption('extraKeys', this.defaultKeyMap) + this.extraKeysMode = + 'default' this.editor.setOption('extraKeys', this.defaultKeyMap) } if (this.state.clientWidth !== this.refs.root.clientWidth) { - this.setState({ - clientWidth: this.refs.root.clientWidth - }) + this.setState({clientWidth: this.refs.root.clientWidth}) needRefresh = true } @@ -583,9 +546,11 @@ export default class CodeEditor extends React.Component { setMode(mode) { let syntax = CodeMirror.findModeByName(convertModeName(mode)) - if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') + if (syntax == null) syntax = + CodeMirror + .findModeByName('Plain Text') - this.editor.setOption('mode', syntax.mime) + this.editor.setOption('mode', syntax.mime) CodeMirror.autoLoadMode(this.editor, syntax.mode) } @@ -599,11 +564,7 @@ export default class CodeEditor extends React.Component { moveCursorTo(row, col) {} scrollToLine(event, num) { - const cursor = { - line: num, - ch: 1 - } - this.editor.setCursor(cursor) + const cursor = { line: num, ch: 1 } this.editor.setCursor(cursor) } focus() { @@ -616,32 +577,22 @@ export default class CodeEditor extends React.Component { reload() { // Change event shouldn't be fired when switch note - this.editor.off('change', this.changeHandler) - this.value = this.props.value - this.editor.setValue(this.props.value) - this.editor.clearHistory() - this.editor.on('change', this.changeHandler) - this.editor.refresh() + this.editor.off('change', this.changeHandler) this.value = + this.props.value this.editor.setValue(this.props.value) this.editor + .clearHistory() this.editor.on('change', this.changeHandler) this + .editor.refresh() } setValue(value) { - const cursor = this.editor.getCursor() - this.editor.setValue(value) - this.editor.setCursor(cursor) + const cursor = this.editor.getCursor() this.editor.setValue(value) this + .editor.setCursor(cursor) } handleDropImage(dropEvent) { dropEvent.preventDefault() - const { - storageKey, - noteKey - } = this.props + const {storageKey, noteKey} = this.props attachmentManagement.handleAttachmentDrop( - this, - storageKey, - noteKey, - dropEvent - ) + this, storageKey, noteKey, dropEvent) } insertAttachmentMd(imageMd) { @@ -650,59 +601,43 @@ export default class CodeEditor extends React.Component { handlePaste(editor, e) { const clipboardData = e.clipboardData - const { - storageKey, - noteKey - } = this.props - const dataTransferItem = clipboardData.items[0] - const pastedTxt = clipboardData.getData('text') - const isURL = str => { - const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ - return matcher.test(str) - } - const isInLinkTag = editor => { - const startCursor = editor.getCursor('start') - const prevChar = editor.getRange({ - line: startCursor.line, - ch: startCursor.ch - 2 - }, { - line: startCursor.line, - ch: startCursor.ch - }) - const endCursor = editor.getCursor('end') - const nextChar = editor.getRange({ - line: endCursor.line, - ch: endCursor.ch - }, { - line: endCursor.line, - ch: endCursor.ch + 1 - }) - return prevChar === '](' && nextChar === ')' - } + const {storageKey, noteKey} = this.props + const dataTransferItem = clipboardData.items[0] const pastedTxt = + clipboardData.getData('text') + const isURL = + str => { + const matcher = + /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ + return matcher.test(str) + } const isInLinkTag = + editor => { + const startCursor = editor.getCursor('start') + const prevChar = editor.getRange( + {line: startCursor.line, ch: startCursor.ch - 2}, + {line: startCursor.line, ch: startCursor.ch}) + const endCursor = editor.getCursor('end') + const nextChar = editor.getRange( + {line: endCursor.line, ch: endCursor.ch}, + {line: endCursor.line, ch: endCursor.ch + 1}) + return prevChar === '](' && nextChar === ')' + } const pastedHtml = clipboardData.getData('text/html') if (pastedHtml !== '') { this.handlePasteHtml(e, editor, pastedHtml) - } else if (dataTransferItem.type.match('image')) { + } + else if (dataTransferItem.type.match('image')) { attachmentManagement.handlePastImageEvent( - this, - storageKey, - noteKey, - dataTransferItem - ) - } else if ( - this.props.fetchUrlTitle && - isURL(pastedTxt) && - !isInLinkTag(editor) - ) { + this, storageKey, noteKey, dataTransferItem) + } + else if ( + this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { this.handlePasteUrl(e, editor, pastedTxt) } if (attachmentManagement.isAttachmentLink(pastedTxt)) { attachmentManagement - .handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt) - .then(modifiedText => { - this.editor.replaceSelection(modifiedText) - }) + .handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt) + .then(modifiedText => {this.editor.replaceSelection(modifiedText)}) e.preventDefault() } } @@ -718,39 +653,36 @@ export default class CodeEditor extends React.Component { const taggedUrl = `<${pastedTxt}>` editor.replaceSelection(taggedUrl) - const isImageReponse = response => { - return ( - response.headers.has('content-type') && - response.headers.get('content-type').match(/^image\/.+$/) - ) - } - const replaceTaggedUrl = replacement => { - const value = editor.getValue() - const cursor = editor.getCursor() - const newValue = value.replace(taggedUrl, replacement) - const newCursor = Object.assign({}, cursor, { - ch: cursor.ch + newValue.length - value.length - }) - editor.setValue(newValue) - editor.setCursor(newCursor) - } + const isImageReponse = + response => { + return ( + response.headers.has('content-type') && + response.headers.get('content-type').match(/^image\/.+$/)) + } const replaceTaggedUrl = + replacement => { + const value = editor.getValue() + const cursor = editor.getCursor() + const newValue = value.replace(taggedUrl, replacement) + const newCursor = Object.assign( + {}, cursor, {ch: cursor.ch + newValue.length - value.length}) + editor.setValue(newValue) + editor.setCursor(newCursor) + } - fetch(pastedTxt, { - method: 'get' - }) - .then(response => { - if (isImageReponse(response)) { - return this.mapImageResponse(response, pastedTxt) - } else { - return this.mapNormalResponse(response, pastedTxt) - } - }) - .then(replacement => { - replaceTaggedUrl(replacement) - }) - .catch(e => { - replaceTaggedUrl(pastedTxt) - }) + fetch(pastedTxt, {method: 'get'}) + .then(response => { + if (isImageReponse(response)) { + return this.mapImageResponse( + response, pastedTxt) + } else { + return this.mapNormalResponse( + response, pastedTxt) + } + }) + .then( + replacement => { + replaceTaggedUrl(replacement)}) + .catch(e => {replaceTaggedUrl(pastedTxt)}) } handlePasteHtml(e, editor, pastedHtml) { @@ -760,23 +692,21 @@ export default class CodeEditor extends React.Component { } mapNormalResponse(response, pastedTxt) { - return this.decodeResponse(response).then(body => { - return new Promise((resolve, reject) => { - try { - const parsedBody = new window.DOMParser().parseFromString( - body, - 'text/html' - ) - const escapePipe = (str) => { - return str.replace('|', '\\|') + return this.decodeResponse(response).then( + body => {return new Promise((resolve, reject) => { + try { + const parsedBody = + new window.DOMParser().parseFromString(body, 'text/html') + const escapePipe = + (str) => { + return str.replace('|', '\\|') + } const linkWithTitle = + `[${escapePipe(parsedBody.title)}](${pastedTxt})` + resolve(linkWithTitle) + } catch (e) { + reject(e) } - const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})` - resolve(linkWithTitle) - } catch (e) { - reject(e) - } - }) - }) + })}) } mapImageResponse(response, pastedTxt) { @@ -795,39 +725,30 @@ export default class CodeEditor extends React.Component { decodeResponse(response) { const headers = response.headers const _charset = headers.has('content-type') ? - this.extractContentTypeCharset(headers.get('content-type')) : - undefined - return response.arrayBuffer().then(buff => { - return new Promise((resolve, reject) => { - try { - const charset = _charset !== undefined && - iconv.encodingExists(_charset) ? - _charset : - 'utf-8' - resolve(iconv.decode(new Buffer(buff), charset).toString()) - } catch (e) { - reject(e) - } - }) - }) + this.extractContentTypeCharset(headers.get('content-type')) : + undefined + return response.arrayBuffer().then( + buff => {return new Promise((resolve, reject) => { + try { + const charset = + _charset !== undefined && iconv.encodingExists(_charset) ? + _charset : + 'utf-8' + resolve(iconv.decode(new Buffer(buff), charset).toString()) + } catch (e) { + reject(e) + } + })}) } extractContentTypeCharset(contentType) { - return contentType - .split(';') - .filter(str => { - return str.trim().toLowerCase().startsWith('charset') - }) - .map(str => { - return str.replace(/['"]/g, '').split('=')[1] - })[0] + return contentType.split(';') + .filter(str => {return str.trim().toLowerCase().startsWith('charset')}) + .map(str => {return str.replace(/['"]/g, '').split('=')[1]})[0] } render() { - const { - className, - fontSize - } = this.props + const {className, fontSize} = this.props const fontFamily = normalizeEditorFontFamily(this.props.fontFamily) const width = this.props.width return ( < @@ -835,18 +756,12 @@ export default class CodeEditor extends React.Component { className == null ? 'CodeEditor' : `CodeEditor ${className}` } ref = 'root' - tabIndex = '-1' - style = { - { - fontFamily, - fontSize: fontSize, - width: width - } - } - onDrop = { - e => this.handleDropImage(e) - } - /> + tabIndex = '-1' + style = { + { fontFamily, fontSize: fontSize, width: width } + } onDrop = { + e => this.handleDropImage(e) + } /> ) } } diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index f10ea8bc..dcb1b5ea 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -269,6 +269,9 @@ class MarkdownEditor extends React.Component { rulers={config.editor.rulers} displayLineNumbers={config.editor.displayLineNumbers} enableBracketMatching={config.editor.enableBracketMatching} + matchingPairs={config.editor.matchingPairs} + matchingTriples={config.editor.matchingTriples} + explodingPairs={config.editor.explodingPairs} scrollPastEnd={config.editor.scrollPastEnd} storageKey={storageKey} noteKey={noteKey} diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 004a6eb0..689799c8 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -159,6 +159,9 @@ class MarkdownSplitEditor extends React.Component { fontSize={editorFontSize} displayLineNumbers={config.editor.displayLineNumbers} enableBracketMatching={config.editor.enableBracketMatching} + matchingPairs={config.editor.matchingPairs} + matchingTriples={config.editor.matchingTriples} + explodingPairs={config.editor.explodingPairs} indentType={config.editor.indentType} indentSize={editorIndentSize} enableRulers={config.editor.enableRulers} diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 90b8faa9..86ce2c7a 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -700,6 +700,9 @@ class SnippetNoteDetail extends React.Component { indentSize={editorIndentSize} displayLineNumbers={config.editor.displayLineNumbers} enableBracketMatching={config.editor.enableBracketMatching} + matchingPairs={config.editor.matchingPairs} + matchingTriples={config.editor.matchingTriples} + explodingPairs={config.editor.explodingPairs} keyMap={config.editor.keyMap} scrollPastEnd={config.editor.scrollPastEnd} fetchUrlTitle={config.editor.fetchUrlTitle} diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index eddf0909..c86e9288 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -45,6 +45,9 @@ export const DEFAULT_CONFIG = { rulers: [80, 120], displayLineNumbers: true, enableBracketMatching: true, + matchingPairs:'()[]{}\'\'""$$**``', + matchingTriples:'```"""\'\'\'', + explodingPairs:'[]{}``$$', switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK' delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE' scrollPastEnd: false, diff --git a/browser/main/modals/PreferencesModal/SnippetTab.js b/browser/main/modals/PreferencesModal/SnippetTab.js index 40b65d39..ebd4012e 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.js +++ b/browser/main/modals/PreferencesModal/SnippetTab.js @@ -137,6 +137,9 @@ class SnippetTab extends React.Component { rulers={config.editor.rulers} displayLineNumbers={config.editor.displayLineNumbers} enableBracketMatching={config.editor.enableBracketMatching} + matchingPairs={config.editor.matchingPairs} + matchingTriples={config.editor.matchingTriples} + explodingPairs={config.editor.explodingPairs} scrollPastEnd={config.editor.scrollPastEnd} onRef={ref => { this.snippetEditor = ref }} /> diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index ab72c914..2bd0c9d0 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -95,7 +95,10 @@ class UiTab extends React.Component { enableTableEditor: this.refs.enableTableEditor.checked, enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked, frontMatterTitleField: this.refs.frontMatterTitleField.value, - enableBracketMatching: this.refs.enableBracketMatching.checked + enableBracketMatching: this.refs.enableBracketMatching.checked, + matchingPairs: this.refs.matchingPairs.value, + matchingTriples: this.refs.matchingTriples.value, + explodingPairs: this.refs.explodingPairs.value }, preview: { fontSize: this.refs.previewFontSize.value, @@ -551,6 +554,48 @@ class UiTab extends React.Component { +