debug-panel.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { Decoration, EditorView, Panel, showPanel } from '@codemirror/view'
  2. import { languageLoadedEffect } from '../../extensions/language'
  3. import { Compartment, EditorState } from '@codemirror/state'
  4. import { getAncestorStack } from '../../utils/tree-query'
  5. import { resolveNodeAtPos } from '../../utils/tree-operations/common'
  6. const decorationsConf = new Compartment()
  7. export const debugPanel = () => {
  8. const enableDebugPanel = new URLSearchParams(window.location.search).has(
  9. 'cm_debug_panel'
  10. )
  11. if (!enableDebugPanel) {
  12. return []
  13. }
  14. return [
  15. showPanel.of(createInfoPanel),
  16. decorationsConf.of(EditorView.decorations.of(Decoration.none)),
  17. // clear the highlight when the selection changes
  18. EditorView.updateListener.of(update => {
  19. if (update.selectionSet) {
  20. update.view.dispatch({
  21. effects: decorationsConf.reconfigure(
  22. EditorView.decorations.of(Decoration.none)
  23. ),
  24. })
  25. }
  26. }),
  27. EditorView.baseTheme({
  28. '.ol-cm-debug-panel': {
  29. paddingBottom: '24px',
  30. },
  31. '.ol-cm-debug-panel-type': {
  32. backgroundColor: '#138a07',
  33. color: '#fff',
  34. padding: '0px 4px',
  35. marginLeft: '4px',
  36. borderRadius: '4px',
  37. },
  38. '.ol-cm-debug-panel-item': {
  39. border: 'none',
  40. backgroundColor: '#fff',
  41. color: '#000',
  42. outline: '1px solid transparent',
  43. marginBottom: '2px',
  44. display: 'inline-flex',
  45. alignItems: 'center',
  46. '&:hover': {
  47. outlineColor: '#000',
  48. },
  49. },
  50. '.ol-cm-debug-panel-position': {
  51. position: 'absolute',
  52. bottom: '0',
  53. right: '0',
  54. padding: '5px',
  55. },
  56. '.ol-cm-debug-panel-node-highlight': {
  57. backgroundColor: '#ffff0077',
  58. },
  59. }),
  60. ]
  61. }
  62. const placeholder = () => document.createElement('div')
  63. const createInfoPanel = (view: EditorView): Panel => {
  64. const dom = document.createElement('div')
  65. dom.className = 'ol-cm-debug-panel'
  66. dom.append(buildPanelContent(view, view.state))
  67. return {
  68. dom,
  69. update(update) {
  70. if (update.selectionSet) {
  71. // update when the selection changes
  72. dom.firstChild!.replaceWith(
  73. buildPanelContent(update.view, update.state)
  74. )
  75. } else {
  76. // update when the language is loaded
  77. for (const tr of update.transactions) {
  78. if (tr.effects.some(effect => effect.is(languageLoadedEffect))) {
  79. dom.firstChild!.replaceWith(
  80. buildPanelContent(update.view, update.state)
  81. )
  82. }
  83. }
  84. }
  85. },
  86. }
  87. }
  88. const buildPanelContent = (
  89. view: EditorView,
  90. state: EditorState
  91. ): HTMLDivElement => {
  92. const pos = state.selection.main.anchor
  93. const ancestors = getAncestorStack(state, pos)
  94. if (!ancestors) {
  95. return placeholder()
  96. }
  97. if (ancestors.length > 0) {
  98. const node = ancestors[ancestors.length - 1]
  99. const nodeBefore = resolveNodeAtPos(state, pos, -1)
  100. const nodeAfter = resolveNodeAtPos(state, pos, 1)
  101. const parts = []
  102. if (nodeBefore) {
  103. parts.push(`[${nodeBefore.name}]`)
  104. }
  105. parts.push(node.label)
  106. if (nodeAfter) {
  107. parts.push(`[${nodeAfter.name}]`)
  108. }
  109. node.label = parts.join(' ')
  110. }
  111. const panelContent = document.createElement('div')
  112. panelContent.style.padding = '5px 10px'
  113. const line = state.doc.lineAt(pos)
  114. const column = pos - line.from + 1
  115. const positionContainer = document.createElement('div')
  116. positionContainer.className = 'ol-cm-debug-panel-position'
  117. positionContainer.textContent = `line ${line.number}, col ${column}, pos ${pos}`
  118. panelContent.appendChild(positionContainer)
  119. const stackContainer = document.createElement('div')
  120. for (const [index, item] of ancestors.entries()) {
  121. if (index > 0) {
  122. stackContainer.append(' > ')
  123. }
  124. const element = document.createElement('button')
  125. element.className = 'ol-cm-debug-panel-item'
  126. const label = document.createElement('span')
  127. label.className = 'ol-cm-debug-panel-label'
  128. label.textContent = item.label
  129. element.append(label)
  130. if (item.type) {
  131. const type = document.createElement('span')
  132. type.className = 'ol-cm-debug-panel-type'
  133. type.textContent = item.type
  134. element.append(type)
  135. }
  136. element.addEventListener('click', () => {
  137. view.dispatch({
  138. effects: [
  139. decorationsConf.reconfigure(
  140. EditorView.decorations.of(
  141. Decoration.set(
  142. Decoration.mark({
  143. class: 'ol-cm-debug-panel-node-highlight',
  144. }).range(item.from, item.to)
  145. )
  146. )
  147. ),
  148. EditorView.scrollIntoView(item.from, { y: 'center' }),
  149. ],
  150. })
  151. })
  152. stackContainer.append(element)
  153. }
  154. panelContent.appendChild(stackContainer)
  155. return panelContent
  156. }