InlineWidget.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import type { editor } from 'monaco-editor'
  2. import { PropsWithChildren, useEffect, useMemo, useRef } from 'react'
  3. import { createPortal } from 'react-dom'
  4. export interface InlineWidgetProps {
  5. /**
  6. * The Monaco editor instance.
  7. */
  8. editor: editor.IStandaloneCodeEditor | editor.IStandaloneDiffEditor
  9. /**
  10. * ID used by Monaco to reference this widget
  11. */
  12. id: string
  13. /**
  14. * The line number after or before which this zone should appear.
  15. * Use 0 to place a view zone before the first line number.
  16. */
  17. afterLineNumber: number
  18. beforeLineNumber?: number
  19. /**
  20. * The height in lines of the view zone.
  21. * @default 1
  22. */
  23. heightInLines?: number
  24. }
  25. /**
  26. * Adds an inline widget to a Monaco editor instance. Acts as a
  27. * container for custom widgets that are rendered inline.
  28. *
  29. * Implemented using the same techniques VS Code uses for their inline
  30. * widgets (such as "Go to References"), but built for React.
  31. * Uses a combination of a view zone and overlay widget.
  32. */
  33. const InlineWidget = ({
  34. children,
  35. editor,
  36. id,
  37. beforeLineNumber,
  38. afterLineNumber = 0,
  39. heightInLines = 1,
  40. }: PropsWithChildren<InlineWidgetProps>) => {
  41. const lineNumber = beforeLineNumber ?? afterLineNumber
  42. const key = `${id}-${lineNumber.toString()}`
  43. const containerElement = useMemo(() => document.createElement('div'), [])
  44. const zoneIdRef = useRef<string>(null)
  45. const viewZoneRef = useRef<{
  46. top: number
  47. height: number
  48. heightInLines: number
  49. }>({ top: 0, height: 0, heightInLines: heightInLines })
  50. // Get the appropriate editor instance for diff editor
  51. const targetEditor = 'getModifiedEditor' in editor ? editor.getModifiedEditor() : editor
  52. const recalculateLayout = () => {
  53. const layoutInfo = targetEditor.getLayoutInfo()
  54. if (!layoutInfo) {
  55. return
  56. }
  57. containerElement.style.left = `${layoutInfo.contentLeft}px`
  58. containerElement.style.top = `${viewZoneRef.current.top}px`
  59. containerElement.style.width = `${layoutInfo.width - layoutInfo.contentLeft - 20}px`
  60. containerElement.style.height = `${viewZoneRef.current.height}px`
  61. }
  62. const createViewZone = () => {
  63. targetEditor.changeViewZones((accessor) => {
  64. // Remove existing zone if it exists
  65. if (zoneIdRef.current) {
  66. accessor.removeZone(zoneIdRef.current)
  67. }
  68. // Create new zone with current height
  69. zoneIdRef.current = accessor.addZone({
  70. afterLineNumber: beforeLineNumber ?? afterLineNumber,
  71. heightInLines: viewZoneRef.current.heightInLines,
  72. domNode: document.createElement('div'),
  73. onDomNodeTop: (top) => {
  74. viewZoneRef.current.top = top
  75. recalculateLayout()
  76. },
  77. onComputedHeight: (height) => {
  78. viewZoneRef.current.height = height
  79. recalculateLayout()
  80. },
  81. })
  82. })
  83. }
  84. // Initial setup of view zone and overlay widget
  85. useEffect(() => {
  86. const overlayWidget: editor.IOverlayWidget = {
  87. getId: () => id,
  88. getDomNode: () => containerElement,
  89. getPosition: () => null,
  90. }
  91. createViewZone()
  92. targetEditor.addOverlayWidget(overlayWidget)
  93. // Remove the view zone & overlay widget on unmount
  94. return () => {
  95. targetEditor.changeViewZones((accessor) => {
  96. if (zoneIdRef.current) {
  97. accessor.removeZone(zoneIdRef.current)
  98. }
  99. targetEditor.removeOverlayWidget(overlayWidget)
  100. })
  101. }
  102. }, [targetEditor, id, beforeLineNumber, afterLineNumber]) // Note: heightInLines removed from deps
  103. // Update view zone height when heightInLines changes
  104. useEffect(() => {
  105. if (heightInLines !== viewZoneRef.current.heightInLines) {
  106. viewZoneRef.current.heightInLines = heightInLines
  107. createViewZone()
  108. }
  109. }, [heightInLines])
  110. return createPortal(children, containerElement, key)
  111. }
  112. export default InlineWidget