| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- import {
- bracketMatching as bracketMatchingExtension,
- matchBrackets,
- type MatchResult,
- } from '@codemirror/language'
- import { Decoration, EditorView } from '@codemirror/view'
- import {
- EditorSelection,
- Extension,
- SelectionRange,
- type Range,
- } from '@codemirror/state'
- const matchingMark = Decoration.mark({ class: 'cm-matchingBracket' })
- const nonmatchingMark = Decoration.mark({ class: 'cm-nonmatchingBracket' })
- const FORWARDS = 1
- const BACKWARDS = -1
- type Direction = 1 | -1
- /**
- * A built-in extension which decorates matching pairs of brackets when focused,
- * configured with a custom render function that combines adjacent pairs of matching markers
- * into a single decoration so there’s no border between them.
- */
- export const bracketMatching = () => {
- return bracketMatchingExtension({
- renderMatch: match => {
- const decorations: Range<Decoration>[] = []
- if (matchedAdjacent(match)) {
- // combine an adjacent pair of matching markers into a single decoration
- decorations.push(
- matchingMark.range(
- Math.min(match.start.from, match.end.from),
- Math.max(match.start.to, match.end.to)
- )
- )
- } else {
- // default match rendering (defaultRenderMatch in @codemirror/matchbrackets)
- const mark = match.matched ? matchingMark : nonmatchingMark
- decorations.push(mark.range(match.start.from, match.start.to))
- if (match.end) {
- decorations.push(mark.range(match.end.from, match.end.to))
- }
- }
- return decorations
- },
- })
- }
- interface AdjacentMatchResult extends MatchResult {
- end: {
- from: number
- to: number
- }
- }
- const matchedAdjacent = (match: MatchResult): match is AdjacentMatchResult =>
- Boolean(
- match.matched &&
- match.end &&
- (match.start.to === match.end.from || match.end.to === match.start.from)
- )
- /**
- * A custom extension which handles double-click events on a matched bracket
- * and extends the selection to cover the contents of the bracket pair.
- */
- export const bracketSelection = (): Extension[] => [
- EditorView.domEventHandlers({
- dblclick: (evt, view) => {
- const pos = view.posAtCoords({
- x: evt.pageX,
- y: evt.pageY,
- })
- if (!pos) return false
- const search = (direction: Direction, position: number) => {
- const match = matchBrackets(view.state, position, direction, {
- // Only look at data in the syntax tree, don't scan the text
- maxScanDistance: 0,
- })
- if (match?.matched && match.end) {
- return EditorSelection.range(
- Math.min(match.start.from, match.end.from),
- Math.max(match.end.to, match.start.to)
- )
- }
- return false
- }
- const dispatchSelection = (range: SelectionRange) => {
- view.dispatch({
- selection: range,
- })
- return true
- }
- // 1. Look forwards, from the character *behind* the cursor
- const forwardsExcludingBrackets = search(FORWARDS, pos - 1)
- if (forwardsExcludingBrackets) {
- return dispatchSelection(
- EditorSelection.range(
- forwardsExcludingBrackets.from + 1,
- forwardsExcludingBrackets.to - 1
- )
- )
- }
- // 2. Look forwards, from the character *in front of* the cursor
- const forwardsIncludingBrackets = search(FORWARDS, pos)
- if (forwardsIncludingBrackets) {
- return dispatchSelection(forwardsIncludingBrackets)
- }
- // 3. Look backwards, from the character *behind* the cursor
- const backwardsIncludingBrackets = search(BACKWARDS, pos)
- if (backwardsIncludingBrackets) {
- return dispatchSelection(backwardsIncludingBrackets)
- }
- // 4. Look backwards, from the character *in front of* the cursor
- const backwardsExcludingBrackets = search(BACKWARDS, pos + 1)
- if (backwardsExcludingBrackets) {
- return dispatchSelection(
- EditorSelection.range(
- backwardsExcludingBrackets.from + 1,
- backwardsExcludingBrackets.to - 1
- )
- )
- }
- return false
- },
- }),
- matchingBracketTheme,
- ]
- const matchingBracketTheme = EditorView.baseTheme({
- '.cm-matchingBracket': {
- pointerEvents: 'none',
- },
- })
|