layer.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /**
  2. * This file is adapted from CodeMirror 6, licensed under the MIT license:
  3. * https://github.com/codemirror/view/blob/main/src/layer.ts
  4. */
  5. import {
  6. BlockInfo,
  7. BlockType,
  8. Direction,
  9. EditorView,
  10. Rect,
  11. RectangleMarker,
  12. } from '@codemirror/view'
  13. import { EditorSelection, SelectionRange } from '@codemirror/state'
  14. import { isVisual } from '../extensions/visual/visual'
  15. import { round } from 'lodash'
  16. function canAssumeUniformLineHeights(view: EditorView) {
  17. return !isVisual(view)
  18. }
  19. export const rectangleMarkerForRange = (
  20. view: EditorView,
  21. className: string,
  22. range: SelectionRange
  23. ): readonly RectangleMarker[] => {
  24. if (range.empty) {
  25. const pos = fullHeightCoordsAtPos(view, range.head, range.assoc || 1)
  26. if (!pos) {
  27. return []
  28. }
  29. const base = getBase(view)
  30. return [
  31. new RectangleMarker(
  32. className,
  33. pos.left - base.left,
  34. pos.top - base.top,
  35. null,
  36. pos.bottom - pos.top
  37. ),
  38. ]
  39. }
  40. return rectanglesForRange(view, className, range)
  41. }
  42. export function getBase(view: EditorView) {
  43. const rect = view.scrollDOM.getBoundingClientRect()
  44. const left =
  45. view.textDirection === Direction.LTR
  46. ? rect.left
  47. : rect.right - view.scrollDOM.clientWidth
  48. return {
  49. left: left - view.scrollDOM.scrollLeft,
  50. top: rect.top - view.scrollDOM.scrollTop,
  51. }
  52. }
  53. function wrappedLine(
  54. view: EditorView,
  55. pos: number,
  56. inside: { from: number; to: number }
  57. ) {
  58. const range = EditorSelection.cursor(pos)
  59. return {
  60. from: Math.max(
  61. inside.from,
  62. view.moveToLineBoundary(range, false, true).from
  63. ),
  64. to: Math.min(inside.to, view.moveToLineBoundary(range, true, true).from),
  65. type: BlockType.Text,
  66. }
  67. }
  68. function blockAt(view: EditorView, pos: number): BlockInfo {
  69. const line = view.lineBlockAt(pos)
  70. if (Array.isArray(line.type))
  71. for (const l of line.type) {
  72. if (
  73. l.to > pos ||
  74. (l.to === pos && (l.to === line.to || l.type === BlockType.Text))
  75. )
  76. return l
  77. }
  78. return line as any
  79. }
  80. // Like coordsAtPos, provides screen coordinates for a document position, but
  81. // unlike coordsAtPos, the top and bottom represent the full height of the
  82. // visual line rather than the top and bottom of the text. To do this, it relies
  83. // on the assumption that all text in the document has the same height and that
  84. // the line contains no widget or decoration that changes the height of the
  85. // line. This is, I am fairly certain, a safe assumption in source mode but not
  86. // in rich text, so in rich text mode this function just returns coordsAtPos.
  87. export function fullHeightCoordsAtPos(
  88. view: EditorView,
  89. pos: number,
  90. side?: -2 | -1 | 1 | 2 | undefined
  91. ): Rect | null {
  92. // @ts-ignore CodeMirror has incorrect type on coordsAtPos
  93. const coords = view.coordsAtPos(pos, side)
  94. if (!coords) {
  95. return null
  96. }
  97. if (!canAssumeUniformLineHeights(view)) {
  98. return coords
  99. }
  100. const { left, right } = coords
  101. const halfLeading =
  102. (view.defaultLineHeight - (coords.bottom - coords.top)) / 2
  103. return {
  104. left,
  105. right,
  106. top: round(coords.top - halfLeading, 2),
  107. bottom: round(coords.bottom + halfLeading, 2),
  108. }
  109. }
  110. // Added to range rectangle's vertical extent to prevent rounding
  111. // errors from introducing gaps in the rendered content.
  112. const Epsilon = 0.01
  113. function rectanglesForRange(
  114. view: EditorView,
  115. className: string,
  116. range: SelectionRange
  117. ): RectangleMarker[] {
  118. if (range.to <= view.viewport.from || range.from >= view.viewport.to) {
  119. return []
  120. }
  121. const from = Math.max(range.from, view.viewport.from)
  122. const to = Math.min(range.to, view.viewport.to)
  123. const ltr = view.textDirection === Direction.LTR
  124. const content = view.contentDOM
  125. const contentRect = content.getBoundingClientRect()
  126. const base = getBase(view)
  127. const lineElt = content.querySelector('.cm-line')
  128. const lineStyle = lineElt && window.getComputedStyle(lineElt)
  129. const leftSide =
  130. contentRect.left +
  131. (lineStyle
  132. ? parseInt(lineStyle.paddingLeft) +
  133. Math.min(0, parseInt(lineStyle.textIndent))
  134. : 0)
  135. const rightSide =
  136. contentRect.right - (lineStyle ? parseInt(lineStyle.paddingRight) : 0)
  137. const startBlock = blockAt(view, from)
  138. const endBlock = blockAt(view, to)
  139. let visualStart: { from: number; to: number } | null =
  140. startBlock.type === BlockType.Text ? startBlock : null
  141. let visualEnd: { from: number; to: number } | null =
  142. endBlock.type === BlockType.Text ? endBlock : null
  143. if (view.lineWrapping) {
  144. if (visualStart) visualStart = wrappedLine(view, from, visualStart)
  145. if (visualEnd) visualEnd = wrappedLine(view, to, visualEnd)
  146. }
  147. if (visualStart && visualEnd && visualStart.from === visualEnd.from) {
  148. return pieces(drawForLine(range.from, range.to, visualStart))
  149. } else {
  150. const top = visualStart
  151. ? drawForLine(range.from, null, visualStart)
  152. : drawForWidget(startBlock, false)
  153. const bottom = visualEnd
  154. ? drawForLine(null, range.to, visualEnd)
  155. : drawForWidget(endBlock, true)
  156. const between = []
  157. if ((visualStart || startBlock).to < (visualEnd || endBlock).from - 1)
  158. between.push(piece(leftSide, top.bottom, rightSide, bottom.top))
  159. else if (
  160. top.bottom < bottom.top &&
  161. view.elementAtHeight((top.bottom + bottom.top) / 2).type ===
  162. BlockType.Text
  163. )
  164. top.bottom = bottom.top = (top.bottom + bottom.top) / 2
  165. return pieces(top).concat(between).concat(pieces(bottom))
  166. }
  167. function piece(left: number, top: number, right: number, bottom: number) {
  168. return new RectangleMarker(
  169. className,
  170. left - base.left,
  171. top - base.top - Epsilon,
  172. right - left,
  173. bottom - top + Epsilon
  174. )
  175. }
  176. function pieces({
  177. top,
  178. bottom,
  179. horizontal,
  180. }: {
  181. top: number
  182. bottom: number
  183. horizontal: number[]
  184. }) {
  185. const pieces = []
  186. for (let i = 0; i < horizontal.length; i += 2)
  187. pieces.push(piece(horizontal[i], top, horizontal[i + 1], bottom))
  188. return pieces
  189. }
  190. // Gets passed from/to in line-local positions
  191. function drawForLine(
  192. from: null | number,
  193. to: null | number,
  194. line: { from: number; to: number }
  195. ) {
  196. let top = 1e9
  197. let bottom = -1e9
  198. const horizontal: number[] = []
  199. function addSpan(
  200. from: number,
  201. fromOpen: boolean,
  202. to: number,
  203. toOpen: boolean,
  204. dir: Direction
  205. ) {
  206. // Passing 2/-2 is a kludge to force the view to return
  207. // coordinates on the proper side of block widgets, since
  208. // normalizing the side there, though appropriate for most
  209. // coordsAtPos queries, would break selection drawing.
  210. const fromCoords = fullHeightCoordsAtPos(
  211. view,
  212. from,
  213. (from === line.to ? -2 : 2) as any
  214. )
  215. const toCoords = fullHeightCoordsAtPos(
  216. view,
  217. to,
  218. (to === line.from ? 2 : -2) as any
  219. )
  220. // coordsAtPos can sometimes return null even when the document position
  221. // is within the viewport. It's not clear exactly when this happens;
  222. // sometimes, the editor has previously failed to complete a measure.
  223. if (!fromCoords || !toCoords) {
  224. return
  225. }
  226. top = Math.min(fromCoords.top, toCoords.top, top)
  227. bottom = Math.max(fromCoords.bottom, toCoords.bottom, bottom)
  228. if (dir === Direction.LTR)
  229. horizontal.push(
  230. ltr && fromOpen ? leftSide : fromCoords.left,
  231. ltr && toOpen ? rightSide : toCoords.right
  232. )
  233. else
  234. horizontal.push(
  235. !ltr && toOpen ? leftSide : toCoords.left,
  236. !ltr && fromOpen ? rightSide : fromCoords.right
  237. )
  238. }
  239. const start = from ?? line.from
  240. const end = to ?? line.to
  241. // Split the range by visible range and document line
  242. for (const r of view.visibleRanges)
  243. if (r.to > start && r.from < end) {
  244. for (
  245. let pos = Math.max(r.from, start), endPos = Math.min(r.to, end);
  246. ;
  247. ) {
  248. const docLine = view.state.doc.lineAt(pos)
  249. for (const span of view.bidiSpans(docLine)) {
  250. const spanFrom = span.from + docLine.from
  251. const spanTo = span.to + docLine.from
  252. if (spanFrom >= endPos) break
  253. if (spanTo > pos)
  254. addSpan(
  255. Math.max(spanFrom, pos),
  256. from === null && spanFrom <= start,
  257. Math.min(spanTo, endPos),
  258. to === null && spanTo >= end,
  259. span.dir
  260. )
  261. }
  262. pos = docLine.to + 1
  263. if (pos >= endPos) break
  264. }
  265. }
  266. if (horizontal.length === 0)
  267. addSpan(start, from === null, end, to === null, view.textDirection)
  268. return { top, bottom, horizontal }
  269. }
  270. function drawForWidget(block: BlockInfo, top: boolean) {
  271. const y = contentRect.top + (top ? block.top : block.bottom)
  272. return { top: y, bottom: y, horizontal: [] }
  273. }
  274. }