cursor-position.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import {
  2. EditorSelection,
  3. EditorState,
  4. SelectionRange,
  5. Text,
  6. TransactionSpec,
  7. } from '@codemirror/state'
  8. import { EditorView, ViewPlugin } from '@codemirror/view'
  9. import { findValidPosition } from '../utils/position'
  10. import customLocalStorage from '../../../infrastructure/local-storage'
  11. const buildStorageKey = (docId: string) => `doc.position.${docId}`
  12. export const cursorPosition = ({
  13. currentDoc: { doc_id: docId },
  14. }: {
  15. currentDoc: { doc_id: string }
  16. }) => {
  17. return [
  18. // store cursor position
  19. ViewPlugin.define(view => {
  20. const unloadListener = () => {
  21. storeCursorPosition(view, docId)
  22. }
  23. window.addEventListener('unload', unloadListener)
  24. return {
  25. destroy: () => {
  26. window.removeEventListener('unload', unloadListener)
  27. unloadListener()
  28. },
  29. }
  30. }),
  31. // Asynchronously dispatch cursor position when the selection changes and
  32. // provide a little debouncing. Using requestAnimationFrame postpones it
  33. // until the next CM6 DOM update.
  34. ViewPlugin.define(view => {
  35. let animationFrameRequest: number | null = null
  36. return {
  37. update(update) {
  38. if (update.selectionSet || update.docChanged) {
  39. if (animationFrameRequest) {
  40. window.cancelAnimationFrame(animationFrameRequest)
  41. }
  42. animationFrameRequest = window.requestAnimationFrame(() => {
  43. animationFrameRequest = null
  44. dispatchCursorPosition(update.state)
  45. })
  46. }
  47. },
  48. }
  49. }),
  50. ]
  51. }
  52. // convert the selection head to a row and column
  53. const buildCursorPosition = (state: EditorState) => {
  54. const pos = state.selection.main.head
  55. const line = state.doc.lineAt(pos)
  56. const row = line.number - 1 // 0-indexed
  57. const column = pos - line.from
  58. return { row, column }
  59. }
  60. // dispatch the current cursor position for use with synctex
  61. const dispatchCursorPosition = (state: EditorState) => {
  62. const cursorPosition = buildCursorPosition(state)
  63. window.dispatchEvent(
  64. new CustomEvent('cursor:editor:update', { detail: cursorPosition })
  65. )
  66. }
  67. // store the cursor position for restoring on load
  68. const storeCursorPosition = (view: EditorView, docId: string) => {
  69. const key = buildStorageKey(docId)
  70. const data = customLocalStorage.getItem(key)
  71. const cursorPosition = buildCursorPosition(view.state)
  72. customLocalStorage.setItem(key, { ...data, cursorPosition })
  73. }
  74. // restore the stored cursor position on load
  75. export const restoreCursorPosition = (
  76. doc: Text,
  77. docId: string
  78. ): TransactionSpec => {
  79. try {
  80. const key = buildStorageKey(docId)
  81. const data = customLocalStorage.getItem(key)
  82. const { row = 0, column = 0 } = data?.cursorPosition || {}
  83. // restore the cursor to its original position, or the end of the document if past the end
  84. const { lines } = doc
  85. const lineNumber = row < lines ? row + 1 : lines
  86. const line = doc.line(lineNumber)
  87. const offset = line.from + column
  88. const pos = Math.min(offset || 0, doc.length)
  89. return {
  90. selection: EditorSelection.cursor(pos),
  91. }
  92. } catch (error) {
  93. // ignore invalid cursor position
  94. console.debug('invalid cursor position', error)
  95. return {}
  96. }
  97. }
  98. const dispatchSelectionAndScroll = (
  99. view: EditorView,
  100. selection: SelectionRange
  101. ) => {
  102. view.dispatch({
  103. selection,
  104. effects: EditorView.scrollIntoView(selection, { y: 'center' }),
  105. })
  106. view.focus()
  107. }
  108. export const setCursorLineAndScroll = (
  109. view: EditorView,
  110. lineNumber: number,
  111. columnNumber = 0
  112. ) => {
  113. // TODO: map the position through any changes since the previous compile?
  114. let selectionRange
  115. try {
  116. const pos = findValidPosition(view.state.doc, lineNumber, columnNumber)
  117. selectionRange = EditorSelection.cursor(pos)
  118. } catch (error) {
  119. // ignore invalid cursor position
  120. console.debug('invalid cursor position', error)
  121. }
  122. if (selectionRange) {
  123. dispatchSelectionAndScroll(view, selectionRange)
  124. }
  125. }
  126. export const setCursorPositionAndScroll = (view: EditorView, pos: number) => {
  127. let selectionRange
  128. try {
  129. pos = Math.min(pos, view.state.doc.length)
  130. selectionRange = EditorSelection.cursor(pos)
  131. } catch (error) {
  132. // ignore invalid cursor position
  133. console.debug('invalid cursor position', error)
  134. }
  135. if (selectionRange) {
  136. dispatchSelectionAndScroll(view, selectionRange)
  137. }
  138. }