spell-check-view.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. let SpellCheckView;
  2. const _ = require('underscore-plus');
  3. const { CompositeDisposable } = require('atom');
  4. const SpellCheckTask = require('./spell-check-task');
  5. let CorrectionsView = null;
  6. module.exports = SpellCheckView = class SpellCheckView {
  7. constructor(editor, spellCheckModule, manager) {
  8. this.addContextMenuEntries = this.addContextMenuEntries.bind(this);
  9. this.makeCorrection = this.makeCorrection.bind(this);
  10. this.editor = editor;
  11. this.spellCheckModule = spellCheckModule;
  12. this.manager = manager;
  13. this.disposables = new CompositeDisposable();
  14. this.initializeMarkerLayer();
  15. this.taskWrapper = new SpellCheckTask(this.manager);
  16. this.correctMisspellingCommand = atom.commands.add(
  17. atom.views.getView(this.editor),
  18. 'spell-check:correct-misspelling',
  19. () => {
  20. let marker;
  21. if (
  22. (marker = this.markerLayer.findMarkers({
  23. containsBufferPosition: this.editor.getCursorBufferPosition(),
  24. })[0])
  25. ) {
  26. if (CorrectionsView == null) {
  27. CorrectionsView = require('./corrections-view');
  28. }
  29. if (this.correctionsView != null) {
  30. this.correctionsView.destroy();
  31. }
  32. this.correctionsView = new CorrectionsView(
  33. this.editor,
  34. this.getCorrections(marker),
  35. marker,
  36. this,
  37. this.updateMisspellings
  38. );
  39. return this.correctionsView.attach();
  40. }
  41. }
  42. );
  43. atom.views
  44. .getView(this.editor)
  45. .addEventListener('contextmenu', this.addContextMenuEntries);
  46. this.disposables.add(
  47. this.editor.onDidChangePath(() => {
  48. return this.subscribeToBuffer();
  49. })
  50. );
  51. this.disposables.add(
  52. this.editor.onDidChangeGrammar(() => {
  53. return this.subscribeToBuffer();
  54. })
  55. );
  56. this.disposables.add(
  57. atom.config.onDidChange('editor.fontSize', () => {
  58. return this.subscribeToBuffer();
  59. })
  60. );
  61. this.disposables.add(
  62. atom.config.onDidChange('spell-check.grammars', () => {
  63. return this.subscribeToBuffer();
  64. })
  65. );
  66. this.subscribeToBuffer();
  67. this.disposables.add(this.editor.onDidDestroy(this.destroy.bind(this)));
  68. }
  69. initializeMarkerLayer() {
  70. this.markerLayer = this.editor.addMarkerLayer({
  71. maintainHistory: false,
  72. });
  73. return (this.markerLayerDecoration = this.editor.decorateMarkerLayer(
  74. this.markerLayer,
  75. {
  76. type: 'highlight',
  77. class: 'spell-check-misspelling',
  78. deprecatedRegionClass: 'misspelling',
  79. }
  80. ));
  81. }
  82. destroy() {
  83. this.unsubscribeFromBuffer();
  84. this.disposables.dispose();
  85. this.taskWrapper.terminate();
  86. this.markerLayer.destroy();
  87. this.markerLayerDecoration.destroy();
  88. this.correctMisspellingCommand.dispose();
  89. if (this.correctionsView != null) {
  90. this.correctionsView.destroy();
  91. }
  92. return this.clearContextMenuEntries();
  93. }
  94. unsubscribeFromBuffer() {
  95. this.destroyMarkers();
  96. if (this.buffer != null) {
  97. this.bufferDisposable.dispose();
  98. return (this.buffer = null);
  99. }
  100. }
  101. subscribeToBuffer() {
  102. this.unsubscribeFromBuffer();
  103. if (this.spellCheckCurrentGrammar()) {
  104. this.buffer = this.editor.getBuffer();
  105. this.bufferDisposable = new CompositeDisposable(
  106. this.buffer.onDidStopChanging(
  107. () => this.updateMisspellings(),
  108. this.editor.onDidTokenize(() => this.updateMisspellings())
  109. )
  110. );
  111. return this.updateMisspellings();
  112. }
  113. }
  114. spellCheckCurrentGrammar() {
  115. const grammar = this.editor.getGrammar().scopeName;
  116. return _.contains(atom.config.get('spell-check.grammars'), grammar);
  117. }
  118. destroyMarkers() {
  119. this.markerLayer.destroy();
  120. this.markerLayerDecoration.destroy();
  121. return this.initializeMarkerLayer();
  122. }
  123. addMarkers(misspellings) {
  124. return (() => {
  125. const result = [];
  126. for (let misspelling of misspellings) {
  127. const scope = this.editor.scopeDescriptorForBufferPosition(
  128. misspelling[0]
  129. );
  130. if (!this.scopeIsExcluded(scope)) {
  131. result.push(
  132. this.markerLayer.markBufferRange(misspelling, {
  133. invalidate: 'touch',
  134. })
  135. );
  136. } else {
  137. result.push(undefined);
  138. }
  139. }
  140. return result;
  141. })();
  142. }
  143. updateMisspellings() {
  144. return this.taskWrapper.start(this.editor, (misspellings) => {
  145. this.destroyMarkers();
  146. if (this.buffer != null) {
  147. return this.addMarkers(misspellings);
  148. }
  149. });
  150. }
  151. getCorrections(marker) {
  152. // Build up the arguments object for this buffer and text.
  153. let projectPath = null;
  154. let relativePath = null;
  155. if (this.buffer != null && this.buffer.file && this.buffer.file.path) {
  156. [projectPath, relativePath] = atom.project.relativizePath(
  157. this.buffer.file.path
  158. );
  159. }
  160. const args = {
  161. projectPath,
  162. relativePath,
  163. };
  164. // Get the misspelled word and then request corrections.
  165. const misspelling = this.editor.getTextInBufferRange(
  166. marker.getBufferRange()
  167. );
  168. return this.manager.suggest(args, misspelling);
  169. }
  170. addContextMenuEntries(mouseEvent) {
  171. let marker;
  172. this.clearContextMenuEntries();
  173. // Get buffer position of the right click event. If the click happens outside
  174. // the boundaries of any text, the method defaults to the buffer position of
  175. // the last character in the editor.
  176. const currentScreenPosition = atom.views
  177. .getView(this.editor)
  178. .component.screenPositionForMouseEvent(mouseEvent);
  179. const currentBufferPosition = this.editor.bufferPositionForScreenPosition(
  180. currentScreenPosition
  181. );
  182. // Check to see if the selected word is incorrect.
  183. if (
  184. (marker = this.markerLayer.findMarkers({
  185. containsBufferPosition: currentBufferPosition,
  186. })[0])
  187. ) {
  188. const corrections = this.getCorrections(marker);
  189. if (corrections.length > 0) {
  190. this.spellCheckModule.contextMenuEntries.push({
  191. menuItem: atom.contextMenu.add({
  192. 'atom-text-editor': [{ type: 'separator' }],
  193. }),
  194. });
  195. let correctionIndex = 0;
  196. for (let correction of corrections) {
  197. const contextMenuEntry = {};
  198. // Register new command for correction.
  199. var commandName =
  200. 'spell-check:correct-misspelling-' + correctionIndex;
  201. contextMenuEntry.command = ((
  202. correction,
  203. contextMenuEntry
  204. ) => {
  205. return atom.commands.add(
  206. atom.views.getView(this.editor),
  207. commandName,
  208. () => {
  209. this.makeCorrection(correction, marker);
  210. return this.clearContextMenuEntries();
  211. }
  212. );
  213. })(correction, contextMenuEntry);
  214. // Add new menu item for correction.
  215. contextMenuEntry.menuItem = atom.contextMenu.add({
  216. 'atom-text-editor': [
  217. { label: correction.label, command: commandName },
  218. ],
  219. });
  220. this.spellCheckModule.contextMenuEntries.push(
  221. contextMenuEntry
  222. );
  223. correctionIndex++;
  224. }
  225. return this.spellCheckModule.contextMenuEntries.push({
  226. menuItem: atom.contextMenu.add({
  227. 'atom-text-editor': [{ type: 'separator' }],
  228. }),
  229. });
  230. }
  231. }
  232. }
  233. makeCorrection(correction, marker) {
  234. if (correction.isSuggestion) {
  235. // Update the buffer with the correction.
  236. this.editor.setSelectedBufferRange(marker.getBufferRange());
  237. return this.editor.insertText(correction.suggestion);
  238. } else {
  239. // Build up the arguments object for this buffer and text.
  240. let projectPath = null;
  241. let relativePath = null;
  242. if (
  243. this.editor.buffer != null &&
  244. this.editor.buffer.file &&
  245. this.editor.buffer.file.path
  246. ) {
  247. [projectPath, relativePath] = atom.project.relativizePath(
  248. this.editor.buffer.file.path
  249. );
  250. }
  251. const args = {
  252. id: this.id,
  253. projectPath,
  254. relativePath,
  255. };
  256. // Send the "add" request to the plugin.
  257. correction.plugin.add(args, correction);
  258. // Update the buffer to handle the corrections.
  259. return this.updateMisspellings.bind(this)();
  260. }
  261. }
  262. clearContextMenuEntries() {
  263. for (let entry of this.spellCheckModule.contextMenuEntries) {
  264. if (entry.command != null) {
  265. entry.command.dispose();
  266. }
  267. if (entry.menuItem != null) {
  268. entry.menuItem.dispose();
  269. }
  270. }
  271. return (this.spellCheckModule.contextMenuEntries = []);
  272. }
  273. scopeIsExcluded(scopeDescriptor, excludedScopes) {
  274. return this.spellCheckModule.excludedScopeRegexLists.some((regexList) =>
  275. scopeDescriptor.scopes.some((scopeName) =>
  276. regexList.every((regex) => regex.test(scopeName))
  277. )
  278. );
  279. }
  280. };