projection.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { ensureSyntaxTree } from '@codemirror/language'
  2. import { EditorState, Transaction } from '@codemirror/state'
  3. import { IterMode, SyntaxNodeRef } from '@lezer/common'
  4. const TWENTY_MS = 20
  5. const FIVE_HUNDRED_MS = 500
  6. /**
  7. * A single item in the projection
  8. */
  9. export abstract class ProjectionItem {
  10. readonly from: number = 0
  11. readonly to: number = 0
  12. readonly line: number = 0
  13. }
  14. /* eslint-disable no-unused-vars */
  15. export enum ProjectionStatus {
  16. Pending,
  17. Partial,
  18. Complete,
  19. }
  20. /* eslint-enable no-unused-vars */
  21. /*
  22. * Result of extracting a projection from the document.
  23. * Holds the list of ProjectionItems and the status of
  24. * the projection
  25. */
  26. export interface ProjectionResult<T extends ProjectionItem> {
  27. items: T[]
  28. status: ProjectionStatus
  29. }
  30. const intersects = (fromA: number, toA: number, fromB: number, toB: number) => {
  31. return !(toA < fromB || fromA > toB)
  32. }
  33. export type NodeIntersectsChangeFn = (node: SyntaxNodeRef) => boolean
  34. export function updatePosition<T extends ProjectionItem>(
  35. item: T,
  36. transaction?: Transaction
  37. ): T {
  38. if (!transaction) {
  39. return item
  40. }
  41. const { from, to } = item
  42. const newFrom = transaction.changes.mapPos(from)
  43. const newTo = transaction.changes.mapPos(to)
  44. const lineNumber = transaction.state.doc.lineAt(newFrom).number
  45. if (newFrom === from && newTo === to && lineNumber === item.line) {
  46. // Optimisation - if the item hasn't moved, don't create a new object
  47. // If items are not immutable this can introduce problems
  48. return item
  49. }
  50. return {
  51. ...item,
  52. from: newFrom,
  53. to: newTo,
  54. line: lineNumber,
  55. }
  56. }
  57. export type EnterNodeFn<T> = (
  58. state: EditorState,
  59. node: SyntaxNodeRef,
  60. items: T[],
  61. nodeIntersectsChange: NodeIntersectsChangeFn
  62. ) => any
  63. /**
  64. * Calculates an updated projection of an editor state. Passing a previous ProjectionResult
  65. * will reuse the existing projection elements (though updating their position to
  66. * point correctly into the latest EditorState), outside of the changed range.
  67. *
  68. * @param state The current editor state
  69. * @param fromA The start of the modified range in the previous state.
  70. * Ignored if `previousResult` is not provided
  71. * @param toA The end of the modified range in the previous state.
  72. * Ignored if `previousResult` is not provided
  73. * @param fromB The start of the modified range in the `state`
  74. * @param toB The end of the modified range in the `state`
  75. * @param initialParse If this is the intial parse of the document. If that's
  76. * the case, we allow 500ms parse time instead of 20ms
  77. * @param enterNode A function to call when 'enter'ing a node while traversing the syntax tree,
  78. * used to identify nodes we are interested in.
  79. * @param transaction Optional, used to update item positions in `previousResult`
  80. * @param previousResult A previous ProjectionResult that will be reused for
  81. * projection elements outside of the range of [fromA; toA]
  82. * @returns A ProjectionResult<T> pointing to locations in `state`
  83. */
  84. export function getUpdatedProjection<T extends ProjectionItem>(
  85. state: EditorState,
  86. fromA: number,
  87. toA: number,
  88. fromB: number,
  89. toB: number,
  90. initialParse = false,
  91. enterNode: EnterNodeFn<T>,
  92. transaction?: Transaction,
  93. previousResult: ProjectionResult<T> = {
  94. items: [],
  95. status: ProjectionStatus.Pending,
  96. }
  97. ): ProjectionResult<T> {
  98. // Only reuse results from a Complete parse, otherwise we may drop entries.
  99. // We keep items that lie outside the change range, and update their positions.
  100. const items: T[] =
  101. previousResult.status === ProjectionStatus.Complete
  102. ? previousResult
  103. .items!.filter(item => !intersects(item.from, item.to, fromA, toA))
  104. .map(x => updatePosition(x, transaction))
  105. : []
  106. if (previousResult.status !== ProjectionStatus.Complete) {
  107. // We have previously tried to compute the projection, but unsuccessfully,
  108. // so we should try to parse the whole file again.
  109. toB = state.doc.length
  110. fromB = 0
  111. }
  112. const tree = ensureSyntaxTree(
  113. state,
  114. toB,
  115. initialParse ? FIVE_HUNDRED_MS : TWENTY_MS
  116. )
  117. if (tree) {
  118. tree.iterate({
  119. from: fromB,
  120. to: toB,
  121. enter(node) {
  122. const nodeIntersectsChange = (n: SyntaxNodeRef) => {
  123. return intersects(n.from, n.to, fromB, toB)
  124. }
  125. return enterNode(state, node, items, nodeIntersectsChange)
  126. },
  127. mode: IterMode.IgnoreMounts | IterMode.IgnoreOverlays,
  128. })
  129. // We know the exact projection. Return it.
  130. return {
  131. status: ProjectionStatus.Complete,
  132. items: items.sort((a, b) => a.from - b.from),
  133. }
  134. } else if (previousResult.status !== ProjectionStatus.Pending) {
  135. // We don't know the latest projection, but we have an idea of a previous
  136. // projection.
  137. return {
  138. status: ProjectionStatus.Partial,
  139. items: previousResult.items,
  140. }
  141. } else {
  142. // We have no previous projection, and no idea of the current projection.
  143. // Return pending.
  144. return {
  145. items: [],
  146. status: ProjectionStatus.Pending,
  147. }
  148. }
  149. }