bracket-matching.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  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. export const bracketMatching = () => {
  19. return bracketMatchingExtension({
  20. renderMatch: match => {
  21. const decorations: Range<Decoration>[] = []
  22. if (matchedAdjacent(match)) {
  23. // combine an adjacent pair of matching markers into a single decoration
  24. decorations.push(
  25. matchingMark.range(
  26. Math.min(match.start.from, match.end.from),
  27. Math.max(match.start.to, match.end.to)
  28. )
  29. )
  30. } else {
  31. // default match rendering (defaultRenderMatch in @codemirror/matchbrackets)
  32. const mark = match.matched ? matchingMark : nonmatchingMark
  33. decorations.push(mark.range(match.start.from, match.start.to))
  34. if (match.end) {
  35. decorations.push(mark.range(match.end.from, match.end.to))
  36. }
  37. }
  38. return decorations
  39. },
  40. })
  41. }
  42. interface AdjacentMatchResult extends MatchResult {
  43. end: {
  44. from: number
  45. to: number
  46. }
  47. }
  48. const matchedAdjacent = (match: MatchResult): match is AdjacentMatchResult =>
  49. Boolean(
  50. match.matched &&
  51. match.end &&
  52. (match.start.to === match.end.from || match.end.to === match.start.from)
  53. )
  54. export const bracketSelection = (): Extension[] => [
  55. EditorView.domEventHandlers({
  56. dblclick: (evt, view) => {
  57. const pos = view.posAtCoords({
  58. x: evt.pageX,
  59. y: evt.pageY,
  60. })
  61. if (!pos) return false
  62. const search = (direction: Direction, position: number) => {
  63. const match = matchBrackets(view.state, position, direction, {
  64. // Only look at data in the syntax tree, don't scan the text
  65. maxScanDistance: 0,
  66. })
  67. if (match?.matched && match.end) {
  68. const newRange = EditorSelection.range(
  69. Math.min(match.start.from, match.end.from),
  70. Math.max(match.end.to, match.start.to)
  71. )
  72. return newRange
  73. }
  74. return false
  75. }
  76. const dispatchSelection = (range: SelectionRange) => {
  77. view.dispatch({
  78. selection: range,
  79. })
  80. return true
  81. }
  82. // 1. Look forwards, from the character *behind* the cursor
  83. const forwardsExcludingBrackets = search(FORWARDS, pos - 1)
  84. if (forwardsExcludingBrackets) {
  85. return dispatchSelection(
  86. EditorSelection.range(
  87. forwardsExcludingBrackets.from + 1,
  88. forwardsExcludingBrackets.to - 1
  89. )
  90. )
  91. }
  92. // 2. Look forwards, from the character *in front of* the cursor
  93. const forwardsIncludingBrackets = search(FORWARDS, pos)
  94. if (forwardsIncludingBrackets) {
  95. return dispatchSelection(forwardsIncludingBrackets)
  96. }
  97. // 3. Look backwards, from the character *behind* the cursor
  98. const backwardsIncludingBrackets = search(BACKWARDS, pos)
  99. if (backwardsIncludingBrackets) {
  100. return dispatchSelection(backwardsIncludingBrackets)
  101. }
  102. // 4. Look backwards, from the character *in front of* the cursor
  103. const backwardsExcludingBrackets = search(BACKWARDS, pos + 1)
  104. if (backwardsExcludingBrackets) {
  105. return dispatchSelection(
  106. EditorSelection.range(
  107. backwardsExcludingBrackets.from + 1,
  108. backwardsExcludingBrackets.to - 1
  109. )
  110. )
  111. }
  112. return false
  113. },
  114. }),
  115. EditorView.baseTheme({
  116. '.cm-matchingBracket': {
  117. pointerEvents: 'none',
  118. },
  119. }),
  120. ]