bracket-matching.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import {
  2. bracketMatching as bracketMatchingExtension,
  3. matchBrackets,
  4. type MatchResult,
  5. } from '@codemirror/language'
  6. import { Decoration, EditorView } from '@codemirror/view'
  7. import {
  8. EditorSelection,
  9. Extension,
  10. SelectionRange,
  11. type Range,
  12. } from '@codemirror/state'
  13. const matchingMark = Decoration.mark({ class: 'cm-matchingBracket' })
  14. const nonmatchingMark = Decoration.mark({ class: 'cm-nonmatchingBracket' })
  15. const FORWARDS = 1
  16. const BACKWARDS = -1
  17. type Direction = 1 | -1
  18. /**
  19. * A built-in extension which decorates matching pairs of brackets when focused,
  20. * configured with a custom render function that combines adjacent pairs of matching markers
  21. * into a single decoration so there’s no border between them.
  22. */
  23. export const bracketMatching = () => {
  24. return bracketMatchingExtension({
  25. renderMatch: match => {
  26. const decorations: Range<Decoration>[] = []
  27. if (matchedAdjacent(match)) {
  28. // combine an adjacent pair of matching markers into a single decoration
  29. decorations.push(
  30. matchingMark.range(
  31. Math.min(match.start.from, match.end.from),
  32. Math.max(match.start.to, match.end.to)
  33. )
  34. )
  35. } else {
  36. // default match rendering (defaultRenderMatch in @codemirror/matchbrackets)
  37. const mark = match.matched ? matchingMark : nonmatchingMark
  38. decorations.push(mark.range(match.start.from, match.start.to))
  39. if (match.end) {
  40. decorations.push(mark.range(match.end.from, match.end.to))
  41. }
  42. }
  43. return decorations
  44. },
  45. })
  46. }
  47. interface AdjacentMatchResult extends MatchResult {
  48. end: {
  49. from: number
  50. to: number
  51. }
  52. }
  53. const matchedAdjacent = (match: MatchResult): match is AdjacentMatchResult =>
  54. Boolean(
  55. match.matched &&
  56. match.end &&
  57. (match.start.to === match.end.from || match.end.to === match.start.from)
  58. )
  59. /**
  60. * A custom extension which handles double-click events on a matched bracket
  61. * and extends the selection to cover the contents of the bracket pair.
  62. */
  63. export const bracketSelection = (): Extension[] => [
  64. EditorView.domEventHandlers({
  65. dblclick: (evt, view) => {
  66. const pos = view.posAtCoords({
  67. x: evt.pageX,
  68. y: evt.pageY,
  69. })
  70. if (!pos) return false
  71. const search = (direction: Direction, position: number) => {
  72. const match = matchBrackets(view.state, position, direction, {
  73. // Only look at data in the syntax tree, don't scan the text
  74. maxScanDistance: 0,
  75. })
  76. if (match?.matched && match.end) {
  77. return EditorSelection.range(
  78. Math.min(match.start.from, match.end.from),
  79. Math.max(match.end.to, match.start.to)
  80. )
  81. }
  82. return false
  83. }
  84. const dispatchSelection = (range: SelectionRange) => {
  85. view.dispatch({
  86. selection: range,
  87. })
  88. return true
  89. }
  90. // 1. Look forwards, from the character *behind* the cursor
  91. const forwardsExcludingBrackets = search(FORWARDS, pos - 1)
  92. if (forwardsExcludingBrackets) {
  93. return dispatchSelection(
  94. EditorSelection.range(
  95. forwardsExcludingBrackets.from + 1,
  96. forwardsExcludingBrackets.to - 1
  97. )
  98. )
  99. }
  100. // 2. Look forwards, from the character *in front of* the cursor
  101. const forwardsIncludingBrackets = search(FORWARDS, pos)
  102. if (forwardsIncludingBrackets) {
  103. return dispatchSelection(forwardsIncludingBrackets)
  104. }
  105. // 3. Look backwards, from the character *behind* the cursor
  106. const backwardsIncludingBrackets = search(BACKWARDS, pos)
  107. if (backwardsIncludingBrackets) {
  108. return dispatchSelection(backwardsIncludingBrackets)
  109. }
  110. // 4. Look backwards, from the character *in front of* the cursor
  111. const backwardsExcludingBrackets = search(BACKWARDS, pos + 1)
  112. if (backwardsExcludingBrackets) {
  113. return dispatchSelection(
  114. EditorSelection.range(
  115. backwardsExcludingBrackets.from + 1,
  116. backwardsExcludingBrackets.to - 1
  117. )
  118. )
  119. }
  120. return false
  121. },
  122. }),
  123. matchingBracketTheme,
  124. ]
  125. const matchingBracketTheme = EditorView.baseTheme({
  126. '.cm-matchingBracket': {
  127. pointerEvents: 'none',
  128. },
  129. })