index.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import { EditorView, ViewPlugin } from '@codemirror/view'
  2. import {
  3. EditorState,
  4. StateEffect,
  5. StateField,
  6. TransactionSpec,
  7. } from '@codemirror/state'
  8. import { misspelledWordsField } from './misspelled-words'
  9. import {
  10. addIgnoredWord,
  11. ignoredWordsField,
  12. removeIgnoredWord,
  13. resetSpellChecker,
  14. updateAfterAddingIgnoredWord,
  15. } from './ignored-words'
  16. import { addWordToCache, cacheField, removeWordFromCache } from './cache'
  17. import { hideSpellingMenu, spellingMenuField } from './context-menu'
  18. import { SpellChecker } from './spellchecker'
  19. import { parserWatcher } from '../wait-for-parser'
  20. import type { HunspellManager } from '@/features/source-editor/hunspell/HunspellManager'
  21. import { debugConsole } from '@/utils/debugging'
  22. import { captureException } from '@/infrastructure/error-reporter'
  23. type Options = {
  24. spellCheckLanguage?: string
  25. hunspellManager?: HunspellManager
  26. }
  27. /**
  28. * A custom extension that creates a spell checker for the current language (from the user settings).
  29. * The spell check runs on the server whenever a line changes.
  30. * The mis-spelled words, ignored words and spell-checked words are stored in a state field.
  31. * Mis-spelled words are decorated with a Mark decoration.
  32. * The suggestions menu is displayed in a tooltip, activated with a right-click on the decoration.
  33. */
  34. export const spelling = ({ spellCheckLanguage, hunspellManager }: Options) => {
  35. return [
  36. spellingTheme,
  37. parserWatcher,
  38. spellCheckLanguageField.init(() => spellCheckLanguage),
  39. spellCheckerField.init(() =>
  40. spellCheckLanguage
  41. ? new SpellChecker(spellCheckLanguage, hunspellManager)
  42. : null
  43. ),
  44. misspelledWordsField,
  45. ignoredWordsField,
  46. cacheField,
  47. spellingMenuField,
  48. ]
  49. }
  50. const spellingTheme = EditorView.baseTheme({
  51. '.ol-cm-spelling-error': {
  52. textDecorationColor: 'red',
  53. textDecorationLine: 'underline',
  54. textDecorationStyle: 'dotted',
  55. textDecorationThickness: '2px',
  56. textDecorationSkipInk: 'none',
  57. textUnderlineOffset: '0.2em',
  58. },
  59. '.cm-tooltip.ol-cm-spelling-context-menu-tooltip': {
  60. borderWidth: '0',
  61. background: 'transparent',
  62. },
  63. })
  64. export const getSpellChecker = (state: EditorState) =>
  65. state.field(spellCheckerField, false)
  66. const spellCheckerField = StateField.define<SpellChecker | null>({
  67. create() {
  68. return null
  69. },
  70. update(value, tr) {
  71. for (const effect of tr.effects) {
  72. if (effect.is(setSpellCheckLanguageEffect)) {
  73. value?.destroy()
  74. value = effect.value.spellCheckLanguage
  75. ? new SpellChecker(
  76. effect.value.spellCheckLanguage,
  77. effect.value.hunspellManager
  78. )
  79. : null
  80. } else if (effect.is(addIgnoredWord)) {
  81. value?.addWord(effect.value.text).catch(error => {
  82. captureException(error)
  83. debugConsole.error(error)
  84. })
  85. } else if (effect.is(removeIgnoredWord)) {
  86. value?.removeWord(effect.value.text).catch(error => {
  87. captureException(error)
  88. debugConsole.error(error)
  89. })
  90. }
  91. }
  92. return value
  93. },
  94. provide(field) {
  95. return [
  96. ViewPlugin.define(view => {
  97. return {
  98. destroy: () => {
  99. view.state.field(field)?.destroy()
  100. },
  101. }
  102. }),
  103. EditorView.domEventHandlers({
  104. focus: (_event, view) => {
  105. if (view.state.facet(EditorView.editable)) {
  106. view.state.field(field)?.scheduleSpellCheck(view)
  107. }
  108. },
  109. }),
  110. EditorView.updateListener.of(update => {
  111. if (update.state.facet(EditorView.editable)) {
  112. update.state.field(field)?.handleUpdate(update)
  113. }
  114. }),
  115. ]
  116. },
  117. })
  118. export const getSpellCheckLanguage = (state: EditorState) =>
  119. state.field(spellCheckLanguageField, false)
  120. const spellCheckLanguageField = StateField.define<string | undefined>({
  121. create() {
  122. return undefined
  123. },
  124. update(value, tr) {
  125. for (const effect of tr.effects) {
  126. if (effect.is(setSpellCheckLanguageEffect)) {
  127. value = effect.value.spellCheckLanguage
  128. }
  129. }
  130. return value
  131. },
  132. })
  133. export const setSpellCheckLanguageEffect = StateEffect.define<{
  134. spellCheckLanguage: string | undefined
  135. hunspellManager?: HunspellManager
  136. }>()
  137. export const setSpellCheckLanguage = ({
  138. spellCheckLanguage,
  139. hunspellManager,
  140. }: Options): TransactionSpec => {
  141. return {
  142. effects: [
  143. setSpellCheckLanguageEffect.of({
  144. spellCheckLanguage,
  145. hunspellManager,
  146. }),
  147. hideSpellingMenu.of(null),
  148. ],
  149. }
  150. }
  151. export const addLearnedWord = (
  152. spellCheckLanguage: string,
  153. word: string
  154. ): TransactionSpec => {
  155. return {
  156. effects: [
  157. addWordToCache.of({
  158. lang: spellCheckLanguage,
  159. wordText: word,
  160. value: true,
  161. }),
  162. updateAfterAddingIgnoredWord.of(word),
  163. ],
  164. }
  165. }
  166. export const removeLearnedWord = (
  167. spellCheckLanguage: string,
  168. word: string
  169. ): TransactionSpec => {
  170. return {
  171. effects: [
  172. removeWordFromCache.of({
  173. lang: spellCheckLanguage,
  174. wordText: word,
  175. }),
  176. removeIgnoredWord.of({ text: word }),
  177. ],
  178. }
  179. }
  180. export const resetLearnedWords = (): TransactionSpec => {
  181. return {
  182. effects: [resetSpellChecker.of(null)],
  183. }
  184. }