From e8c7e3136328784d8596174eb2d00df564029095 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Wed, 16 Aug 2017 18:09:50 -0700 Subject: [PATCH] Multi-error, multi-language, and cross-platform spell-check FREEBIE --- js/spell_check.js | 134 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + preload.js | 64 ++-------------------- 3 files changed, 138 insertions(+), 61 deletions(-) create mode 100644 js/spell_check.js diff --git a/js/spell_check.js b/js/spell_check.js new file mode 100644 index 000000000..b3db6a217 --- /dev/null +++ b/js/spell_check.js @@ -0,0 +1,134 @@ +(function () { + var electron = require('electron'); + var remote = electron.remote; + var app = remote.app; + var webFrame = electron.webFrame; + var path = require('path'); + + var osLocale = require('os-locale'); + var os = require('os'); + var semver = require('semver'); + var spellchecker = require('spellchecker'); + + // `remote.require` since `Menu` is a main-process module. + var buildEditorContextMenu = remote.require('electron-editor-context-menu'); + + var EN_VARIANT = /^en/; + + // Prevent the spellchecker from showing contractions as errors. + var 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- can be run for easy access to other dictionaries + var location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell'; + + console.log('Detected Linux. Setting up spell check with locale', locale, 'and dictionary location', location); + spellchecker.setDictionary(locale, location); + } else { + console.log('Detected Linux. Using default en_US spell check dictionary'); + } + } + + function setupWin7AndEarlier(locale) { + if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') { + var location = process.env.HUNSPELL_DICTIONARIES; + + console.log('Detected Windows 7 or below. Setting up spell-check with locale', locale, 'and dictionary location', location); + spellchecker.setDictionary(locale, location); + } else { + console.log('Detected Windows 7 or below. Using default en_US spell check dictionary'); + } + } + + var 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 + console.log('Using OS-level spell check API with locale', process.env.LANG); + } + + var simpleChecker = window.spellChecker = { + spellCheck: function(text) { + return !this.isMisspelled(text); + }, + isMisspelled: function(text) { + var misspelled = spellchecker.isMisspelled(text); + + // 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; + } + + // Only if we think we've found an error do we check the locale and skip list. + if (locale.match(EN_VARIANT) && _.contains(ENGLISH_SKIP_WORDS, text)) { + return false; + } + + return true; + }, + getSuggestions: function(text) { + return spellchecker.getCorrectionsForMisspelling(text); + }, + add: function(text) { + spellchecker.add(text); + } + }; + + webFrame.setSpellCheckProvider( + 'en-US', + // Not sure what this parameter (`autoCorrectWord`) does: https://github.com/atom/electron/issues/4371 + // The documentation for `webFrame.setSpellCheckProvider` passes `true` so we do too. + true, + simpleChecker + ); + + window.addEventListener('contextmenu', function(e) { + // Only show the context menu in text editors. + if (!e.target.closest('textarea, input, [contenteditable="true"]')) { + return; + } + + var selectedText = window.getSelection().toString(); + var isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText); + var spellingSuggestions = isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5); + var menu = buildEditorContextMenu({ + isMisspelled: isMisspelled, + spellingSuggestions: spellingSuggestions, + }); + + // 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(function() { + menu.popup(remote.getCurrentWindow()); + }, 30); + }); +})(); diff --git a/package.json b/package.json index 9a6011944..cf353870b 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "images/**", "fonts/*", "node_modules/**", + "!node_modules/spellchecker/vendor/hunspell/**/*", "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts}", "!**/node_modules/.bin", "!**/node_modules/*/build/**", diff --git a/preload.js b/preload.js index aa1adc8dc..4d5940579 100644 --- a/preload.js +++ b/preload.js @@ -2,7 +2,7 @@ 'use strict'; console.log('preload'); - const electron = require('electron') + const electron = require('electron'); window.PROTO_ROOT = 'protos'; window.config = require('url').parse(window.location.toString(), true).query; @@ -28,68 +28,10 @@ ipc.on('debug-log', function() { Whisper.events.trigger('showDebugLog'); }); - /** - * Enables spell-checking and the right-click context menu in text editors. - * Electron (`webFrame.setSpellCheckProvider`) only underlines misspelled words; - * we must manage the menu ourselves. - * - * Run this in the renderer process. - */ - var remote = electron.remote; - var webFrame = electron.webFrame; - var SpellCheckProvider = require('electron-spell-check-provider'); - // `remote.require` since `Menu` is a main-process module. - var buildEditorContextMenu = remote.require('electron-editor-context-menu'); - var selection; - function resetSelection() { - selection = { - isMisspelled: false, - spellingSuggestions: [] - }; - } - resetSelection(); + // We pull these dependencies in now, from here, because they have Node.js dependencies - window.spellChecker = new SpellCheckProvider(window.config.locale).on('misspelling', function(suggestions) { - // Prime the context menu with spelling suggestions _if_ the user has selected text. Electron - // may sometimes re-run the spell-check provider for an outdated selection e.g. if the user - // right-clicks some misspelled text and then an image. - if (window.getSelection().toString()) { - selection.isMisspelled = true; - // Take the first three suggestions if any. - selection.spellingSuggestions = suggestions.slice(0, 3); - } - }); - - // Reset the selection when clicking around, before the spell-checker runs and the context menu shows. - window.addEventListener('mousedown', resetSelection); - - // The spell-checker runs when the user clicks on text and before the 'contextmenu' event fires. - // Thus, we may retrieve spell-checking suggestions to put in the menu just before it shows. - - webFrame.setSpellCheckProvider( - 'en-US', - // Not sure what this parameter (`autoCorrectWord`) does: https://github.com/atom/electron/issues/4371 - // The documentation for `webFrame.setSpellCheckProvider` passes `true` so we do too. - true, - spellChecker - ); - - window.addEventListener('contextmenu', function(e) { - // Only show the context menu in text editors. - if (!e.target.closest('textarea, input, [contenteditable="true"]')) return; - - var menu = buildEditorContextMenu(selection); - - // 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(function() { - menu.popup(remote.getCurrentWindow()); - }, 30); - }); - - // we have to pull this in this way because it references node APIs + require('./js/spell_check'); require('./js/backup'); })();