| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- /**
- * This file is adapted from CodeMirror 6, licensed under the MIT license:
- * https://github.com/codemirror/view/blob/main/src/layer.ts
- */
- import {
- BlockInfo,
- BlockType,
- Direction,
- EditorView,
- Rect,
- RectangleMarker,
- } from '@codemirror/view'
- import { EditorSelection, SelectionRange } from '@codemirror/state'
- import { isVisual } from '../extensions/visual/visual'
- import { round } from 'lodash'
- function canAssumeUniformLineHeights(view: EditorView) {
- return !isVisual(view)
- }
- export const rectangleMarkerForRange = (
- view: EditorView,
- className: string,
- range: SelectionRange
- ): readonly RectangleMarker[] => {
- if (range.empty) {
- const pos = fullHeightCoordsAtPos(view, range.head, range.assoc || 1)
- if (!pos) {
- return []
- }
- const base = getBase(view)
- return [
- new RectangleMarker(
- className,
- pos.left - base.left,
- pos.top - base.top,
- null,
- pos.bottom - pos.top
- ),
- ]
- }
- return rectanglesForRange(view, className, range)
- }
- export function getBase(view: EditorView) {
- const rect = view.scrollDOM.getBoundingClientRect()
- const left =
- view.textDirection === Direction.LTR
- ? rect.left
- : rect.right - view.scrollDOM.clientWidth
- return {
- left: left - view.scrollDOM.scrollLeft,
- top: rect.top - view.scrollDOM.scrollTop,
- }
- }
- function wrappedLine(
- view: EditorView,
- pos: number,
- inside: { from: number; to: number }
- ) {
- const range = EditorSelection.cursor(pos)
- return {
- from: Math.max(
- inside.from,
- view.moveToLineBoundary(range, false, true).from
- ),
- to: Math.min(inside.to, view.moveToLineBoundary(range, true, true).from),
- type: BlockType.Text,
- }
- }
- function blockAt(view: EditorView, pos: number): BlockInfo {
- const line = view.lineBlockAt(pos)
- if (Array.isArray(line.type))
- for (const l of line.type) {
- if (
- l.to > pos ||
- (l.to === pos && (l.to === line.to || l.type === BlockType.Text))
- )
- return l
- }
- return line as any
- }
- // Like coordsAtPos, provides screen coordinates for a document position, but
- // unlike coordsAtPos, the top and bottom represent the full height of the
- // visual line rather than the top and bottom of the text. To do this, it relies
- // on the assumption that all text in the document has the same height and that
- // the line contains no widget or decoration that changes the height of the
- // line. This is, I am fairly certain, a safe assumption in source mode but not
- // in rich text, so in rich text mode this function just returns coordsAtPos.
- export function fullHeightCoordsAtPos(
- view: EditorView,
- pos: number,
- side?: -2 | -1 | 1 | 2 | undefined
- ): Rect | null {
- // @ts-ignore CodeMirror has incorrect type on coordsAtPos
- const coords = view.coordsAtPos(pos, side)
- if (!coords) {
- return null
- }
- if (!canAssumeUniformLineHeights(view)) {
- return coords
- }
- const { left, right } = coords
- const halfLeading =
- (view.defaultLineHeight - (coords.bottom - coords.top)) / 2
- return {
- left,
- right,
- top: round(coords.top - halfLeading, 2),
- bottom: round(coords.bottom + halfLeading, 2),
- }
- }
- // Added to range rectangle's vertical extent to prevent rounding
- // errors from introducing gaps in the rendered content.
- const Epsilon = 0.01
- function rectanglesForRange(
- view: EditorView,
- className: string,
- range: SelectionRange
- ): RectangleMarker[] {
- if (range.to <= view.viewport.from || range.from >= view.viewport.to) {
- return []
- }
- const from = Math.max(range.from, view.viewport.from)
- const to = Math.min(range.to, view.viewport.to)
- const ltr = view.textDirection === Direction.LTR
- const content = view.contentDOM
- const contentRect = content.getBoundingClientRect()
- const base = getBase(view)
- const lineElt = content.querySelector('.cm-line')
- const lineStyle = lineElt && window.getComputedStyle(lineElt)
- const leftSide =
- contentRect.left +
- (lineStyle
- ? parseInt(lineStyle.paddingLeft) +
- Math.min(0, parseInt(lineStyle.textIndent))
- : 0)
- const rightSide =
- contentRect.right - (lineStyle ? parseInt(lineStyle.paddingRight) : 0)
- const startBlock = blockAt(view, from)
- const endBlock = blockAt(view, to)
- let visualStart: { from: number; to: number } | null =
- startBlock.type === BlockType.Text ? startBlock : null
- let visualEnd: { from: number; to: number } | null =
- endBlock.type === BlockType.Text ? endBlock : null
- if (view.lineWrapping) {
- if (visualStart) visualStart = wrappedLine(view, from, visualStart)
- if (visualEnd) visualEnd = wrappedLine(view, to, visualEnd)
- }
- if (visualStart && visualEnd && visualStart.from === visualEnd.from) {
- return pieces(drawForLine(range.from, range.to, visualStart))
- } else {
- const top = visualStart
- ? drawForLine(range.from, null, visualStart)
- : drawForWidget(startBlock, false)
- const bottom = visualEnd
- ? drawForLine(null, range.to, visualEnd)
- : drawForWidget(endBlock, true)
- const between = []
- if (
- (visualStart || startBlock).to <
- (visualEnd || endBlock).from - (visualStart && visualEnd ? 1 : 0)
- )
- between.push(piece(leftSide, top.bottom, rightSide, bottom.top))
- else if (
- top.bottom < bottom.top &&
- view.elementAtHeight((top.bottom + bottom.top) / 2).type ===
- BlockType.Text
- )
- top.bottom = bottom.top = (top.bottom + bottom.top) / 2
- return pieces(top).concat(between).concat(pieces(bottom))
- }
- function piece(left: number, top: number, right: number, bottom: number) {
- return new RectangleMarker(
- className,
- left - base.left,
- top - base.top - Epsilon,
- right - left,
- bottom - top + Epsilon
- )
- }
- function pieces({
- top,
- bottom,
- horizontal,
- }: {
- top: number
- bottom: number
- horizontal: number[]
- }) {
- const pieces = []
- for (let i = 0; i < horizontal.length; i += 2)
- pieces.push(piece(horizontal[i], top, horizontal[i + 1], bottom))
- return pieces
- }
- // Gets passed from/to in line-local positions
- function drawForLine(
- from: null | number,
- to: null | number,
- line: { from: number; to: number }
- ) {
- let top = 1e9
- let bottom = -1e9
- const horizontal: number[] = []
- function addSpan(
- from: number,
- fromOpen: boolean,
- to: number,
- toOpen: boolean,
- dir: Direction
- ) {
- // Passing 2/-2 is a kludge to force the view to return
- // coordinates on the proper side of block widgets, since
- // normalizing the side there, though appropriate for most
- // coordsAtPos queries, would break selection drawing.
- const fromCoords = fullHeightCoordsAtPos(
- view,
- from,
- (from === line.to ? -2 : 2) as any
- )
- const toCoords = fullHeightCoordsAtPos(
- view,
- to,
- (to === line.from ? 2 : -2) as any
- )
- // coordsAtPos can sometimes return null even when the document position
- // is within the viewport. It's not clear exactly when this happens;
- // sometimes, the editor has previously failed to complete a measure.
- if (!fromCoords || !toCoords) {
- return
- }
- top = Math.min(fromCoords.top, toCoords.top, top)
- bottom = Math.max(fromCoords.bottom, toCoords.bottom, bottom)
- if (dir === Direction.LTR)
- horizontal.push(
- ltr && fromOpen ? leftSide : fromCoords.left,
- ltr && toOpen ? rightSide : toCoords.right
- )
- else
- horizontal.push(
- !ltr && toOpen ? leftSide : toCoords.left,
- !ltr && fromOpen ? rightSide : fromCoords.right
- )
- }
- const start = from ?? line.from
- const end = to ?? line.to
- // Split the range by visible range and document line
- for (const r of view.visibleRanges)
- if (r.to > start && r.from < end) {
- for (
- let pos = Math.max(r.from, start), endPos = Math.min(r.to, end);
- ;
- ) {
- const docLine = view.state.doc.lineAt(pos)
- for (const span of view.bidiSpans(docLine)) {
- const spanFrom = span.from + docLine.from
- const spanTo = span.to + docLine.from
- if (spanFrom >= endPos) break
- if (spanTo > pos)
- addSpan(
- Math.max(spanFrom, pos),
- from === null && spanFrom <= start,
- Math.min(spanTo, endPos),
- to === null && spanTo >= end,
- span.dir
- )
- }
- pos = docLine.to + 1
- if (pos >= endPos) break
- }
- }
- if (horizontal.length === 0)
- addSpan(start, from === null, end, to === null, view.textDirection)
- return { top, bottom, horizontal }
- }
- function drawForWidget(block: BlockInfo, top: boolean) {
- const y = contentRect.top + (top ? block.top : block.bottom)
- return { top: y, bottom: y, horizontal: [] }
- }
- }
|