index.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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 { removeLearnedWord } from './learned-words'
  10. import { cacheField } from './cache'
  11. import { hideSpellingMenu, spellingMenuField } from './context-menu'
  12. import { SpellChecker } from './spellchecker'
  13. import { parserWatcher } from '../wait-for-parser'
  14. import type { HunspellManager } from '@/features/source-editor/hunspell/HunspellManager'
  15. type Options = {
  16. spellCheckLanguage?: string
  17. hunspellManager?: HunspellManager
  18. }
  19. /**
  20. * A custom extension that creates a spell checker for the current language (from the user settings).
  21. * The spell check runs on the server whenever a line changes.
  22. * The mis-spelled words, ignored words and spell-checked words are stored in a state field.
  23. * Mis-spelled words are decorated with a Mark decoration.
  24. * The suggestions menu is displayed in a tooltip, activated with a right-click on the decoration.
  25. */
  26. export const spelling = ({ spellCheckLanguage, hunspellManager }: Options) => {
  27. return [
  28. spellingTheme,
  29. parserWatcher,
  30. spellCheckLanguageField.init(() => spellCheckLanguage),
  31. spellCheckerField.init(() =>
  32. spellCheckLanguage
  33. ? new SpellChecker(spellCheckLanguage, hunspellManager)
  34. : null
  35. ),
  36. misspelledWordsField,
  37. cacheField,
  38. spellingMenuField,
  39. dictionary,
  40. ]
  41. }
  42. const dictionary = ViewPlugin.define(view => {
  43. const listener = (event: Event) => {
  44. view.dispatch(removeLearnedWord((event as CustomEvent<string>).detail))
  45. }
  46. window.addEventListener('editor:remove-learned-word', listener)
  47. return {
  48. destroy() {
  49. window.removeEventListener('editor:remove-learned-word', listener)
  50. },
  51. }
  52. })
  53. const spellingTheme = EditorView.baseTheme({
  54. '.ol-cm-spelling-error': {
  55. textDecorationColor: 'red',
  56. textDecorationLine: 'underline',
  57. textDecorationStyle: 'dotted',
  58. textDecorationThickness: '2px',
  59. textDecorationSkipInk: 'none',
  60. textUnderlineOffset: '0.2em',
  61. },
  62. '.cm-tooltip.ol-cm-spelling-context-menu-tooltip': {
  63. borderWidth: '0',
  64. background: 'transparent',
  65. },
  66. })
  67. export const getSpellChecker = (state: EditorState) =>
  68. state.field(spellCheckerField, false)
  69. const spellCheckerField = StateField.define<SpellChecker | null>({
  70. create() {
  71. return null
  72. },
  73. update(value, tr) {
  74. for (const effect of tr.effects) {
  75. if (effect.is(setSpellCheckLanguageEffect)) {
  76. value?.destroy()
  77. value = effect.value.spellCheckLanguage
  78. ? new SpellChecker(
  79. effect.value.spellCheckLanguage,
  80. effect.value.hunspellManager
  81. )
  82. : null
  83. }
  84. }
  85. return value
  86. },
  87. provide(field) {
  88. return [
  89. ViewPlugin.define(view => {
  90. return {
  91. destroy: () => {
  92. view.state.field(field)?.destroy()
  93. },
  94. }
  95. }),
  96. EditorView.domEventHandlers({
  97. focus: (_event, view) => {
  98. if (view.state.facet(EditorView.editable)) {
  99. view.state.field(field)?.scheduleSpellCheck(view)
  100. }
  101. },
  102. }),
  103. EditorView.updateListener.of(update => {
  104. if (update.state.facet(EditorView.editable)) {
  105. update.state.field(field)?.handleUpdate(update)
  106. }
  107. }),
  108. ]
  109. },
  110. })
  111. export const getSpellCheckLanguage = (state: EditorState) =>
  112. state.field(spellCheckLanguageField, false)
  113. const spellCheckLanguageField = StateField.define<string | undefined>({
  114. create() {
  115. return undefined
  116. },
  117. update(value, tr) {
  118. for (const effect of tr.effects) {
  119. if (effect.is(setSpellCheckLanguageEffect)) {
  120. value = effect.value.spellCheckLanguage
  121. }
  122. }
  123. return value
  124. },
  125. })
  126. export const setSpellCheckLanguageEffect = StateEffect.define<{
  127. spellCheckLanguage: string | undefined
  128. hunspellManager?: HunspellManager
  129. }>()
  130. export const setSpellCheckLanguage = ({
  131. spellCheckLanguage,
  132. hunspellManager,
  133. }: Options): TransactionSpec => {
  134. return {
  135. effects: [
  136. setSpellCheckLanguageEffect.of({
  137. spellCheckLanguage,
  138. hunspellManager,
  139. }),
  140. hideSpellingMenu.of(null),
  141. ],
  142. }
  143. }