| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- import {
- EditorSelection,
- EditorState,
- SelectionRange,
- Text,
- TransactionSpec,
- } from '@codemirror/state'
- import { EditorView, ViewPlugin } from '@codemirror/view'
- import { findValidPosition } from '../utils/position'
- import customLocalStorage from '../../../infrastructure/local-storage'
- import { debugConsole } from '@/utils/debugging'
- const buildStorageKey = (docId: string) => `doc.position.${docId}`
- /**
- * A custom extension that:
- * a) stores the cursor position in localStorage when the view is destroyed or the window is closed.
- * b) dispatches the cursor position when it changes, for use with “show position in PDF”.
- */
- export const cursorPosition = ({
- currentDoc: { doc_id: docId },
- }: {
- currentDoc: { doc_id: string }
- }) => {
- return [
- // store cursor position
- ViewPlugin.define(view => {
- const unloadListener = () => {
- storeCursorPosition(view, docId)
- }
- window.addEventListener('unload', unloadListener)
- return {
- destroy: () => {
- window.removeEventListener('unload', unloadListener)
- unloadListener()
- },
- }
- }),
- // Asynchronously dispatch cursor position when the selection changes and
- // provide a little debouncing. Using requestAnimationFrame postpones it
- // until the next CM6 DOM update.
- ViewPlugin.define(view => {
- let animationFrameRequest: number | null = null
- return {
- update(update) {
- if (update.selectionSet || update.docChanged) {
- if (animationFrameRequest) {
- window.cancelAnimationFrame(animationFrameRequest)
- }
- animationFrameRequest = window.requestAnimationFrame(() => {
- animationFrameRequest = null
- dispatchCursorPosition(update.state)
- })
- }
- },
- }
- }),
- ]
- }
- // convert the selection head to a row and column
- const buildCursorPosition = (state: EditorState) => {
- const pos = state.selection.main.head
- const line = state.doc.lineAt(pos)
- const row = line.number - 1 // 0-indexed
- const column = pos - line.from
- return { row, column }
- }
- // dispatch the current cursor position for use with synctex
- const dispatchCursorPosition = (state: EditorState) => {
- const cursorPosition = buildCursorPosition(state)
- window.dispatchEvent(
- new CustomEvent('cursor:editor:update', { detail: cursorPosition })
- )
- }
- // store the cursor position for restoring on load
- const storeCursorPosition = (view: EditorView, docId: string) => {
- const key = buildStorageKey(docId)
- const data = customLocalStorage.getItem(key)
- const cursorPosition = buildCursorPosition(view.state)
- customLocalStorage.setItem(key, { ...data, cursorPosition })
- }
- // restore the stored cursor position on load
- export const restoreCursorPosition = (
- doc: Text,
- docId: string
- ): TransactionSpec => {
- try {
- const key = buildStorageKey(docId)
- const data = customLocalStorage.getItem(key)
- const { row = 0, column = 0 } = data?.cursorPosition || {}
- // restore the cursor to its original position, or the end of the document if past the end
- const { lines } = doc
- const lineNumber = row < lines ? row + 1 : lines
- const line = doc.line(lineNumber)
- const offset = line.from + column
- const pos = Math.min(offset || 0, doc.length)
- return {
- selection: EditorSelection.cursor(pos),
- }
- } catch (error) {
- // ignore invalid cursor position
- debugConsole.debug('invalid cursor position', error)
- return {}
- }
- }
- const dispatchSelectionAndScroll = (
- view: EditorView,
- selection: SelectionRange
- ) => {
- view.dispatch({
- selection,
- effects: EditorView.scrollIntoView(selection, { y: 'center' }),
- })
- view.focus()
- }
- export const setCursorLineAndScroll = (
- view: EditorView,
- lineNumber: number,
- columnNumber = 0
- ) => {
- // TODO: map the position through any changes since the previous compile?
- let selectionRange
- try {
- const pos = findValidPosition(view.state.doc, lineNumber, columnNumber)
- selectionRange = EditorSelection.cursor(pos)
- } catch (error) {
- // ignore invalid cursor position
- debugConsole.debug('invalid cursor position', error)
- }
- if (selectionRange) {
- dispatchSelectionAndScroll(view, selectionRange)
- }
- }
- export const setCursorPositionAndScroll = (view: EditorView, pos: number) => {
- let selectionRange
- try {
- pos = Math.min(pos, view.state.doc.length)
- selectionRange = EditorSelection.cursor(pos)
- } catch (error) {
- // ignore invalid cursor position
- debugConsole.debug('invalid cursor position', error)
- }
- if (selectionRange) {
- dispatchSelectionAndScroll(view, selectionRange)
- }
- }
|