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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs"
import { ComponentProps, createEffect, createMemo, onCleanup, splitProps } from "solid-js"
import { createDefaultOptions, styleVariables } from "../pierre"
import { getWorkerPool } from "../pierre/worker"
type SelectionSide = "additions" | "deletions"
export type CodeProps<T = {}> = FileOptions<T> & {
file: FileContents
annotations?: LineAnnotation<T>[]
selectedLines?: SelectedLineRange | null
class?: string
classList?: ComponentProps<"div">["classList"]
}
function findElement(node: Node | null): HTMLElement | undefined {
if (!node) return
if (node instanceof HTMLElement) return node
return node.parentElement ?? undefined
}
function findLineNumber(node: Node | null): number | undefined {
const element = findElement(node)
if (!element) return
const line = element.closest("[data-line]")
if (!(line instanceof HTMLElement)) return
const value = parseInt(line.dataset.line ?? "", 10)
if (Number.isNaN(value)) return
return value
}
function findSide(node: Node | null): SelectionSide | undefined {
const element = findElement(node)
if (!element) return
const code = element.closest("[data-code]")
if (!(code instanceof HTMLElement)) return
if (code.hasAttribute("data-deletions")) return "deletions"
return "additions"
}
export function Code<T>(props: CodeProps<T>) {
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["file", "class", "classList", "annotations", "selectedLines"])
const file = createMemo(
() =>
new File<T>(
{
...createDefaultOptions<T>("unified"),
...others,
},
getWorkerPool("unified"),
),
)
const getRoot = () => {
const host = container.querySelector("diffs-container")
if (!(host instanceof HTMLElement)) return
const root = host.shadowRoot
if (!root) return
return root
}
const handleMouseUp = () => {
if (props.enableLineSelection !== true) return
const root = getRoot()
if (!root) return
const selection = window.getSelection()
if (!selection || selection.isCollapsed) return
const anchor = selection.anchorNode
const focus = selection.focusNode
if (!anchor || !focus) return
if (!root.contains(anchor) || !root.contains(focus)) return
const start = findLineNumber(anchor)
const end = findLineNumber(focus)
if (start === undefined || end === undefined) return
const startSide = findSide(anchor)
const endSide = findSide(focus)
const side = startSide ?? endSide
const range: SelectedLineRange = {
start,
end,
}
if (side) range.side = side
if (endSide && side && endSide !== side) range.endSide = endSide
file().setSelectedLines(range)
}
createEffect(() => {
const current = file()
onCleanup(() => {
current.cleanUp()
})
})
createEffect(() => {
container.innerHTML = ""
file().render({
file: local.file,
lineAnnotations: local.annotations,
containerWrapper: container,
})
})
createEffect(() => {
file().setSelectedLines(local.selectedLines ?? null)
})
createEffect(() => {
if (props.enableLineSelection !== true) return
container.addEventListener("mouseup", handleMouseUp)
onCleanup(() => {
container.removeEventListener("mouseup", handleMouseUp)
})
})
return (
<div
data-component="code"
style={styleVariables}
classList={{
...(local.classList || {}),
[local.class ?? ""]: !!local.class,
}}
ref={container}
/>
)
}
|