visual.ts 6.0 KB

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