projection.ts 5.2 KB

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