diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 7719ed90..35a71ec7 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -38,6 +38,7 @@ export default class CodeEditor extends React.Component { trailing: true }) this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject) + this.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject) this.focusHandler = () => { ipcRenderer.send('editor:focused', true) } @@ -235,6 +236,7 @@ export default class CodeEditor extends React.Component { this.editor = CodeMirror(this.refs.root, { rulers: buildCMRulers(rulers, enableRulers), value: this.props.value, + linesHighlighted: this.props.linesHighlighted, lineNumbers: this.props.displayLineNumbers, lineWrapping: true, theme: this.props.theme, @@ -261,6 +263,7 @@ export default class CodeEditor extends React.Component { this.editor.on('focus', this.focusHandler) this.editor.on('blur', this.blurHandler) this.editor.on('change', this.changeHandler) + this.editor.on('gutterClick', this.highlightHandler) this.editor.on('paste', this.pasteHandler) if (this.props.switchPreview !== 'RIGHTCLICK') { this.editor.on('contextmenu', this.contextMenuHandler) @@ -339,6 +342,8 @@ export default class CodeEditor extends React.Component { this.setState({ clientWidth: this.refs.root.clientWidth }) + + this.initialHighlighting() } expandSnippet (line, cursor, cm, snippets) { @@ -537,12 +542,96 @@ export default class CodeEditor extends React.Component { handleChange (editor, changeObject) { spellcheck.handleChange(editor, changeObject) + + this.updateHighlight(editor, changeObject) + this.value = editor.getValue() if (this.props.onChange) { this.props.onChange(editor) } } + incrementLines (start, linesAdded, linesRemoved, editor) { + let highlightedLines = editor.options.linesHighlighted + + const totalHighlightedLines = highlightedLines.length + + let offset = linesAdded - linesRemoved + + // Store new items to be added as we're changing the lines + let newLines = [] + + let i = totalHighlightedLines + + while (i--) { + const lineNumber = highlightedLines[i] + + // Interval that will need to be updated + // Between start and (start + offset) remove highlight + if (lineNumber >= start) { + highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1) + + // Lines that need to be relocated + if (lineNumber >= (start + linesRemoved)) { + newLines.push(lineNumber + offset) + } + } + } + + // Adding relocated lines + highlightedLines.push(...newLines) + + if (this.props.onChange) { + this.props.onChange(editor) + } + } + + handleHighlight (editor, changeObject) { + const lines = editor.options.linesHighlighted + + if (!lines.includes(changeObject)) { + lines.push(changeObject) + editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background') + } else { + lines.splice(lines.indexOf(changeObject), 1) + editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background') + } + if (this.props.onChange) { + this.props.onChange(editor) + } + } + + updateHighlight (editor, changeObject) { + const linesAdded = changeObject.text.length - 1 + const linesRemoved = changeObject.removed.length - 1 + + // If no lines added or removed return + if (linesAdded === 0 && linesRemoved === 0) { + return + } + + let start = changeObject.from.line + + switch (changeObject.origin) { + case '+insert", "undo': + start += 1 + break + + case 'paste': + case '+delete': + case '+input': + if (changeObject.to.ch !== 0 || changeObject.from.ch !== 0) { + start += 1 + } + break + + default: + return + } + + this.incrementLines(start, linesAdded, linesRemoved, editor) + } + moveCursorTo (row, col) {} scrollToLine (event, num) { @@ -567,6 +656,7 @@ export default class CodeEditor extends React.Component { this.value = this.props.value this.editor.setValue(this.props.value) this.editor.clearHistory() + this.restartHighlighting() this.editor.on('change', this.changeHandler) this.editor.refresh() } @@ -758,6 +848,29 @@ export default class CodeEditor extends React.Component { }) } + initialHighlighting () { + if (this.editor.options.linesHighlighted == null) { + return + } + + const totalHighlightedLines = this.editor.options.linesHighlighted.length + const totalAvailableLines = this.editor.lineCount() + + for (let i = 0; i < totalHighlightedLines; i++) { + const lineNumber = this.editor.options.linesHighlighted[i] + if (lineNumber > totalAvailableLines) { + // make sure that we skip the invalid lines althrough this case should not be happened. + continue + } + this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background') + } + } + + restartHighlighting () { + this.editor.options.linesHighlighted = this.props.linesHighlighted + this.initialHighlighting() + } + mapImageResponse (response, pastedTxt) { return new Promise((resolve, reject) => { try { diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index d3270c18..00a93b21 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -232,7 +232,7 @@ class MarkdownEditor extends React.Component { } render () { - const {className, value, config, storageKey, noteKey} = this.props + const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props let editorFontSize = parseInt(config.editor.fontSize, 10) if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 @@ -275,6 +275,7 @@ class MarkdownEditor extends React.Component { noteKey={noteKey} fetchUrlTitle={config.editor.fetchUrlTitle} enableTableEditor={config.editor.enableTableEditor} + linesHighlighted={linesHighlighted} onChange={(e) => this.handleChange(e)} onBlur={(e) => this.handleBlur(e)} spellCheck={config.editor.spellcheck} diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index bd79bc24..f8f8b366 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -24,9 +24,9 @@ class MarkdownSplitEditor extends React.Component { this.refs.code.setValue(value) } - handleOnChange () { + handleOnChange (e) { this.value = this.refs.code.value - this.props.onChange() + this.props.onChange(e) } handleScroll (e) { @@ -136,7 +136,7 @@ class MarkdownSplitEditor extends React.Component { } render () { - const {config, value, storageKey, noteKey} = this.props + const {config, value, storageKey, noteKey, linesHighlighted} = this.props const storage = findStorage(storageKey) let editorFontSize = parseInt(config.editor.fontSize, 10) if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 @@ -169,7 +169,8 @@ class MarkdownSplitEditor extends React.Component { enableTableEditor={config.editor.enableTableEditor} storageKey={storageKey} noteKey={noteKey} - onChange={this.handleOnChange.bind(this)} + linesHighlighted={linesHighlighted} + onChange={(e) => this.handleOnChange(e)} onScroll={this.handleScroll.bind(this)} spellCheck={config.editor.spellcheck} enableSmartPaste={config.editor.enableSmartPaste} diff --git a/browser/lib/newNote.js b/browser/lib/newNote.js index 0b64d0e1..9511f847 100644 --- a/browser/lib/newNote.js +++ b/browser/lib/newNote.js @@ -18,7 +18,8 @@ export function createMarkdownNote (storage, folder, dispatch, location, params, folder: folder, title: '', tags, - content: '' + content: '', + linesHighlighted: [] }) .then(note => { const noteHash = note.key @@ -56,7 +57,8 @@ export function createSnippetNote (storage, folder, dispatch, location, params, { name: '', mode: config.editor.snippetDefaultLanguage || 'text', - content: '' + content: '', + linesHighlighted: [] } ] }) diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index b4e7a5b3..bc6cd499 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -39,12 +39,14 @@ class MarkdownNoteDetail extends React.Component { isMovingNote: false, note: Object.assign({ title: '', - content: '' + content: '', + linesHighlighted: [] }, props.note), isLockButtonShown: false, isLocked: false, editorType: props.config.editor.type } + this.dispatchTimer = null this.toggleLockButton = this.handleToggleLockButton.bind(this) @@ -71,7 +73,7 @@ class MarkdownNoteDetail extends React.Component { if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) { if (this.saveQueue != null) this.saveNow() this.setState({ - note: Object.assign({}, nextProps.note) + note: Object.assign({linesHighlighted: []}, nextProps.note) }, () => { this.refs.content.reload() if (this.refs.tags) this.refs.tags.reset() @@ -361,6 +363,7 @@ class MarkdownNoteDetail extends React.Component { value={note.content} storageKey={note.storage} noteKey={note.key} + linesHighlighted={note.linesHighlighted} onChange={this.handleUpdateContent.bind(this)} ignorePreviewPointerEvents={ignorePreviewPointerEvents} /> @@ -371,6 +374,7 @@ class MarkdownNoteDetail extends React.Component { value={note.content} storageKey={note.storage} noteKey={note.key} + linesHighlighted={note.linesHighlighted} onChange={this.handleUpdateContent.bind(this)} ignorePreviewPointerEvents={ignorePreviewPointerEvents} /> diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 887e5237..ebe61ba9 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -48,7 +48,7 @@ class SnippetNoteDetail extends React.Component { note: Object.assign({ description: '' }, props.note, { - snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet)) + snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet)) }) } @@ -76,8 +76,9 @@ class SnippetNoteDetail extends React.Component { const nextNote = Object.assign({ description: '' }, nextProps.note, { - snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet)) + snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet)) }) + this.setState({ snippetIndex: 0, note: nextNote @@ -410,6 +411,8 @@ class SnippetNoteDetail extends React.Component { return (e) => { const snippets = this.state.note.snippets.slice() snippets[index].content = this.refs['code-' + index].value + snippets[index].linesHighlighted = e.options.linesHighlighted + this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})})) this.setState(state => ({ note: state.note @@ -602,7 +605,8 @@ class SnippetNoteDetail extends React.Component { note.snippets = note.snippets.concat([{ name: '', mode: config.editor.snippetDefaultLanguage || 'text', - content: '' + content: '', + linesHighlighted: [] }]) const snippetIndex = note.snippets.length - 1 @@ -692,10 +696,8 @@ class SnippetNoteDetail extends React.Component { const viewList = note.snippets.map((snippet, index) => { const isActive = this.state.snippetIndex === index - let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode)) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') - return