2023-04-13 04:21:25 -04:00
|
|
|
|
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
|
|
|
|
|
|
2023-06-08 04:35:51 -04:00
|
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-04-13 04:21:25 -04:00
|
|
|
|
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)
|
|
|
|
|
)
|
|
|
|
|
|
2023-06-08 04:35:51 -04:00
|
|
|
|
/**
|
|
|
|
|
* A custom extension which handles double-click events on a matched bracket
|
|
|
|
|
* and extends the selection to cover the contents of the bracket pair.
|
|
|
|
|
*/
|
2023-04-13 04:21:25 -04:00
|
|
|
|
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) {
|
2023-06-08 04:35:51 -04:00
|
|
|
|
return EditorSelection.range(
|
2023-04-13 04:21:25 -04:00
|
|
|
|
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
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
EditorView.baseTheme({
|
|
|
|
|
'.cm-matchingBracket': {
|
|
|
|
|
pointerEvents: 'none',
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
]
|