summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/pierre/selection-bridge.ts
blob: 4055ec0874a38889c33c125a08c095a5c4238389 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { type SelectedLineRange } from "@pierre/diffs"

type SelectionKey = "ui.sessionReview.selection.line" | "ui.sessionReview.selection.lines"
type SelectionVars = Record<string, string | number>

type PointerMode = "none" | "text" | "numbers"
type Side = SelectedLineRange["side"]
type LineSpan = Pick<SelectedLineRange, "start" | "end">

export function formatSelectedLineLabel(range: LineSpan, t: (key: SelectionKey, params: SelectionVars) => string) {
  const start = Math.min(range.start, range.end)
  const end = Math.max(range.start, range.end)
  if (start === end) return t("ui.sessionReview.selection.line", { line: start })
  return t("ui.sessionReview.selection.lines", { start, end })
}

export function previewSelectedLines(source: string, range: LineSpan) {
  const start = Math.max(1, Math.min(range.start, range.end))
  const end = Math.max(range.start, range.end)
  const lines = source.split("\n").slice(start - 1, end)
  if (lines.length === 0) return
  return lines.slice(0, 2).join("\n")
}

export function cloneSelectedLineRange(range: SelectedLineRange): SelectedLineRange {
  const next: SelectedLineRange = {
    start: range.start,
    end: range.end,
  }

  if (range.side) next.side = range.side
  if (range.endSide) next.endSide = range.endSide
  return next
}

export function lineInSelectedRange(range: SelectedLineRange | null | undefined, line: number, side?: Side) {
  if (!range) return false

  const start = Math.min(range.start, range.end)
  const end = Math.max(range.start, range.end)
  if (line < start || line > end) return false
  if (!side) return true

  const first = range.side
  const last = range.endSide ?? first
  if (!first && !last) return true
  if (!first || !last) return (first ?? last) === side
  if (first === last) return first === side
  if (line === start) return first === side
  if (line === end) return last === side
  return true
}

export function isSingleLineSelection(range: SelectedLineRange | null) {
  if (!range) return false
  return range.start === range.end && (range.endSide == null || range.endSide === range.side)
}

export function toRange(source: Range | StaticRange): Range {
  if (source instanceof Range) return source
  const range = new Range()
  range.setStart(source.startContainer, source.startOffset)
  range.setEnd(source.endContainer, source.endOffset)
  return range
}

export function restoreShadowTextSelection(root: ShadowRoot | undefined, range: Range | undefined) {
  if (!root || !range) return

  requestAnimationFrame(() => {
    const selection =
      (root as unknown as { getSelection?: () => Selection | null }).getSelection?.() ?? window.getSelection()
    if (!selection) return

    try {
      selection.removeAllRanges()
      selection.addRange(range)
    } catch {}
  })
}

export function createLineNumberSelectionBridge() {
  let mode: PointerMode = "none"
  let line: number | undefined
  let moved = false
  let pending = false

  const clear = () => {
    mode = "none"
    line = undefined
    moved = false
  }

  return {
    begin(numberColumn: boolean, next: number | undefined) {
      if (!numberColumn) {
        mode = "text"
        return
      }

      mode = "numbers"
      line = next
      moved = false
    },
    track(buttons: number, next: number | undefined) {
      if (mode !== "numbers") return false

      if ((buttons & 1) === 0) {
        clear()
        return true
      }

      if (next !== undefined && line !== undefined && next !== line) moved = true
      return true
    },
    finish() {
      const current = mode
      pending = current === "numbers" && moved
      clear()
      return current
    },
    consume(range: SelectedLineRange | null) {
      const result = pending && !isSingleLineSelection(range)
      pending = false
      return result
    },
    reset() {
      pending = false
      clear()
    },
  }
}