Add CodeMirror dev tools extension (#12142)
GitOrigin-RevId: 148a0fba5faf6dc8f638fcb4666e2fda6c5c6c40
This commit is contained in:
@@ -48,6 +48,7 @@ import { foldingKeymap } from './folding-keymap'
|
|||||||
import { inlineBackground } from './inline-background'
|
import { inlineBackground } from './inline-background'
|
||||||
import { fontLoad } from './font-load'
|
import { fontLoad } from './font-load'
|
||||||
import { indentationMarkers } from './indentation-markers'
|
import { indentationMarkers } from './indentation-markers'
|
||||||
|
import { codemirrorDevTools } from '../languages/latex/codemirror-dev-tools'
|
||||||
|
|
||||||
const ignoredDefaultKeybindings = new Set([
|
const ignoredDefaultKeybindings = new Set([
|
||||||
// NOTE: disable "Mod-Enter" as it's used for "Compile"
|
// NOTE: disable "Mod-Enter" as it's used for "Compile"
|
||||||
@@ -133,6 +134,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
|
|||||||
scrollOneLine(),
|
scrollOneLine(),
|
||||||
fontLoad(),
|
fontLoad(),
|
||||||
inlineBackground(options.visual.visual),
|
inlineBackground(options.visual.visual),
|
||||||
|
codemirrorDevTools(),
|
||||||
exceptionLogger(),
|
exceptionLogger(),
|
||||||
moduleExtensions.map(extension => extension()),
|
moduleExtensions.map(extension => extension()),
|
||||||
thirdPartyExtensions(),
|
thirdPartyExtensions(),
|
||||||
|
|||||||
+309
@@ -0,0 +1,309 @@
|
|||||||
|
import {
|
||||||
|
Annotation,
|
||||||
|
Compartment,
|
||||||
|
EditorSelection,
|
||||||
|
EditorState,
|
||||||
|
StateEffect,
|
||||||
|
StateField,
|
||||||
|
Transaction,
|
||||||
|
} from '@codemirror/state'
|
||||||
|
import {
|
||||||
|
Decoration,
|
||||||
|
DecorationSet,
|
||||||
|
EditorView,
|
||||||
|
ViewPlugin,
|
||||||
|
} from '@codemirror/view'
|
||||||
|
import { syntaxTree } from '@codemirror/language'
|
||||||
|
import { toggleVisualEffect } from '../../extensions/visual/visual'
|
||||||
|
import { hasLanguageLoadedEffect } from '../../extensions/language'
|
||||||
|
|
||||||
|
// to enable: window.localStorage.setItem('cm6-dev-tools', 'on')
|
||||||
|
// to disable: window.localStorage.removeItem('cm6-dev-tools')
|
||||||
|
const enabled = window.localStorage.getItem('cm6-dev-tools') === 'on'
|
||||||
|
|
||||||
|
const devToolsConf = new Compartment()
|
||||||
|
|
||||||
|
export const codemirrorDevTools = () => {
|
||||||
|
return enabled ? [devToolsButton, devToolsConf.of(createExtension())] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
const devToolsButton = ViewPlugin.define(view => {
|
||||||
|
const getContainer = () =>
|
||||||
|
document.querySelector('.formatting-buttons-wrapper')
|
||||||
|
|
||||||
|
const removeButton = () => {
|
||||||
|
getContainer()?.querySelector('#cm6-dev-tools-button')?.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addButton = () => {
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.classList.add('btn', 'formatting-btn', 'formatting-btn--icon')
|
||||||
|
button.id = 'cm6-dev-tools-button'
|
||||||
|
button.textContent = '🦧'
|
||||||
|
button.addEventListener('click', event => {
|
||||||
|
event.preventDefault()
|
||||||
|
view.dispatch(toggleDevTools())
|
||||||
|
})
|
||||||
|
|
||||||
|
getContainer()?.prepend(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeButton()
|
||||||
|
addButton()
|
||||||
|
|
||||||
|
return {
|
||||||
|
update(update) {
|
||||||
|
for (const tr of update.transactions) {
|
||||||
|
for (const effect of tr.effects) {
|
||||||
|
if (effect.is(toggleVisualEffect)) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
removeButton()
|
||||||
|
addButton()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
removeButton()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isActive = () =>
|
||||||
|
window.localStorage.getItem('cm6-dev-tools-active') === 'on'
|
||||||
|
|
||||||
|
const toggleDevTools = () => {
|
||||||
|
window.localStorage.setItem('cm6-dev-tools-active', isActive() ? 'off' : 'on')
|
||||||
|
|
||||||
|
return {
|
||||||
|
effects: devToolsConf.reconfigure(createExtension()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createExtension = () =>
|
||||||
|
isActive() ? [devToolsView, highlightSelectedNode, devToolsTheme] : []
|
||||||
|
|
||||||
|
const devToolsTheme = EditorView.baseTheme({
|
||||||
|
'.ol-cm-dev-tools-container': {
|
||||||
|
padding: '8px 8px 0',
|
||||||
|
backgroundColor: '#222',
|
||||||
|
color: '#eee',
|
||||||
|
fontSize: '13px',
|
||||||
|
flexShrink: '0',
|
||||||
|
fontFamily: '"SF Mono", monospace',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
'.ol-cm-dev-tools-item': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderTop: '2px solid transparent',
|
||||||
|
borderBottom: '2px solid transparent',
|
||||||
|
scrollMargin: '2em',
|
||||||
|
},
|
||||||
|
'.ol-cm-selected-node-highlight': {
|
||||||
|
backgroundColor: 'yellow',
|
||||||
|
},
|
||||||
|
'.ol-cm-dev-tools-covered-item': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 0, 0.2)',
|
||||||
|
},
|
||||||
|
'.ol-cm-dev-tools-selected-item': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 0, 0.5)',
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
'.ol-cm-dev-tools-cursor-before': {
|
||||||
|
borderTopColor: 'rgba(255, 255, 0, 1)',
|
||||||
|
'& + .ol-cm-dev-tools-cursor-before': {
|
||||||
|
borderTopColor: 'transparent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'.ol-cm-dev-tools-positions': {
|
||||||
|
position: 'sticky',
|
||||||
|
bottom: '0',
|
||||||
|
backgroundColor: 'inherit',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
'.ol-cm-dev-tools-position': {
|
||||||
|
padding: '4px 0',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fromDevTools = Annotation.define()
|
||||||
|
|
||||||
|
const transactionIsFromDevTools = (tr: Transaction) =>
|
||||||
|
tr.annotation(fromDevTools)
|
||||||
|
|
||||||
|
const devToolsView = ViewPlugin.define(view => {
|
||||||
|
const scroller = document.querySelector<HTMLDivElement>('.cm-scroller')
|
||||||
|
|
||||||
|
if (!scroller) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.classList.add('ol-cm-dev-tools-container')
|
||||||
|
scroller.append(container)
|
||||||
|
|
||||||
|
const highlightNodeRange = (from: number, to: number) => {
|
||||||
|
view.dispatch({
|
||||||
|
effects: [selectedNodeEffect.of({ from, to })],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectNodeRange = (from: number, to: number) => {
|
||||||
|
view.dispatch({
|
||||||
|
annotations: [fromDevTools.of(true)],
|
||||||
|
selection: EditorSelection.single(from, to),
|
||||||
|
effects: EditorView.scrollIntoView(from, { y: 'center' }),
|
||||||
|
})
|
||||||
|
view.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPanel(view.state, container, highlightNodeRange, selectNodeRange, true)
|
||||||
|
|
||||||
|
return {
|
||||||
|
update(update) {
|
||||||
|
if (
|
||||||
|
update.docChanged ||
|
||||||
|
update.selectionSet ||
|
||||||
|
hasLanguageLoadedEffect(update)
|
||||||
|
) {
|
||||||
|
const scroll = !update.transactions.some(transactionIsFromDevTools)
|
||||||
|
buildPanel(
|
||||||
|
update.state,
|
||||||
|
container,
|
||||||
|
highlightNodeRange,
|
||||||
|
selectNodeRange,
|
||||||
|
scroll
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
container.remove()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const buildPanel = (
|
||||||
|
state: EditorState,
|
||||||
|
container: HTMLDivElement,
|
||||||
|
highlightNodeRange: (from: number, to: number) => void,
|
||||||
|
selectNodeRange: (from: number, to: number) => void,
|
||||||
|
scroll: boolean
|
||||||
|
) => {
|
||||||
|
container.textContent = '' // clear
|
||||||
|
|
||||||
|
const tree = syntaxTree(state)
|
||||||
|
const { selection } = state
|
||||||
|
let itemToCenter: HTMLDivElement
|
||||||
|
|
||||||
|
let depth = 0
|
||||||
|
tree.iterate({
|
||||||
|
enter(nodeRef) {
|
||||||
|
const { from, to, name } = nodeRef
|
||||||
|
|
||||||
|
const element = document.createElement('div')
|
||||||
|
element.classList.add('ol-cm-dev-tools-item')
|
||||||
|
element.style.paddingLeft = `${depth * 16}px`
|
||||||
|
element.textContent = name
|
||||||
|
|
||||||
|
element.addEventListener('mouseover', () => {
|
||||||
|
highlightNodeRange(from, to)
|
||||||
|
})
|
||||||
|
|
||||||
|
element.addEventListener('click', () => {
|
||||||
|
selectNodeRange(from, to)
|
||||||
|
})
|
||||||
|
|
||||||
|
container.append(element)
|
||||||
|
|
||||||
|
for (const range of selection.ranges) {
|
||||||
|
// completely covered by selection
|
||||||
|
if (range.from <= from && range.to >= to) {
|
||||||
|
element.classList.add('ol-cm-dev-tools-selected-item')
|
||||||
|
itemToCenter = element
|
||||||
|
} else if (
|
||||||
|
(range.from > from && range.from < to) ||
|
||||||
|
(range.to > from && range.to < to)
|
||||||
|
) {
|
||||||
|
element.classList.add('ol-cm-dev-tools-covered-item')
|
||||||
|
itemToCenter = element
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range.head === from) {
|
||||||
|
element.classList.add('ol-cm-dev-tools-cursor-before')
|
||||||
|
itemToCenter = element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
depth++
|
||||||
|
},
|
||||||
|
leave(node) {
|
||||||
|
depth--
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const positions = document.createElement('div')
|
||||||
|
positions.classList.add('ol-cm-dev-tools-positions')
|
||||||
|
container.append(positions)
|
||||||
|
|
||||||
|
for (const range of state.selection.ranges) {
|
||||||
|
const line = state.doc.lineAt(range.head)
|
||||||
|
const column = range.head - line.from + 1
|
||||||
|
const position = document.createElement('div')
|
||||||
|
position.classList.add('ol-cm-dev-tools-position')
|
||||||
|
position.textContent = `line ${line.number}, col ${column}, pos ${range.head}`
|
||||||
|
positions.append(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scroll && itemToCenter!) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
itemToCenter.scrollIntoView({
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedNodeEffect = StateEffect.define<{
|
||||||
|
from: number
|
||||||
|
to: number
|
||||||
|
} | null>()
|
||||||
|
|
||||||
|
const highlightSelectedNode = StateField.define<DecorationSet>({
|
||||||
|
create() {
|
||||||
|
return Decoration.none
|
||||||
|
},
|
||||||
|
update(value, tr) {
|
||||||
|
if (tr.selection) {
|
||||||
|
value = Decoration.none
|
||||||
|
}
|
||||||
|
for (const effect of tr.effects) {
|
||||||
|
if (effect.is(selectedNodeEffect)) {
|
||||||
|
if (effect.value) {
|
||||||
|
const { from, to } = effect.value
|
||||||
|
|
||||||
|
// TODO: widget decoration if no range to decorate?
|
||||||
|
if (to > from) {
|
||||||
|
value = Decoration.set([
|
||||||
|
Decoration.mark({
|
||||||
|
class: 'ol-cm-selected-node-highlight',
|
||||||
|
}).range(from, to),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = Decoration.none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
provide(f) {
|
||||||
|
return EditorView.decorations.from(f)
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import { Decoration, EditorView, Panel, showPanel } from '@codemirror/view'
|
|
||||||
import { languageLoadedEffect } from '../../extensions/language'
|
|
||||||
import { Compartment, EditorState } from '@codemirror/state'
|
|
||||||
import { getAncestorStack } from '../../utils/tree-query'
|
|
||||||
import { resolveNodeAtPos } from '../../utils/tree-operations/common'
|
|
||||||
|
|
||||||
const decorationsConf = new Compartment()
|
|
||||||
|
|
||||||
export const debugPanel = () => {
|
|
||||||
const enableDebugPanel = new URLSearchParams(window.location.search).has(
|
|
||||||
'cm_debug_panel'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!enableDebugPanel) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
showPanel.of(createInfoPanel),
|
|
||||||
|
|
||||||
decorationsConf.of(EditorView.decorations.of(Decoration.none)),
|
|
||||||
|
|
||||||
// clear the highlight when the selection changes
|
|
||||||
EditorView.updateListener.of(update => {
|
|
||||||
if (update.selectionSet) {
|
|
||||||
update.view.dispatch({
|
|
||||||
effects: decorationsConf.reconfigure(
|
|
||||||
EditorView.decorations.of(Decoration.none)
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
EditorView.baseTheme({
|
|
||||||
'.ol-cm-debug-panel': {
|
|
||||||
paddingBottom: '24px',
|
|
||||||
},
|
|
||||||
'.ol-cm-debug-panel-type': {
|
|
||||||
backgroundColor: '#138a07',
|
|
||||||
color: '#fff',
|
|
||||||
padding: '0px 4px',
|
|
||||||
marginLeft: '4px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
},
|
|
||||||
'.ol-cm-debug-panel-item': {
|
|
||||||
border: 'none',
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
color: '#000',
|
|
||||||
outline: '1px solid transparent',
|
|
||||||
marginBottom: '2px',
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
'&:hover': {
|
|
||||||
outlineColor: '#000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.ol-cm-debug-panel-position': {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: '0',
|
|
||||||
right: '0',
|
|
||||||
padding: '5px',
|
|
||||||
},
|
|
||||||
'.ol-cm-debug-panel-node-highlight': {
|
|
||||||
backgroundColor: '#ffff0077',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const placeholder = () => document.createElement('div')
|
|
||||||
|
|
||||||
const createInfoPanel = (view: EditorView): Panel => {
|
|
||||||
const dom = document.createElement('div')
|
|
||||||
dom.className = 'ol-cm-debug-panel'
|
|
||||||
dom.append(buildPanelContent(view, view.state))
|
|
||||||
|
|
||||||
return {
|
|
||||||
dom,
|
|
||||||
update(update) {
|
|
||||||
if (update.selectionSet) {
|
|
||||||
// update when the selection changes
|
|
||||||
dom.firstChild!.replaceWith(
|
|
||||||
buildPanelContent(update.view, update.state)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// update when the language is loaded
|
|
||||||
for (const tr of update.transactions) {
|
|
||||||
if (tr.effects.some(effect => effect.is(languageLoadedEffect))) {
|
|
||||||
dom.firstChild!.replaceWith(
|
|
||||||
buildPanelContent(update.view, update.state)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildPanelContent = (
|
|
||||||
view: EditorView,
|
|
||||||
state: EditorState
|
|
||||||
): HTMLDivElement => {
|
|
||||||
const pos = state.selection.main.anchor
|
|
||||||
const ancestors = getAncestorStack(state, pos)
|
|
||||||
|
|
||||||
if (!ancestors) {
|
|
||||||
return placeholder()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ancestors.length > 0) {
|
|
||||||
const node = ancestors[ancestors.length - 1]
|
|
||||||
const nodeBefore = resolveNodeAtPos(state, pos, -1)
|
|
||||||
const nodeAfter = resolveNodeAtPos(state, pos, 1)
|
|
||||||
|
|
||||||
const parts = []
|
|
||||||
if (nodeBefore) {
|
|
||||||
parts.push(`[${nodeBefore.name}]`)
|
|
||||||
}
|
|
||||||
parts.push(node.label)
|
|
||||||
if (nodeAfter) {
|
|
||||||
parts.push(`[${nodeAfter.name}]`)
|
|
||||||
}
|
|
||||||
node.label = parts.join(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
const panelContent = document.createElement('div')
|
|
||||||
panelContent.style.padding = '5px 10px'
|
|
||||||
|
|
||||||
const line = state.doc.lineAt(pos)
|
|
||||||
const column = pos - line.from + 1
|
|
||||||
const positionContainer = document.createElement('div')
|
|
||||||
positionContainer.className = 'ol-cm-debug-panel-position'
|
|
||||||
positionContainer.textContent = `line ${line.number}, col ${column}, pos ${pos}`
|
|
||||||
panelContent.appendChild(positionContainer)
|
|
||||||
|
|
||||||
const stackContainer = document.createElement('div')
|
|
||||||
for (const [index, item] of ancestors.entries()) {
|
|
||||||
if (index > 0) {
|
|
||||||
stackContainer.append(' > ')
|
|
||||||
}
|
|
||||||
const element = document.createElement('button')
|
|
||||||
element.className = 'ol-cm-debug-panel-item'
|
|
||||||
|
|
||||||
const label = document.createElement('span')
|
|
||||||
label.className = 'ol-cm-debug-panel-label'
|
|
||||||
label.textContent = item.label
|
|
||||||
element.append(label)
|
|
||||||
|
|
||||||
if (item.type) {
|
|
||||||
const type = document.createElement('span')
|
|
||||||
type.className = 'ol-cm-debug-panel-type'
|
|
||||||
type.textContent = item.type
|
|
||||||
element.append(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
element.addEventListener('click', () => {
|
|
||||||
view.dispatch({
|
|
||||||
effects: [
|
|
||||||
decorationsConf.reconfigure(
|
|
||||||
EditorView.decorations.of(
|
|
||||||
Decoration.set(
|
|
||||||
Decoration.mark({
|
|
||||||
class: 'ol-cm-debug-panel-node-highlight',
|
|
||||||
}).range(item.from, item.to)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
EditorView.scrollIntoView(item.from, { y: 'center' }),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
stackContainer.append(element)
|
|
||||||
}
|
|
||||||
panelContent.appendChild(stackContainer)
|
|
||||||
|
|
||||||
return panelContent
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ import { shortcuts } from './shortcuts'
|
|||||||
import { linting } from './linting'
|
import { linting } from './linting'
|
||||||
import { LanguageSupport, indentUnit } from '@codemirror/language'
|
import { LanguageSupport, indentUnit } from '@codemirror/language'
|
||||||
import { CompletionSource } from '@codemirror/autocomplete'
|
import { CompletionSource } from '@codemirror/autocomplete'
|
||||||
import { debugPanel } from './debug-panel'
|
|
||||||
import { openAutocomplete } from './open-autocomplete'
|
import { openAutocomplete } from './open-autocomplete'
|
||||||
import { metadata } from './metadata'
|
import { metadata } from './metadata'
|
||||||
import {
|
import {
|
||||||
@@ -35,7 +34,6 @@ export const latex = () => {
|
|||||||
latexIndentService(),
|
latexIndentService(),
|
||||||
linting(),
|
linting(),
|
||||||
metadata(),
|
metadata(),
|
||||||
debugPanel(),
|
|
||||||
openAutocomplete(),
|
openAutocomplete(),
|
||||||
...completionSources.map(completionSource =>
|
...completionSources.map(completionSource =>
|
||||||
LaTeXLanguage.data.of({
|
LaTeXLanguage.data.of({
|
||||||
|
|||||||
Reference in New Issue
Block a user