projection.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. from = 0
  11. to = 0
  12. line = 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. return {
  44. ...item,
  45. from: newFrom,
  46. to: transaction.changes.mapPos(to),
  47. line: transaction.state.doc.lineAt(newFrom).number,
  48. }
  49. }
  50. export type EnterNodeFn<T> = (
  51. state: EditorState,
  52. node: SyntaxNodeRef,
  53. items: T[],
  54. nodeIntersectsChange: NodeIntersectsChangeFn
  55. ) => any
  56. /**
  57. * Calculates an updated projection of an editor state. Passing a previous ProjectionResult
  58. * will reuse the existing projection elements (though updating their position to
  59. * point correctly into the latest EditorState), outside of the changed range.
  60. *
  61. * @param state The current editor state
  62. * @param fromA The start of the modified range in the previous state.
  63. * Ignored if `previousResult` is not provided
  64. * @param toA The end of the modified range in the previous state.
  65. * Ignored if `previousResult` is not provided
  66. * @param fromB The start of the modified range in the `state`
  67. * @param toB The end of the modified range in the `state`
  68. * @param initialParse If this is the intial parse of the document. If that's
  69. * the case, we allow 500ms parse time instead of 20ms
  70. * @param enterNode A function to call when 'enter'ing a node while traversing the syntax tree,
  71. * used to identify nodes we are interested in.
  72. * @param transaction Optional, used to update item positions in `previousResult`
  73. * @param previousResult A previous ProjectionResult that will be reused for
  74. * projection elements outside of the range of [fromA; toA]
  75. * @returns A ProjectionResult<T> pointing to locations in `state`
  76. */
  77. export function getUpdatedProjection<T extends ProjectionItem>(
  78. state: EditorState,
  79. fromA: number,
  80. toA: number,
  81. fromB: number,
  82. toB: number,
  83. initialParse = false,
  84. enterNode: EnterNodeFn<T>,
  85. transaction?: Transaction,
  86. previousResult: ProjectionResult<T> = {
  87. items: [],
  88. status: ProjectionStatus.Pending,
  89. }
  90. ): ProjectionResult<T> {
  91. // Only reuse results from a Complete parse, otherwise we may drop entries.
  92. // We keep items that lie outside the change range, and update their positions.
  93. const items: T[] =
  94. previousResult.status === ProjectionStatus.Complete
  95. ? previousResult
  96. .items!.filter(item => !intersects(item.from, item.to, fromA, toA))
  97. .map(x => updatePosition(x, transaction))
  98. : []
  99. if (previousResult.status !== ProjectionStatus.Complete) {
  100. // We have previously tried to compute the projection, but unsuccessfully,
  101. // so we should try to parse the whole file again.
  102. toB = state.doc.length
  103. fromB = 0
  104. }
  105. const tree = ensureSyntaxTree(
  106. state,
  107. toB,
  108. initialParse ? FIVE_HUNDRED_MS : TWENTY_MS
  109. )
  110. if (tree) {
  111. tree.iterate({
  112. from: fromB,
  113. to: toB,
  114. enter(node) {
  115. const nodeIntersectsChange = (n: SyntaxNodeRef) => {
  116. return intersects(n.from, n.to, fromB, toB)
  117. }
  118. return enterNode(state, node, items, nodeIntersectsChange)
  119. },
  120. mode: IterMode.IgnoreMounts | IterMode.IgnoreOverlays,
  121. })
  122. // We know the exact projection. Return it.
  123. return {
  124. status: ProjectionStatus.Complete,
  125. items: items.sort((a, b) => a.from - b.from),
  126. }
  127. } else if (previousResult.status !== ProjectionStatus.Pending) {
  128. // We don't know the latest projection, but we have an idea of a previous
  129. // projection.
  130. return {
  131. status: ProjectionStatus.Partial,
  132. items: previousResult.items,
  133. }
  134. } else {
  135. // We have no previous projection, and no idea of the current projection.
  136. // Return pending.
  137. return {
  138. items: [],
  139. status: ProjectionStatus.Pending,
  140. }
  141. }
  142. }