inline-background.ts 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { Annotation, Compartment } from '@codemirror/state'
  2. import { EditorView, ViewPlugin } from '@codemirror/view'
  3. import { themeOptionsChange } from './theme'
  4. import { sourceOnly } from './visual/visual'
  5. import { round } from 'lodash'
  6. import { hasLanguageLoadedEffect } from './language'
  7. import { fontLoad, hasFontLoadedEffect } from './font-load'
  8. const themeConf = new Compartment()
  9. const changeHalfLeadingAnnotation = Annotation.define<boolean>()
  10. function firstVisibleNonSpacePos(view: EditorView) {
  11. for (const range of view.visibleRanges) {
  12. const match = /\S/.exec(view.state.sliceDoc(range.from, range.to))
  13. if (match) {
  14. return range.from + match.index
  15. }
  16. }
  17. return null
  18. }
  19. function measureHalfLeading(view: EditorView) {
  20. const pos = firstVisibleNonSpacePos(view)
  21. if (pos === null) {
  22. return 0
  23. }
  24. const coords = view.coordsAtPos(pos)
  25. if (!coords) {
  26. return 0
  27. }
  28. const inlineBoxHeight = coords.bottom - coords.top
  29. // Rounding prevents gaps appearing in some situations
  30. return round((view.defaultLineHeight - inlineBoxHeight) / 2, 2)
  31. }
  32. function createTheme(halfLeading: number) {
  33. return EditorView.contentAttributes.of({
  34. style: `--half-leading: ${halfLeading}px`,
  35. })
  36. }
  37. /**
  38. * A custom extension which measures the height of the first non-space position and provides a CSS variable via an editor theme,
  39. * used for extending elements over the whole line height using padding.
  40. */
  41. const plugin = ViewPlugin.define(
  42. view => {
  43. let halfLeading = 0
  44. const measureRequest = {
  45. read: () => {
  46. return measureHalfLeading(view)
  47. },
  48. write: (newHalfLeading: number) => {
  49. if (newHalfLeading !== halfLeading) {
  50. halfLeading = newHalfLeading
  51. window.setTimeout(() =>
  52. view.dispatch({
  53. effects: themeConf.reconfigure(createTheme(newHalfLeading)),
  54. annotations: changeHalfLeadingAnnotation.of(true),
  55. })
  56. )
  57. }
  58. },
  59. }
  60. return {
  61. update(update) {
  62. // Ignore any update triggered by this plugin
  63. if (
  64. update.transactions.some(tr =>
  65. tr.annotation(changeHalfLeadingAnnotation)
  66. )
  67. ) {
  68. return
  69. }
  70. if (
  71. hasFontLoadedEffect(update) ||
  72. (update.geometryChanged && !update.docChanged) ||
  73. update.transactions.some(tr => tr.annotation(themeOptionsChange)) ||
  74. hasLanguageLoadedEffect(update)
  75. ) {
  76. view.requestMeasure(measureRequest)
  77. }
  78. },
  79. }
  80. },
  81. {
  82. provide: () => [themeConf.of(createTheme(0))],
  83. }
  84. )
  85. export const inlineBackground = (visual: boolean) => {
  86. return sourceOnly(visual, [fontLoad, plugin])
  87. }