overleaf/services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts
Alf Eaton 3f29aa2195 Ensure that each editor theme is only created once (#17095)
GitOrigin-RevId: 3551e02fab44fae7fcab5cb12886d45969e3990f
2024-02-16 09:05:03 +00:00

144 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 theres 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',
},
})