@tsplc/debug 0.2.17 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/format.js +36 -18
- package/dist/format.js.map +4 -4
- package/dist/index.js +768 -516
- package/dist/index.js.map +20 -17
- package/dist/manifest.js +6 -2
- package/dist/manifest.js.map +3 -3
- package/dist/paths.js +60 -28
- package/dist/paths.js.map +3 -3
- package/package.json +47 -46
- package/src/adapter.ts +43 -36
- package/src/api.ts +126 -123
- package/src/debugger.ts +584 -467
- package/src/errors.ts +3 -0
- package/src/hardware.ts +127 -96
- package/src/http.ts +147 -116
- package/src/index.ts +34 -4
- package/src/jobs.ts +102 -77
- package/src/login-value.ts +165 -0
- package/src/login.ts +233 -253
- package/src/manifest.ts +200 -194
- package/src/opcua.ts +55 -55
- package/src/paths.ts +230 -157
- package/src/requests.ts +86 -0
- package/src/telemetry.ts +104 -90
- package/src/timeline.ts +96 -61
- package/src/trace.ts +41 -31
- package/src/ui/bits.ts +45 -29
- package/src/ui/client.ts +33 -26
- package/src/ui/dom.ts +189 -108
- package/src/ui/format.ts +127 -72
- package/src/ui/hardware.ts +416 -343
- package/src/ui/header.ts +186 -164
- package/src/ui/index.ts +16 -9
- package/src/ui/jobs.ts +263 -214
- package/src/ui/main.ts +52 -48
- package/src/ui/overview.ts +152 -108
- package/src/ui/recent.ts +329 -276
- package/src/ui/requests.ts +69 -73
- package/src/ui/store.ts +236 -198
- package/src/ui/tree.ts +162 -130
- package/src/ui/variables.ts +557 -431
- package/src/watch.ts +22 -17
- package/src/ui/dev.ts +0 -579
- package/src/ui/fixture.ts +0 -548
package/src/ui/dom.ts
CHANGED
|
@@ -1,142 +1,197 @@
|
|
|
1
1
|
import type { DeviceState } from "../api.ts";
|
|
2
2
|
import type { DebugDevice, SourceRange } from "../manifest.ts";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
formatReadAt,
|
|
5
|
+
formatValue,
|
|
6
|
+
recordLocation,
|
|
7
|
+
sourceLocation,
|
|
8
|
+
valueStateLabels,
|
|
9
9
|
} from "./format.ts";
|
|
10
10
|
import type { Store, Topic } from "./store.ts";
|
|
11
11
|
import type { TreeNode } from "./tree.ts";
|
|
12
12
|
|
|
13
13
|
type Child = Node | string | null | undefined | false;
|
|
14
|
-
type Attribute =
|
|
14
|
+
type Attribute =
|
|
15
|
+
string | number | boolean | undefined | ((event: Event) => void);
|
|
15
16
|
|
|
16
17
|
export const h = <K extends keyof HTMLElementTagNameMap>(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
tag: K,
|
|
19
|
+
attributes: Record<string, Attribute> = {},
|
|
20
|
+
...children: Child[]
|
|
20
21
|
): HTMLElementTagNameMap[K] => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const element = document.createElement(tag);
|
|
23
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
24
|
+
if (value === undefined || value === false) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "function") {
|
|
28
|
+
element.addEventListener(name.slice(2), value);
|
|
29
|
+
} else {
|
|
30
|
+
element.setAttribute(name, value === true ? "" : String(value));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return fill(element, ...children);
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
/** Replaces an element's children, skipping the absent ones. */
|
|
31
37
|
export const fill = <E extends Element>(element: E, ...children: Child[]) => {
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
element.replaceChildren(
|
|
39
|
+
...children.filter((child): child is Node | string => Boolean(child)),
|
|
40
|
+
);
|
|
41
|
+
return element;
|
|
34
42
|
};
|
|
35
43
|
|
|
36
44
|
/** A page: built once per route, then told which store topics changed each frame. */
|
|
37
45
|
export interface View {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
element: HTMLElement;
|
|
47
|
+
update(topics: Set<Topic>): void;
|
|
48
|
+
dispose?(): void;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
export const empty = (text: string) => h("p", { class: "empty" }, text);
|
|
44
52
|
|
|
53
|
+
/** A table of `class` (`grid`, styled as a panel) with a header row of `columns`. */
|
|
54
|
+
export const table = (
|
|
55
|
+
attributes: { class: string },
|
|
56
|
+
columns: string[],
|
|
57
|
+
body: HTMLElement,
|
|
58
|
+
) =>
|
|
59
|
+
h(
|
|
60
|
+
"table",
|
|
61
|
+
attributes,
|
|
62
|
+
h("thead", {}, h("tr", {}, ...columns.map((label) => h("th", {}, label)))),
|
|
63
|
+
body,
|
|
64
|
+
);
|
|
65
|
+
|
|
45
66
|
const locationLink = (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
{ label, href }: { label: string; href?: string },
|
|
68
|
+
title: string,
|
|
69
|
+
text?: string,
|
|
49
70
|
) =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
href
|
|
72
|
+
? h(
|
|
73
|
+
"a",
|
|
74
|
+
{ class: "src", href, title: `Open ${title} in VS Code` },
|
|
75
|
+
text ?? label,
|
|
76
|
+
)
|
|
77
|
+
: h("span", { class: "src", title }, text ?? label);
|
|
53
78
|
|
|
54
79
|
export const sourceLink = (store: Store, range: SourceRange, text?: string) => {
|
|
55
|
-
|
|
56
|
-
|
|
80
|
+
const location = sourceLocation(
|
|
81
|
+
store.manifest,
|
|
82
|
+
store.session.projectRoot,
|
|
83
|
+
range,
|
|
84
|
+
);
|
|
85
|
+
return locationLink(location, location.label, text);
|
|
57
86
|
};
|
|
58
87
|
|
|
59
88
|
/** A telemetry record's `source`, a project-relative `file:line`. */
|
|
60
89
|
export const recordSourceLink = (store: Store, source: string) =>
|
|
61
|
-
|
|
90
|
+
locationLink(recordLocation(store.session.projectRoot, source), source);
|
|
62
91
|
|
|
63
|
-
const variableHref = (path: string) =>
|
|
92
|
+
export const variableHref = (path: string) =>
|
|
93
|
+
`#/variables?path=${encodeURIComponent(path)}`;
|
|
94
|
+
|
|
95
|
+
export const deviceHref = (id: string) =>
|
|
96
|
+
`#/hardware/${encodeURIComponent(id)}`;
|
|
64
97
|
|
|
65
98
|
export const pathLink = (path: string, text: Node | string = path) =>
|
|
66
|
-
|
|
99
|
+
h(
|
|
100
|
+
"a",
|
|
101
|
+
{
|
|
102
|
+
class: "path",
|
|
103
|
+
href: variableHref(path),
|
|
104
|
+
title: `Show ${path} in Variables`,
|
|
105
|
+
},
|
|
106
|
+
text,
|
|
107
|
+
);
|
|
67
108
|
|
|
68
109
|
/** A path that wraps only between its segments. */
|
|
69
110
|
export const wrappingPath = (path: string) =>
|
|
70
|
-
|
|
111
|
+
h(
|
|
112
|
+
"span",
|
|
113
|
+
{},
|
|
114
|
+
...path.split(/(?=[.[])/).flatMap((segment) => [h("wbr"), segment]),
|
|
115
|
+
);
|
|
71
116
|
|
|
72
117
|
const setText = (element: HTMLElement, text: string) => {
|
|
73
|
-
|
|
118
|
+
if (element.textContent !== text) {
|
|
119
|
+
element.textContent = text;
|
|
120
|
+
}
|
|
74
121
|
};
|
|
75
122
|
const setClass = (element: HTMLElement, name: string) => {
|
|
76
|
-
|
|
123
|
+
if (element.className !== name) {
|
|
124
|
+
element.className = name;
|
|
125
|
+
}
|
|
77
126
|
};
|
|
78
127
|
|
|
79
128
|
/** A variable's value and its quality and age, kept current by `update()` from the store. */
|
|
80
129
|
export class LiveValue {
|
|
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
|
-
|
|
130
|
+
readonly value = h("span");
|
|
131
|
+
readonly meta = h("span");
|
|
132
|
+
|
|
133
|
+
constructor(
|
|
134
|
+
private readonly store: Store,
|
|
135
|
+
readonly node: TreeNode,
|
|
136
|
+
) {
|
|
137
|
+
this.update();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Whether the stream should carry this path. */
|
|
141
|
+
get subscribed() {
|
|
142
|
+
return this.node.readable && !this.node.constant;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
update() {
|
|
146
|
+
const { node, store } = this;
|
|
147
|
+
if (node.constant) {
|
|
148
|
+
return this.show(
|
|
149
|
+
"constant",
|
|
150
|
+
formatValue(store.manifest, node.type, node.constant.value),
|
|
151
|
+
"constant",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (!node.readable) {
|
|
155
|
+
return this.show(
|
|
156
|
+
"unavailable",
|
|
157
|
+
"—",
|
|
158
|
+
"not readable",
|
|
159
|
+
"This build can't read it from the controller",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const { sample, state } = store.live(node.path);
|
|
163
|
+
if (!sample) {
|
|
164
|
+
return this.show("unavailable", "—", "not read", "Not read yet");
|
|
165
|
+
}
|
|
166
|
+
const meta =
|
|
167
|
+
state === "unavailable" || state === "bad"
|
|
168
|
+
? valueStateLabels[state]
|
|
169
|
+
: `${valueStateLabels[state]} · ${formatReadAt(sample.receivedAt)}`;
|
|
170
|
+
this.show(
|
|
171
|
+
state,
|
|
172
|
+
formatValue(store.manifest, node.type, sample.value),
|
|
173
|
+
meta,
|
|
174
|
+
`${sample.status}\nread ${sample.receivedAt}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private show(state: string, value: string, meta: string, title?: string) {
|
|
179
|
+
setClass(this.value, `value s-${state}`);
|
|
180
|
+
setClass(this.meta, `meta s-${state}`);
|
|
181
|
+
setText(this.value, value);
|
|
182
|
+
setText(this.meta, meta);
|
|
183
|
+
if (title !== undefined && this.meta.title !== title) {
|
|
184
|
+
this.meta.title = title;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
132
187
|
}
|
|
133
188
|
|
|
134
189
|
export type Health = "ok" | "fault" | "stale" | "unknown";
|
|
135
190
|
|
|
136
191
|
interface HealthView {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
192
|
+
health: Health;
|
|
193
|
+
text: string;
|
|
194
|
+
detail: string;
|
|
140
195
|
}
|
|
141
196
|
|
|
142
197
|
/**
|
|
@@ -144,25 +199,51 @@ interface HealthView {
|
|
|
144
199
|
* login" while nothing can read it, never "OK" by default.
|
|
145
200
|
*/
|
|
146
201
|
export const deviceHealth = (store: Store, device: DebugDevice): HealthView => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
202
|
+
const state: DeviceState | undefined = store.devices.get(device.id);
|
|
203
|
+
const { connection } = store.session;
|
|
204
|
+
if (!device.health) {
|
|
205
|
+
return {
|
|
206
|
+
health: "unknown",
|
|
207
|
+
text: "unknown",
|
|
208
|
+
detail: "This build reads no health for this device",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (connection.state === "unavailable") {
|
|
212
|
+
return {
|
|
213
|
+
health: "unknown",
|
|
214
|
+
text: "needs login",
|
|
215
|
+
detail: connection.error ?? "",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (!state || state.health.state === "unknown") {
|
|
219
|
+
return {
|
|
220
|
+
health: "unknown",
|
|
221
|
+
text: "unknown",
|
|
222
|
+
detail: "The driver's state has not been read",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const { health } = state;
|
|
226
|
+
const detail = [health.name, health.message].filter(Boolean).join(" · ");
|
|
227
|
+
if (health.state === "stale") {
|
|
228
|
+
return { health: "stale", text: "stale", detail };
|
|
229
|
+
}
|
|
230
|
+
if (!store.connected || health.quality !== "good") {
|
|
231
|
+
return { health: "stale", text: `stale, was ${health.state}`, detail };
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
health: health.state,
|
|
235
|
+
text: health.state === "ok" ? "OK" : health.state,
|
|
236
|
+
detail,
|
|
237
|
+
};
|
|
165
238
|
};
|
|
166
239
|
|
|
240
|
+
/** The devices whose driver state this build reads: the ones a page listing health asks for. */
|
|
241
|
+
export const devicesWithHealth = (store: Store) =>
|
|
242
|
+
store.manifest.devices.filter((device) => device.health).map(({ id }) => id);
|
|
243
|
+
|
|
167
244
|
export const healthBadge = (health: HealthView) =>
|
|
168
|
-
|
|
245
|
+
h(
|
|
246
|
+
"span",
|
|
247
|
+
{ class: `badge h-${health.health}`, title: health.detail },
|
|
248
|
+
health.text,
|
|
249
|
+
);
|
package/src/ui/format.ts
CHANGED
|
@@ -3,123 +3,178 @@ import type { DebugManifest, SourceRange } from "../manifest.ts";
|
|
|
3
3
|
import { typeOf } from "../paths.ts";
|
|
4
4
|
|
|
5
5
|
/** How a value is shown: each state has its own styling and label, so none passes for live. */
|
|
6
|
-
export type ValueState =
|
|
6
|
+
export type ValueState =
|
|
7
|
+
"live" | "uncertain" | "bad" | "stale" | "unavailable" | "constant";
|
|
7
8
|
|
|
8
9
|
export const valueStateLabels: Record<ValueState, string> = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
live: "good",
|
|
11
|
+
uncertain: "uncertain",
|
|
12
|
+
bad: "bad",
|
|
13
|
+
stale: "stale",
|
|
14
|
+
unavailable: "unavailable",
|
|
15
|
+
constant: "constant",
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
/** A sample is stale when the controller connection is down or the sample is older than three sample intervals. */
|
|
18
19
|
export const valueState = (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
quality: Quality,
|
|
21
|
+
ageMs: number,
|
|
22
|
+
context: { connected: boolean; intervalMs: number },
|
|
22
23
|
): ValueState => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
if (quality === "unavailable") {
|
|
25
|
+
return "unavailable";
|
|
26
|
+
}
|
|
27
|
+
if (!context.connected || ageMs > 3 * context.intervalMs) {
|
|
28
|
+
return "stale";
|
|
29
|
+
}
|
|
30
|
+
if (quality === "bad") {
|
|
31
|
+
return "bad";
|
|
32
|
+
}
|
|
33
|
+
if (quality === "uncertain") {
|
|
34
|
+
return "uncertain";
|
|
35
|
+
}
|
|
36
|
+
return "live";
|
|
28
37
|
};
|
|
29
38
|
|
|
30
39
|
export const formatAge = (ms: number) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
if (ms < 1000) {
|
|
41
|
+
return "now";
|
|
42
|
+
}
|
|
43
|
+
if (ms < 60_000) {
|
|
44
|
+
return `${Math.floor(ms / 1000)} s ago`;
|
|
45
|
+
}
|
|
46
|
+
if (ms < 3_600_000) {
|
|
47
|
+
return `${Math.floor(ms / 60_000)} min ago`;
|
|
48
|
+
}
|
|
49
|
+
return `${Math.floor(ms / 3_600_000)} h ago`;
|
|
35
50
|
};
|
|
36
51
|
|
|
37
52
|
export const formatDuration = (ms: number) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
if (ms < 1000) {
|
|
54
|
+
return `${Math.round(ms)} ms`;
|
|
55
|
+
}
|
|
56
|
+
if (ms < 60_000) {
|
|
57
|
+
return `${(ms / 1000).toFixed(1)} s`;
|
|
58
|
+
}
|
|
59
|
+
return `${Math.floor(ms / 60_000)} min ${Math.floor((ms % 60_000) / 1000)} s`;
|
|
41
60
|
};
|
|
42
61
|
|
|
43
62
|
/** How often values are read: `250 ms`, `1 s`, `1.5 s`. */
|
|
44
|
-
export const formatInterval = (ms: number) =>
|
|
63
|
+
export const formatInterval = (ms: number) =>
|
|
64
|
+
ms < 1000 ? `${ms} ms` : `${ms / 1000} s`;
|
|
45
65
|
|
|
46
66
|
/** Local time of day with milliseconds, for ordering events by eye. */
|
|
47
67
|
export const formatClock = (iso: string) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
const date = new Date(iso);
|
|
69
|
+
const pad = (value: number, width = 2) => String(value).padStart(width, "0");
|
|
70
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
|
|
51
71
|
};
|
|
52
72
|
|
|
53
73
|
/** When a value was read, to the second: `read 14:03:27`. */
|
|
54
|
-
export const formatReadAt = (iso: string) =>
|
|
74
|
+
export const formatReadAt = (iso: string) =>
|
|
75
|
+
`read ${formatClock(iso).slice(0, 8)}`;
|
|
55
76
|
|
|
56
77
|
/** A value of unknown type: booleans as IEC literals, strings quoted. */
|
|
57
78
|
export const formatScalar = (value: unknown): string => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
if (typeof value === "boolean") {
|
|
80
|
+
return value ? "TRUE" : "FALSE";
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === "number") {
|
|
83
|
+
return Number.isInteger(value)
|
|
84
|
+
? String(value)
|
|
85
|
+
: String(Number(value.toPrecision(6)));
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
return JSON.stringify(value);
|
|
89
|
+
}
|
|
90
|
+
if (value === null || value === undefined) {
|
|
91
|
+
return "—";
|
|
92
|
+
}
|
|
93
|
+
return JSON.stringify(value);
|
|
64
94
|
};
|
|
65
95
|
|
|
66
96
|
/** A value in the program's terms: enum members by name, whether the host decoded them already or not. */
|
|
67
|
-
export const formatValue = (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
export const formatValue = (
|
|
98
|
+
manifest: DebugManifest,
|
|
99
|
+
typeId: string,
|
|
100
|
+
value: unknown,
|
|
101
|
+
): string => {
|
|
102
|
+
const type = typeOf(manifest, typeId);
|
|
103
|
+
if (type.kind !== "enum") {
|
|
104
|
+
return formatScalar(value);
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === "string" && Object.hasOwn(type.members, value)) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
if (typeof value !== "number") {
|
|
110
|
+
return formatScalar(value);
|
|
111
|
+
}
|
|
112
|
+
const member = Object.entries(type.members).find(
|
|
113
|
+
([, number]) => number === value,
|
|
114
|
+
)?.[0];
|
|
115
|
+
return member ?? `${value} (not a ${type.name})`;
|
|
74
116
|
};
|
|
75
117
|
|
|
76
118
|
export const typeLabel = (manifest: DebugManifest, typeId: string): string => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
119
|
+
const type = typeOf(manifest, typeId);
|
|
120
|
+
switch (type.kind) {
|
|
121
|
+
case "scalar":
|
|
122
|
+
return type.iec;
|
|
123
|
+
case "enum":
|
|
124
|
+
case "record":
|
|
125
|
+
return type.name;
|
|
126
|
+
case "array":
|
|
127
|
+
return `${typeLabel(manifest, type.element)}[${type.length}]`;
|
|
128
|
+
}
|
|
87
129
|
};
|
|
88
130
|
|
|
89
131
|
const PROPERTY_TEXT = 16;
|
|
90
132
|
|
|
91
133
|
/** A record's properties as `barcode=CON-MD3-884… scanner=3`, long strings cut short. */
|
|
92
134
|
export const formatProperties = (properties: Record<string, unknown>) =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
Object.entries(properties)
|
|
136
|
+
.map(([key, value]) => {
|
|
137
|
+
const text = typeof value === "string" ? value : formatScalar(value);
|
|
138
|
+
return `${key}=${text.length > PROPERTY_TEXT ? `${text.slice(0, PROPERTY_TEXT)}…` : text}`;
|
|
139
|
+
})
|
|
140
|
+
.join(" ");
|
|
99
141
|
|
|
100
142
|
/** A VS Code link to a project file, when the project root on the debugger host is known. */
|
|
101
|
-
const fileHref = (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
143
|
+
const fileHref = (
|
|
144
|
+
projectRoot: string | undefined,
|
|
145
|
+
file: string,
|
|
146
|
+
line: number,
|
|
147
|
+
column: number,
|
|
148
|
+
) => {
|
|
149
|
+
if (projectRoot === undefined) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const root = projectRoot.replaceAll("\\", "/").replace(/\/$/, "");
|
|
153
|
+
const absolute = `${root}/${file}`.replace(/^\/?/, "/");
|
|
154
|
+
return `vscode://file${absolute}:${line}:${column}`;
|
|
106
155
|
};
|
|
107
156
|
|
|
108
157
|
/** `file:line`, and a VS Code link when the project root on the debugger host is known. */
|
|
109
158
|
export const sourceLocation = (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
159
|
+
manifest: DebugManifest,
|
|
160
|
+
projectRoot: string | undefined,
|
|
161
|
+
range: SourceRange,
|
|
113
162
|
) => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
163
|
+
const file = manifest.sources[range.file] ?? `source ${range.file}`;
|
|
164
|
+
const href = fileHref(projectRoot, file, range.line, range.column);
|
|
165
|
+
return { label: `${file}:${range.line}`, ...(href && { href }) };
|
|
117
166
|
};
|
|
118
167
|
|
|
119
168
|
/** A record's project-relative `file:line` as `basename:line`, with the same VS Code link. */
|
|
120
|
-
export const recordLocation = (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
169
|
+
export const recordLocation = (
|
|
170
|
+
projectRoot: string | undefined,
|
|
171
|
+
source: string,
|
|
172
|
+
) => {
|
|
173
|
+
const [, file = source, line] = /^(.*):(\d+)$/.exec(source) ?? [];
|
|
174
|
+
const label = `${file.slice(file.lastIndexOf("/") + 1)}${line ? `:${line}` : ""}`;
|
|
175
|
+
const href =
|
|
176
|
+
line === undefined
|
|
177
|
+
? undefined
|
|
178
|
+
: fileHref(projectRoot, file, Number(line), 1);
|
|
179
|
+
return { label, ...(href && { href }) };
|
|
125
180
|
};
|