visual.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import {
  2. Compartment,
  3. EditorState,
  4. Extension,
  5. StateEffect,
  6. StateField,
  7. TransactionSpec,
  8. } from '@codemirror/state'
  9. import { visualHighlightStyle, visualTheme } from './visual-theme'
  10. import { atomicDecorations } from './atomic-decorations'
  11. import { markDecorations } from './mark-decorations'
  12. import { EditorView, ViewPlugin } from '@codemirror/view'
  13. import { visualKeymap } from './visual-keymap'
  14. import { mousedown, mouseDownEffect } from './selection'
  15. import { forceParsing, syntaxTree } from '@codemirror/language'
  16. import { hasLanguageLoadedEffect } from '../language'
  17. import { restoreScrollPosition } from '../scroll-position'
  18. import { listItemMarker } from './list-item-marker'
  19. import { pasteHtml } from './paste-html'
  20. import { commandTooltip } from '../command-tooltip'
  21. import { tableGeneratorTheme } from './table-generator'
  22. import { debugConsole } from '@/utils/debugging'
  23. import { PreviewPath } from '../../../../../../types/preview-path'
  24. type Options = {
  25. visual: boolean
  26. previewByPath: (path: string) => PreviewPath | null
  27. }
  28. const visualConf = new Compartment()
  29. export const toggleVisualEffect = StateEffect.define<boolean>()
  30. const visualState = StateField.define<boolean>({
  31. create() {
  32. return false
  33. },
  34. update(value, tr) {
  35. for (const effect of tr.effects) {
  36. if (effect.is(toggleVisualEffect)) {
  37. return effect.value
  38. }
  39. }
  40. return value
  41. },
  42. })
  43. const configureVisualExtensions = (options: Options) =>
  44. options.visual ? extension(options) : []
  45. export const visual = (options: Options): Extension => {
  46. return [
  47. visualState.init(() => options.visual),
  48. visualConf.of(configureVisualExtensions(options)),
  49. ]
  50. }
  51. export const isVisual = (view: EditorView) => {
  52. return view.state.field(visualState, false) || false
  53. }
  54. export const setVisual = (options: Options): TransactionSpec => {
  55. return {
  56. effects: [
  57. toggleVisualEffect.of(options.visual),
  58. visualConf.reconfigure(configureVisualExtensions(options)),
  59. ],
  60. }
  61. }
  62. export const sourceOnly = (visual: boolean, extension: Extension) => {
  63. const conf = new Compartment()
  64. const configure = (visual: boolean) => (visual ? [] : extension)
  65. return [
  66. conf.of(configure(visual)),
  67. // Respond to switching editor modes
  68. EditorState.transactionExtender.of(tr => {
  69. for (const effect of tr.effects) {
  70. if (effect.is(toggleVisualEffect)) {
  71. return {
  72. effects: conf.reconfigure(configure(effect.value)),
  73. }
  74. }
  75. }
  76. return null
  77. }),
  78. ]
  79. }
  80. const parsedAttributesConf = new Compartment()
  81. /**
  82. * A view plugin which shows the editor content, makes it focusable,
  83. * and restores the scroll position, once the initial decorations have been applied.
  84. */
  85. const showContentWhenParsed = [
  86. parsedAttributesConf.of([EditorView.editable.of(false)]),
  87. ViewPlugin.define(view => {
  88. const showContent = () => {
  89. view.dispatch(
  90. {
  91. effects: parsedAttributesConf.reconfigure([
  92. EditorView.editorAttributes.of({
  93. class: 'ol-cm-parsed',
  94. }),
  95. EditorView.editable.of(true),
  96. ]),
  97. },
  98. restoreScrollPosition()
  99. )
  100. view.focus()
  101. }
  102. // already parsed
  103. if (syntaxTree(view.state).length === view.state.doc.length) {
  104. window.setTimeout(showContent)
  105. return {}
  106. }
  107. // as a fallback, make sure the content is visible after 5s
  108. const fallbackTimer = window.setTimeout(showContent, 5000)
  109. let languageLoaded = false
  110. return {
  111. update(update) {
  112. // wait for the language to load before telling the parser to run
  113. if (!languageLoaded && hasLanguageLoadedEffect(update)) {
  114. languageLoaded = true
  115. // in a timeout, as this is already in a dispatch cycle
  116. window.setTimeout(() => {
  117. // run asynchronously
  118. new Promise(() => {
  119. // tell the parser to run until the end of the document
  120. forceParsing(view, view.state.doc.length, Infinity)
  121. // clear the fallback timeout
  122. window.clearTimeout(fallbackTimer)
  123. // show the content, in a timeout so the decorations can build first
  124. window.setTimeout(showContent)
  125. }).catch(debugConsole.error)
  126. })
  127. }
  128. },
  129. }
  130. }),
  131. ]
  132. /**
  133. * A transaction extender which scrolls mouse clicks into view, in case decorations have moved the cursor out of view.
  134. */
  135. const scrollJumpAdjuster = EditorState.transactionExtender.of(tr => {
  136. // Attach a "scrollIntoView" effect on all mouse selections to adjust for
  137. // any jumps that may occur when hiding/showing decorations.
  138. if (!tr.scrollIntoView) {
  139. for (const effect of tr.effects) {
  140. if (effect.is(mouseDownEffect) && effect.value === false) {
  141. return {
  142. effects: EditorView.scrollIntoView(tr.newSelection.main.head),
  143. }
  144. }
  145. }
  146. }
  147. return {}
  148. })
  149. const extension = (options: Options) => [
  150. visualTheme,
  151. visualHighlightStyle,
  152. mousedown,
  153. listItemMarker,
  154. atomicDecorations(options),
  155. markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
  156. visualKeymap,
  157. commandTooltip,
  158. scrollJumpAdjuster,
  159. showContentWhenParsed,
  160. pasteHtml,
  161. tableGeneratorTheme,
  162. ]