Skip to content

Commit 2df0f1b

Browse files
authored
Merge pull request #2682 from duartefrazao/master
Feature - Line highlighting within code block #2469
2 parents 1ae1414 + e93bf1c commit 2df0f1b

16 files changed

+193
-31
lines changed

browser/components/CodeEditor.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export default class CodeEditor extends React.Component {
3838
trailing: true
3939
})
4040
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
41+
this.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject)
4142
this.focusHandler = () => {
4243
ipcRenderer.send('editor:focused', true)
4344
}
@@ -235,6 +236,7 @@ export default class CodeEditor extends React.Component {
235236
this.editor = CodeMirror(this.refs.root, {
236237
rulers: buildCMRulers(rulers, enableRulers),
237238
value: this.props.value,
239+
linesHighlighted: this.props.linesHighlighted,
238240
lineNumbers: this.props.displayLineNumbers,
239241
lineWrapping: true,
240242
theme: this.props.theme,
@@ -261,6 +263,7 @@ export default class CodeEditor extends React.Component {
261263
this.editor.on('focus', this.focusHandler)
262264
this.editor.on('blur', this.blurHandler)
263265
this.editor.on('change', this.changeHandler)
266+
this.editor.on('gutterClick', this.highlightHandler)
264267
this.editor.on('paste', this.pasteHandler)
265268
if (this.props.switchPreview !== 'RIGHTCLICK') {
266269
this.editor.on('contextmenu', this.contextMenuHandler)
@@ -339,6 +342,8 @@ export default class CodeEditor extends React.Component {
339342
this.setState({
340343
clientWidth: this.refs.root.clientWidth
341344
})
345+
346+
this.initialHighlighting()
342347
}
343348

344349
expandSnippet (line, cursor, cm, snippets) {
@@ -537,12 +542,96 @@ export default class CodeEditor extends React.Component {
537542

538543
handleChange (editor, changeObject) {
539544
spellcheck.handleChange(editor, changeObject)
545+
546+
this.updateHighlight(editor, changeObject)
547+
540548
this.value = editor.getValue()
541549
if (this.props.onChange) {
542550
this.props.onChange(editor)
543551
}
544552
}
545553

554+
incrementLines (start, linesAdded, linesRemoved, editor) {
555+
let highlightedLines = editor.options.linesHighlighted
556+
557+
const totalHighlightedLines = highlightedLines.length
558+
559+
let offset = linesAdded - linesRemoved
560+
561+
// Store new items to be added as we're changing the lines
562+
let newLines = []
563+
564+
let i = totalHighlightedLines
565+
566+
while (i--) {
567+
const lineNumber = highlightedLines[i]
568+
569+
// Interval that will need to be updated
570+
// Between start and (start + offset) remove highlight
571+
if (lineNumber >= start) {
572+
highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1)
573+
574+
// Lines that need to be relocated
575+
if (lineNumber >= (start + linesRemoved)) {
576+
newLines.push(lineNumber + offset)
577+
}
578+
}
579+
}
580+
581+
// Adding relocated lines
582+
highlightedLines.push(...newLines)
583+
584+
if (this.props.onChange) {
585+
this.props.onChange(editor)
586+
}
587+
}
588+
589+
handleHighlight (editor, changeObject) {
590+
const lines = editor.options.linesHighlighted
591+
592+
if (!lines.includes(changeObject)) {
593+
lines.push(changeObject)
594+
editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
595+
} else {
596+
lines.splice(lines.indexOf(changeObject), 1)
597+
editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
598+
}
599+
if (this.props.onChange) {
600+
this.props.onChange(editor)
601+
}
602+
}
603+
604+
updateHighlight (editor, changeObject) {
605+
const linesAdded = changeObject.text.length - 1
606+
const linesRemoved = changeObject.removed.length - 1
607+
608+
// If no lines added or removed return
609+
if (linesAdded === 0 && linesRemoved === 0) {
610+
return
611+
}
612+
613+
let start = changeObject.from.line
614+
615+
switch (changeObject.origin) {
616+
case '+insert", "undo':
617+
start += 1
618+
break
619+
620+
case 'paste':
621+
case '+delete':
622+
case '+input':
623+
if (changeObject.to.ch !== 0 || changeObject.from.ch !== 0) {
624+
start += 1
625+
}
626+
break
627+
628+
default:
629+
return
630+
}
631+
632+
this.incrementLines(start, linesAdded, linesRemoved, editor)
633+
}
634+
546635
moveCursorTo (row, col) {}
547636

548637
scrollToLine (event, num) {
@@ -567,6 +656,7 @@ export default class CodeEditor extends React.Component {
567656
this.value = this.props.value
568657
this.editor.setValue(this.props.value)
569658
this.editor.clearHistory()
659+
this.restartHighlighting()
570660
this.editor.on('change', this.changeHandler)
571661
this.editor.refresh()
572662
}
@@ -758,6 +848,29 @@ export default class CodeEditor extends React.Component {
758848
})
759849
}
760850

851+
initialHighlighting () {
852+
if (this.editor.options.linesHighlighted == null) {
853+
return
854+
}
855+
856+
const totalHighlightedLines = this.editor.options.linesHighlighted.length
857+
const totalAvailableLines = this.editor.lineCount()
858+
859+
for (let i = 0; i < totalHighlightedLines; i++) {
860+
const lineNumber = this.editor.options.linesHighlighted[i]
861+
if (lineNumber > totalAvailableLines) {
862+
// make sure that we skip the invalid lines althrough this case should not be happened.
863+
continue
864+
}
865+
this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background')
866+
}
867+
}
868+
869+
restartHighlighting () {
870+
this.editor.options.linesHighlighted = this.props.linesHighlighted
871+
this.initialHighlighting()
872+
}
873+
761874
mapImageResponse (response, pastedTxt) {
762875
return new Promise((resolve, reject) => {
763876
try {

browser/components/MarkdownEditor.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ class MarkdownEditor extends React.Component {
232232
}
233233

234234
render () {
235-
const {className, value, config, storageKey, noteKey} = this.props
235+
const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props
236236

237237
let editorFontSize = parseInt(config.editor.fontSize, 10)
238238
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -275,6 +275,7 @@ class MarkdownEditor extends React.Component {
275275
noteKey={noteKey}
276276
fetchUrlTitle={config.editor.fetchUrlTitle}
277277
enableTableEditor={config.editor.enableTableEditor}
278+
linesHighlighted={linesHighlighted}
278279
onChange={(e) => this.handleChange(e)}
279280
onBlur={(e) => this.handleBlur(e)}
280281
spellCheck={config.editor.spellcheck}

browser/components/MarkdownSplitEditor.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ class MarkdownSplitEditor extends React.Component {
2424
this.refs.code.setValue(value)
2525
}
2626

27-
handleOnChange () {
27+
handleOnChange (e) {
2828
this.value = this.refs.code.value
29-
this.props.onChange()
29+
this.props.onChange(e)
3030
}
3131

3232
handleScroll (e) {
@@ -136,7 +136,7 @@ class MarkdownSplitEditor extends React.Component {
136136
}
137137

138138
render () {
139-
const {config, value, storageKey, noteKey} = this.props
139+
const {config, value, storageKey, noteKey, linesHighlighted} = this.props
140140
const storage = findStorage(storageKey)
141141
let editorFontSize = parseInt(config.editor.fontSize, 10)
142142
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -169,7 +169,8 @@ class MarkdownSplitEditor extends React.Component {
169169
enableTableEditor={config.editor.enableTableEditor}
170170
storageKey={storageKey}
171171
noteKey={noteKey}
172-
onChange={this.handleOnChange.bind(this)}
172+
linesHighlighted={linesHighlighted}
173+
onChange={(e) => this.handleOnChange(e)}
173174
onScroll={this.handleScroll.bind(this)}
174175
spellCheck={config.editor.spellcheck}
175176
enableSmartPaste={config.editor.enableSmartPaste}

browser/lib/newNote.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
1818
folder: folder,
1919
title: '',
2020
tags,
21-
content: ''
21+
content: '',
22+
linesHighlighted: []
2223
})
2324
.then(note => {
2425
const noteHash = note.key
@@ -56,7 +57,8 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
5657
{
5758
name: '',
5859
mode: config.editor.snippetDefaultLanguage || 'text',
59-
content: ''
60+
content: '',
61+
linesHighlighted: []
6062
}
6163
]
6264
})

browser/main/Detail/MarkdownNoteDetail.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ class MarkdownNoteDetail extends React.Component {
3939
isMovingNote: false,
4040
note: Object.assign({
4141
title: '',
42-
content: ''
42+
content: '',
43+
linesHighlighted: []
4344
}, props.note),
4445
isLockButtonShown: false,
4546
isLocked: false,
4647
editorType: props.config.editor.type
4748
}
49+
4850
this.dispatchTimer = null
4951

5052
this.toggleLockButton = this.handleToggleLockButton.bind(this)
@@ -71,7 +73,7 @@ class MarkdownNoteDetail extends React.Component {
7173
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
7274
if (this.saveQueue != null) this.saveNow()
7375
this.setState({
74-
note: Object.assign({}, nextProps.note)
76+
note: Object.assign({linesHighlighted: []}, nextProps.note)
7577
}, () => {
7678
this.refs.content.reload()
7779
if (this.refs.tags) this.refs.tags.reset()
@@ -361,6 +363,7 @@ class MarkdownNoteDetail extends React.Component {
361363
value={note.content}
362364
storageKey={note.storage}
363365
noteKey={note.key}
366+
linesHighlighted={note.linesHighlighted}
364367
onChange={this.handleUpdateContent.bind(this)}
365368
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
366369
/>
@@ -371,6 +374,7 @@ class MarkdownNoteDetail extends React.Component {
371374
value={note.content}
372375
storageKey={note.storage}
373376
noteKey={note.key}
377+
linesHighlighted={note.linesHighlighted}
374378
onChange={this.handleUpdateContent.bind(this)}
375379
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
376380
/>

browser/main/Detail/SnippetNoteDetail.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class SnippetNoteDetail extends React.Component {
4848
note: Object.assign({
4949
description: ''
5050
}, props.note, {
51-
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
51+
snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
5252
})
5353
}
5454

@@ -76,8 +76,9 @@ class SnippetNoteDetail extends React.Component {
7676
const nextNote = Object.assign({
7777
description: ''
7878
}, nextProps.note, {
79-
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
79+
snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
8080
})
81+
8182
this.setState({
8283
snippetIndex: 0,
8384
note: nextNote
@@ -410,6 +411,8 @@ class SnippetNoteDetail extends React.Component {
410411
return (e) => {
411412
const snippets = this.state.note.snippets.slice()
412413
snippets[index].content = this.refs['code-' + index].value
414+
snippets[index].linesHighlighted = e.options.linesHighlighted
415+
413416
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
414417
this.setState(state => ({
415418
note: state.note
@@ -602,7 +605,8 @@ class SnippetNoteDetail extends React.Component {
602605
note.snippets = note.snippets.concat([{
603606
name: '',
604607
mode: config.editor.snippetDefaultLanguage || 'text',
605-
content: ''
608+
content: '',
609+
linesHighlighted: []
606610
}])
607611
const snippetIndex = note.snippets.length - 1
608612

@@ -692,10 +696,8 @@ class SnippetNoteDetail extends React.Component {
692696

693697
const viewList = note.snippets.map((snippet, index) => {
694698
const isActive = this.state.snippetIndex === index
695-
696699
let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
697700
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
698-
699701
return <div styleName='tabView'
700702
key={index}
701703
style={{zIndex: isActive ? 5 : 4}}
@@ -704,6 +706,7 @@ class SnippetNoteDetail extends React.Component {
704706
? <MarkdownEditor styleName='tabView-content'
705707
value={snippet.content}
706708
config={config}
709+
linesHighlighted={snippet.linesHighlighted}
707710
onChange={(e) => this.handleCodeChange(index)(e)}
708711
ref={'code-' + index}
709712
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
@@ -712,6 +715,7 @@ class SnippetNoteDetail extends React.Component {
712715
: <CodeEditor styleName='tabView-content'
713716
mode={snippet.mode}
714717
value={snippet.content}
718+
linesHighlighted={snippet.linesHighlighted}
715719
theme={config.editor.theme}
716720
fontFamily={config.editor.fontFamily}
717721
fontSize={editorFontSize}

browser/main/Main.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,14 @@ class Main extends React.Component {
9696
{
9797
name: 'example.html',
9898
mode: 'html',
99-
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
99+
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
100+
linesHighlighted: []
100101
},
101102
{
102103
name: 'example.js',
103104
mode: 'javascript',
104-
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
105+
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)",
106+
linesHighlighted: []
105107
}
106108
]
107109
})

browser/main/NoteList/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,8 @@ class NoteList extends React.Component {
711711
type: firstNote.type,
712712
folder: folder.key,
713713
title: firstNote.title + ' ' + i18n.__('copy'),
714-
content: firstNote.content
714+
content: firstNote.content,
715+
linesHighlighted: firstNote.linesHighlighted
715716
})
716717
.then((note) => {
717718
attachmentManagement.cloneAttachments(firstNote, note)

browser/main/lib/dataApi/createNote.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ function validateInput (input) {
1616
switch (input.type) {
1717
case 'MARKDOWN_NOTE':
1818
if (!_.isString(input.content)) input.content = ''
19+
if (!_.isArray(input.linesHighlighted)) input.linesHighlighted = []
1920
break
2021
case 'SNIPPET_NOTE':
2122
if (!_.isString(input.description)) input.description = ''
2223
if (!_.isArray(input.snippets)) {
2324
input.snippets = [{
2425
name: '',
2526
mode: 'text',
26-
content: ''
27+
content: '',
28+
linesHighlighted: []
2729
}]
2830
}
2931
break

browser/main/lib/dataApi/createSnippet.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ function createSnippet (snippetFile) {
99
id: crypto.randomBytes(16).toString('hex'),
1010
name: 'Unnamed snippet',
1111
prefix: [],
12-
content: ''
12+
content: '',
13+
linesHighlighted: []
1314
}
1415
fetchSnippet(null, snippetFile).then((snippets) => {
1516
snippets.push(newSnippet)

0 commit comments

Comments
 (0)