visual.ts 5.8 KB

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