2018-07-07 00:48:14 +00:00
|
|
|
/* global require, process, _ */
|
|
|
|
|
|
|
|
/* eslint-disable strict */
|
|
|
|
|
|
|
|
const electron = require('electron');
|
|
|
|
|
|
|
|
const osLocale = require('os-locale');
|
|
|
|
const os = require('os');
|
|
|
|
const semver = require('semver');
|
|
|
|
const spellchecker = require('spellchecker');
|
|
|
|
|
|
|
|
const { remote, webFrame } = electron;
|
|
|
|
|
|
|
|
// `remote.require` since `Menu` is a main-process module.
|
|
|
|
const buildEditorContextMenu = remote.require('electron-editor-context-menu');
|
|
|
|
|
|
|
|
const EN_VARIANT = /^en/;
|
|
|
|
|
|
|
|
// Prevent the spellchecker from showing contractions as errors.
|
|
|
|
const ENGLISH_SKIP_WORDS = [
|
|
|
|
'ain',
|
|
|
|
'couldn',
|
|
|
|
'didn',
|
|
|
|
'doesn',
|
|
|
|
'hadn',
|
|
|
|
'hasn',
|
|
|
|
'mightn',
|
|
|
|
'mustn',
|
|
|
|
'needn',
|
|
|
|
'oughtn',
|
|
|
|
'shan',
|
|
|
|
'shouldn',
|
|
|
|
'wasn',
|
|
|
|
'weren',
|
|
|
|
'wouldn',
|
|
|
|
];
|
|
|
|
|
|
|
|
function setupLinux(locale) {
|
|
|
|
if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') {
|
|
|
|
// apt-get install hunspell-<locale> can be run for easy access
|
|
|
|
// to other dictionaries
|
|
|
|
const location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell';
|
|
|
|
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info(
|
2018-07-07 00:48:14 +00:00
|
|
|
'Detected Linux. Setting up spell check with locale',
|
|
|
|
locale,
|
|
|
|
'and dictionary location',
|
|
|
|
location
|
|
|
|
);
|
|
|
|
spellchecker.setDictionary(locale, location);
|
|
|
|
} else {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info(
|
|
|
|
'Detected Linux. Using default en_US spell check dictionary'
|
|
|
|
);
|
2017-08-17 01:09:50 +00:00
|
|
|
}
|
2018-07-07 00:48:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function setupWin7AndEarlier(locale) {
|
|
|
|
if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') {
|
|
|
|
const location = process.env.HUNSPELL_DICTIONARIES;
|
|
|
|
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info(
|
2018-07-07 00:48:14 +00:00
|
|
|
'Detected Windows 7 or below. Setting up spell-check with locale',
|
|
|
|
locale,
|
|
|
|
'and dictionary location',
|
|
|
|
location
|
|
|
|
);
|
|
|
|
spellchecker.setDictionary(locale, location);
|
|
|
|
} else {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info(
|
2018-07-07 00:48:14 +00:00
|
|
|
'Detected Windows 7 or below. Using default en_US spell check dictionary'
|
|
|
|
);
|
2017-08-17 01:09:50 +00:00
|
|
|
}
|
2018-07-07 00:48:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// We load locale this way and not via app.getLocale() because this call returns
|
|
|
|
// 'es_ES' and not just 'es.' And hunspell requires the fully-qualified locale.
|
|
|
|
const locale = osLocale.sync().replace('-', '_');
|
|
|
|
|
|
|
|
// The LANG environment variable is how node spellchecker finds its default language:
|
|
|
|
// https://github.com/atom/node-spellchecker/blob/59d2d5eee5785c4b34e9669cd5d987181d17c098/lib/spellchecker.js#L29
|
|
|
|
if (!process.env.LANG) {
|
|
|
|
process.env.LANG = locale;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (process.platform === 'linux') {
|
|
|
|
setupLinux(locale);
|
|
|
|
} else if (process.platform === 'windows' && semver.lt(os.release(), '8.0.0')) {
|
|
|
|
setupWin7AndEarlier(locale);
|
|
|
|
} else {
|
|
|
|
// OSX and Windows 8+ have OS-level spellcheck APIs
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info(
|
|
|
|
'Using OS-level spell check API with locale',
|
|
|
|
process.env.LANG
|
|
|
|
);
|
2018-07-07 00:48:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const simpleChecker = {
|
2019-08-22 21:11:36 +00:00
|
|
|
spellCheck(words, callback) {
|
|
|
|
const mispelled = words.filter(word => this.isMisspelled(word));
|
|
|
|
callback(mispelled);
|
2018-07-07 00:48:14 +00:00
|
|
|
},
|
2019-08-22 21:11:36 +00:00
|
|
|
isMisspelled(word) {
|
|
|
|
const misspelled = spellchecker.isMisspelled(word);
|
2018-07-07 00:48:14 +00:00
|
|
|
|
|
|
|
// The idea is to make this as fast as possible. For the many, many calls which
|
|
|
|
// don't result in the red squiggly, we minimize the number of checks.
|
|
|
|
if (!misspelled) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-08-17 01:09:50 +00:00
|
|
|
|
2018-07-07 00:48:14 +00:00
|
|
|
// Only if we think we've found an error do we check the locale and skip list.
|
2019-08-22 21:11:36 +00:00
|
|
|
if (locale.match(EN_VARIANT) && _.contains(ENGLISH_SKIP_WORDS, word)) {
|
2018-07-07 00:48:14 +00:00
|
|
|
return false;
|
|
|
|
}
|
2017-08-17 01:09:50 +00:00
|
|
|
|
2018-07-07 00:48:14 +00:00
|
|
|
return true;
|
|
|
|
},
|
|
|
|
getSuggestions(text) {
|
|
|
|
return spellchecker.getCorrectionsForMisspelling(text);
|
|
|
|
},
|
2019-08-22 21:11:36 +00:00
|
|
|
add(word) {
|
|
|
|
spellchecker.add(word);
|
2018-07-07 00:48:14 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2018-07-19 01:46:12 +00:00
|
|
|
const dummyChecker = {
|
2019-08-22 21:11:36 +00:00
|
|
|
spellCheck(words, callback) {
|
|
|
|
callback([]);
|
2018-07-19 01:46:12 +00:00
|
|
|
},
|
|
|
|
isMisspelled() {
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
getSuggestions() {
|
|
|
|
return [];
|
|
|
|
},
|
|
|
|
add() {
|
|
|
|
// nothing
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2018-07-07 00:48:14 +00:00
|
|
|
window.spellChecker = simpleChecker;
|
2018-07-19 01:46:12 +00:00
|
|
|
window.disableSpellCheck = () => {
|
|
|
|
window.removeEventListener('contextmenu', spellCheckHandler);
|
2019-08-19 17:59:30 +00:00
|
|
|
webFrame.setSpellCheckProvider('en-US', dummyChecker);
|
2018-07-19 01:46:12 +00:00
|
|
|
};
|
2018-07-07 00:48:14 +00:00
|
|
|
|
2018-07-19 01:46:12 +00:00
|
|
|
window.enableSpellCheck = () => {
|
2019-08-19 17:59:30 +00:00
|
|
|
webFrame.setSpellCheckProvider('en-US', simpleChecker);
|
2018-07-19 01:46:12 +00:00
|
|
|
window.addEventListener('contextmenu', spellCheckHandler);
|
|
|
|
};
|
2018-07-07 00:48:14 +00:00
|
|
|
|
2018-07-19 01:46:12 +00:00
|
|
|
const spellCheckHandler = e => {
|
2018-07-07 00:48:14 +00:00
|
|
|
// Only show the context menu in text editors.
|
|
|
|
if (!e.target.closest('textarea, input, [contenteditable="true"]')) {
|
|
|
|
return;
|
2017-08-17 01:09:50 +00:00
|
|
|
}
|
|
|
|
|
2018-07-07 00:48:14 +00:00
|
|
|
const selectedText = window.getSelection().toString();
|
|
|
|
const isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText);
|
|
|
|
const spellingSuggestions =
|
|
|
|
isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5);
|
|
|
|
const menu = buildEditorContextMenu({
|
|
|
|
isMisspelled,
|
|
|
|
spellingSuggestions,
|
2018-04-27 21:25:04 +00:00
|
|
|
});
|
2017-08-17 01:09:50 +00:00
|
|
|
|
2018-07-07 00:48:14 +00:00
|
|
|
// The 'contextmenu' event is emitted after 'selectionchange' has fired
|
|
|
|
// but possibly before the visible selection has changed. Try to wait
|
|
|
|
// to show the menu until after that, otherwise the visible selection
|
|
|
|
// will update after the menu dismisses and look weird.
|
|
|
|
setTimeout(() => {
|
|
|
|
menu.popup(remote.getCurrentWindow());
|
|
|
|
}, 30);
|
2018-07-19 01:46:12 +00:00
|
|
|
};
|