inline-background.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  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.theme({
  34. '.cm-content': {
  35. '--half-leading': halfLeading + 'px',
  36. },
  37. })
  38. }
  39. /**
  40. * A custom extension which measures the height of the first non-space position and provides a CSS variable via an editor theme,
  41. * used for extending elements over the whole line height using padding.
  42. */
  43. const plugin = ViewPlugin.define(
  44. view => {
  45. let halfLeading = 0
  46. const measureRequest = {
  47. read: () => {
  48. return measureHalfLeading(view)
  49. },
  50. write: (newHalfLeading: number) => {
  51. if (newHalfLeading !== halfLeading) {
  52. halfLeading = newHalfLeading
  53. window.setTimeout(() =>
  54. view.dispatch({
  55. effects: themeConf.reconfigure(createTheme(newHalfLeading)),
  56. annotations: changeHalfLeadingAnnotation.of(true),
  57. })
  58. )
  59. }
  60. },
  61. }
  62. return {
  63. update(update) {
  64. // Ignore any update triggered by this plugin
  65. if (
  66. update.transactions.some(tr =>
  67. tr.annotation(changeHalfLeadingAnnotation)
  68. )
  69. ) {
  70. return
  71. }
  72. if (
  73. hasFontLoadedEffect(update) ||
  74. (update.geometryChanged && !update.docChanged) ||
  75. update.transactions.some(tr => tr.annotation(themeOptionsChange)) ||
  76. hasLanguageLoadedEffect(update)
  77. ) {
  78. view.requestMeasure(measureRequest)
  79. }
  80. },
  81. }
  82. },
  83. {
  84. provide: () => [themeConf.of(createTheme(0))],
  85. }
  86. )
  87. export const inlineBackground = (visual: boolean) => {
  88. return sourceOnly(visual, [fontLoad, plugin])
  89. }