In the XML and HTML languages, if your cursor is in an element’s tag name, that tag name and its closing tag are highlighted. Otherwise, some other highlight is applied. I believe this is automatically added by the openedBy and closedBy attributes, so it can’t be used by tokens that are “self-closing”.
How can I apply a highlight/css class to a token only when the cursor is in the token?
(To be clear, I already know how to associate a token with a “tag”/style. I’m unsure how to apply that style conditioned with the cursor.)
You’d need to have an extension check the cursor and syntax tree on updates, and only add a decoration when the cursor is in the kind of node you’re interested in.
Pretty straightforward, thanks! Here’s some demo code for anyone else. It highlights the quotes if the cursor is in a “quoted string” or the nearest parens if the cursor is in a (group). Could use some refactoring/deduping but whatever.
import { Decoration, type DecorationSet, EditorView } from '@codemirror/view'
import { type Range, StateField, type EditorState } from '@codemirror/state'
import { syntaxTree } from '@codemirror/language'
import { queryTerms } from 'shared-dom'
const quoteDecorator = Decoration.mark({ class: 'query-quote' })
const parenDecorator = Decoration.mark({ class: 'query-paren' })
const activeDecorator = Decoration.mark({ class: 'query-active' })
function getDecorations(state: EditorState): DecorationSet {
const decorations: Array<Range<Decoration>> = []
let activeParenSet = false
syntaxTree(state).iterate({
enter: (node) => {
if (node.type.is(queryTerms.QuotedString)) {
if (
state.selection.main.head > node.from &&
state.selection.main.head < node.to
) {
decorations.push(activeDecorator.range(node.from, node.from + 1))
decorations.push(activeDecorator.range(node.to - 1, node.to))
} else {
decorations.push(quoteDecorator.range(node.from, node.from + 1))
decorations.push(quoteDecorator.range(node.to - 1, node.to))
}
}
},
leave: (node) => {
// this is on the `leave` callback because we want to set `activeParenSet=true` on the most nested parens
if (node.type.is(queryTerms.Group)) {
if (
state.selection.main.head > node.from &&
state.selection.main.head < node.to &&
!activeParenSet
) {
decorations.push(activeDecorator.range(node.from, node.from + 1))
decorations.push(activeDecorator.range(node.to - 1, node.to))
activeParenSet = true
} else {
decorations.push(parenDecorator.range(node.from, node.from + 1))
decorations.push(parenDecorator.range(node.to - 1, node.to))
}
}
},
})
return Decoration.set(decorations, true)
}
export const queryDecorations = StateField.define<DecorationSet>({
create: getDecorations,
update(decorations, tr) {
if (!tr.docChanged && tr.selection == null) return decorations
return getDecorations(tr.state)
},
provide: (f) => EditorView.decorations.from(f),
})
const baseTheme = EditorView.baseTheme({
'.query-paren': {
color: 'darkorange',
fontWeight: 'bold',
},
'.query-quote': {
color: 'darkorange',
fontWeight: 'bold',
},
'.query-active': {
color: 'red',
fontWeight: 'bold',
},
})