Skip to content

Commit c76b653

Browse files
authored
Merge pull request #2338 from ehhc/spellchecker
Spellchecker
2 parents b703c42 + 49abfac commit c76b653

40 files changed

+219645
-27
lines changed

browser/components/CodeEditor.js

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ import eventEmitter from 'browser/main/lib/eventEmitter'
1111
import iconv from 'iconv-lite'
1212
import crypto from 'crypto'
1313
import consts from 'browser/lib/consts'
14+
import styles from '../components/CodeEditor.styl'
1415
import fs from 'fs'
15-
const { ipcRenderer } = require('electron')
16+
const { ipcRenderer, remote } = require('electron')
1617
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
18+
const spellcheck = require('browser/lib/spellcheck')
19+
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
1720
import TurndownService from 'turndown'
1821
import { gfm } from 'turndown-plugin-gfm'
1922

@@ -30,7 +33,7 @@ export default class CodeEditor extends React.Component {
3033
leading: false,
3134
trailing: true
3235
})
33-
this.changeHandler = e => this.handleChange(e)
36+
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
3437
this.focusHandler = () => {
3538
ipcRenderer.send('editor:focused', true)
3639
}
@@ -62,6 +65,12 @@ export default class CodeEditor extends React.Component {
6265
this.scrollToLineHandeler = this.scrollToLine.bind(this)
6366

6467
this.formatTable = () => this.handleFormatTable()
68+
this.contextMenuHandler = function (editor, event) {
69+
const menu = buildEditorContextMenu(editor, event)
70+
if (menu != null) {
71+
setTimeout(() => menu.popup(remote.getCurrentWindow()), 30)
72+
}
73+
}
6574
this.editorActivityHandler = () => this.handleEditorActivity()
6675

6776
this.turndownService = new TurndownService()
@@ -232,6 +241,7 @@ export default class CodeEditor extends React.Component {
232241
this.editor.on('blur', this.blurHandler)
233242
this.editor.on('change', this.changeHandler)
234243
this.editor.on('paste', this.pasteHandler)
244+
this.editor.on('contextmenu', this.contextMenuHandler)
235245
eventEmitter.on('top:search', this.searchHandler)
236246

237247
eventEmitter.emit('code:init')
@@ -248,6 +258,10 @@ export default class CodeEditor extends React.Component {
248258

249259
this.textEditorInterface = new TextEditorInterface(this.editor)
250260
this.tableEditor = new TableEditor(this.textEditorInterface)
261+
if (this.props.spellCheck) {
262+
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
263+
}
264+
251265
eventEmitter.on('code:format-table', this.formatTable)
252266

253267
this.tableEditorOptions = options({
@@ -395,9 +409,11 @@ export default class CodeEditor extends React.Component {
395409
this.editor.off('paste', this.pasteHandler)
396410
eventEmitter.off('top:search', this.searchHandler)
397411
this.editor.off('scroll', this.scrollHandler)
412+
this.editor.off('contextmenu', this.contextMenuHandler)
398413
const editorTheme = document.getElementById('editorTheme')
399414
editorTheme.removeEventListener('load', this.loadStyleHandler)
400415

416+
spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED)
401417
eventEmitter.off('code:format-table', this.formatTable)
402418
}
403419

@@ -465,6 +481,16 @@ export default class CodeEditor extends React.Component {
465481
needRefresh = true
466482
}
467483

484+
if (prevProps.spellCheck !== this.props.spellCheck) {
485+
if (this.props.spellCheck === false) {
486+
spellcheck.setLanguage(this.editor, spellcheck.SPELLCHECK_DISABLED)
487+
let elem = document.getElementById('editor-bottom-panel')
488+
elem.parentNode.removeChild(elem)
489+
} else {
490+
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
491+
}
492+
}
493+
468494
if (needRefresh) {
469495
this.editor.refresh()
470496
}
@@ -478,10 +504,11 @@ export default class CodeEditor extends React.Component {
478504
CodeMirror.autoLoadMode(this.editor, syntax.mode)
479505
}
480506

481-
handleChange (e) {
482-
this.value = this.editor.getValue()
507+
handleChange (editor, changeObject) {
508+
spellcheck.handleChange(editor, changeObject)
509+
this.value = editor.getValue()
483510
if (this.props.onChange) {
484-
this.props.onChange(e)
511+
this.props.onChange(editor)
485512
}
486513
}
487514

@@ -718,6 +745,25 @@ export default class CodeEditor extends React.Component {
718745
/>
719746
)
720747
}
748+
749+
createSpellCheckPanel () {
750+
const panel = document.createElement('div')
751+
panel.className = 'panel bottom'
752+
panel.id = 'editor-bottom-panel'
753+
const dropdown = document.createElement('select')
754+
dropdown.title = 'Spellcheck'
755+
dropdown.className = styles['spellcheck-select']
756+
dropdown.addEventListener('change', (e) => spellcheck.setLanguage(this.editor, dropdown.value))
757+
const options = spellcheck.getAvailableDictionaries()
758+
for (const op of options) {
759+
const option = document.createElement('option')
760+
option.value = op.value
761+
option.innerHTML = op.label
762+
dropdown.appendChild(option)
763+
}
764+
panel.appendChild(dropdown)
765+
return panel
766+
}
721767
}
722768

723769
CodeEditor.propTypes = {
@@ -728,7 +774,8 @@ CodeEditor.propTypes = {
728774
className: PropTypes.string,
729775
onBlur: PropTypes.func,
730776
onChange: PropTypes.func,
731-
readOnly: PropTypes.bool
777+
readOnly: PropTypes.bool,
778+
spellCheck: PropTypes.bool
732779
}
733780

734781
CodeEditor.defaultProps = {
@@ -738,5 +785,6 @@ CodeEditor.defaultProps = {
738785
fontSize: 14,
739786
fontFamily: 'Monaco, Consolas',
740787
indentSize: 4,
741-
indentType: 'space'
788+
indentType: 'space',
789+
spellCheck: false
742790
}

browser/components/CodeEditor.styl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.codeEditor-typo
2+
text-decoration underline wavy red
3+
4+
.spellcheck-select
5+
border: none
6+
text-decoration underline wavy red

browser/components/MarkdownEditor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ class MarkdownEditor extends React.Component {
275275
enableTableEditor={config.editor.enableTableEditor}
276276
onChange={(e) => this.handleChange(e)}
277277
onBlur={(e) => this.handleBlur(e)}
278+
spellCheck={config.editor.spellcheck}
278279
/>
279280
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
280281
? 'preview'

browser/components/MarkdownSplitEditor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ class MarkdownSplitEditor extends React.Component {
169169
noteKey={noteKey}
170170
onChange={this.handleOnChange.bind(this)}
171171
onScroll={this.handleScroll.bind(this)}
172+
spellCheck={config.editor.spellcheck}
172173
/>
173174
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
174175
<div styleName='slider-hitbox' />

browser/lib/contextMenuBuilder.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const {remote} = require('electron')
2+
const {Menu} = remote.require('electron')
3+
const spellcheck = require('./spellcheck')
4+
5+
/**
6+
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
7+
* If the word is does not contains a spelling error (determined by the 'error style'), no suggestions for corrections are requested
8+
* => they are not visible in the context menu
9+
* @param editor CodeMirror editor
10+
* @param {MouseEvent} event that has triggered the creation of the context menu
11+
* @returns {Electron.Menu} The created electron context menu
12+
*/
13+
const buildEditorContextMenu = function (editor, event) {
14+
if (editor == null || event == null || event.pageX == null || event.pageY == null) {
15+
return null
16+
}
17+
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY})
18+
const wordRange = editor.findWordAt(cursor)
19+
const word = editor.getRange(wordRange.anchor, wordRange.head)
20+
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
21+
let isMisspelled = false
22+
for (const mark of existingMarks) {
23+
if (mark.className === spellcheck.getCSSClassName()) {
24+
isMisspelled = true
25+
break
26+
}
27+
}
28+
let suggestion = []
29+
if (isMisspelled) {
30+
suggestion = spellcheck.getSpellingSuggestion(word)
31+
}
32+
33+
const selection = {
34+
isMisspelled: isMisspelled,
35+
spellingSuggestions: suggestion
36+
}
37+
const template = [{
38+
role: 'cut'
39+
}, {
40+
role: 'copy'
41+
}, {
42+
role: 'paste'
43+
}, {
44+
role: 'selectall'
45+
}]
46+
47+
if (selection.isMisspelled) {
48+
const suggestions = selection.spellingSuggestions
49+
template.unshift.apply(template, suggestions.map(function (suggestion) {
50+
return {
51+
label: suggestion,
52+
click: function (suggestion) {
53+
if (editor != null) {
54+
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head)
55+
}
56+
}
57+
}
58+
}).concat({
59+
type: 'separator'
60+
}))
61+
}
62+
return Menu.buildFromTemplate(template)
63+
}
64+
65+
module.exports = buildEditorContextMenu

0 commit comments

Comments
 (0)