pi-x-ide 0.1.0
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/LICENSE +201 -0
- package/README.md +138 -0
- package/README.zh.md +138 -0
- package/dist/src/pi/commands.d.ts +12 -0
- package/dist/src/pi/commands.js +96 -0
- package/dist/src/pi/commands.js.map +1 -0
- package/dist/src/pi/connection.d.ts +27 -0
- package/dist/src/pi/connection.js +136 -0
- package/dist/src/pi/connection.js.map +1 -0
- package/dist/src/pi/context.d.ts +8 -0
- package/dist/src/pi/context.js +66 -0
- package/dist/src/pi/context.js.map +1 -0
- package/dist/src/pi/discovery.d.ts +11 -0
- package/dist/src/pi/discovery.js +79 -0
- package/dist/src/pi/discovery.js.map +1 -0
- package/dist/src/pi/index.d.ts +4 -0
- package/dist/src/pi/index.js +182 -0
- package/dist/src/pi/index.js.map +1 -0
- package/dist/src/pi/state.d.ts +24 -0
- package/dist/src/pi/state.js +12 -0
- package/dist/src/pi/state.js.map +1 -0
- package/dist/src/pi/ui.d.ts +6 -0
- package/dist/src/pi/ui.js +84 -0
- package/dist/src/pi/ui.js.map +1 -0
- package/dist/src/shared/format.d.ts +23 -0
- package/dist/src/shared/format.js +84 -0
- package/dist/src/shared/format.js.map +1 -0
- package/dist/src/shared/paths.d.ts +5 -0
- package/dist/src/shared/paths.js +50 -0
- package/dist/src/shared/paths.js.map +1 -0
- package/dist/src/shared/protocol.d.ts +92 -0
- package/dist/src/shared/protocol.js +8 -0
- package/dist/src/shared/protocol.js.map +1 -0
- package/dist/src/shared/schema.d.ts +9 -0
- package/dist/src/shared/schema.js +94 -0
- package/dist/src/shared/schema.js.map +1 -0
- package/dist/src/shared/ws.d.ts +2 -0
- package/dist/src/shared/ws.js +12 -0
- package/dist/src/shared/ws.js.map +1 -0
- package/dist/test/shared.test.d.ts +1 -0
- package/dist/test/shared.test.js +101 -0
- package/dist/test/shared.test.js.map +1 -0
- package/package.json +55 -0
- package/src/pi/commands.ts +122 -0
- package/src/pi/connection.ts +155 -0
- package/src/pi/context.ts +82 -0
- package/src/pi/discovery.ts +88 -0
- package/src/pi/index.ts +190 -0
- package/src/pi/state.ts +29 -0
- package/src/pi/ui.ts +85 -0
- package/src/shared/format.ts +95 -0
- package/src/shared/paths.ts +47 -0
- package/src/shared/protocol.ts +107 -0
- package/src/shared/schema.ts +113 -0
- package/src/shared/ws.ts +8 -0
package/src/pi/ui.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent" with { "resolution-mode": "import" };
|
|
2
|
+
import { describeRanges } from "../shared/format";
|
|
3
|
+
import { toRelativeDisplayPath } from "../shared/paths";
|
|
4
|
+
import type { PiIdeRuntime } from "./state";
|
|
5
|
+
|
|
6
|
+
export function updateIdeUi(runtime: PiIdeRuntime, ctx: ExtensionContext | undefined = runtime.ctx): void {
|
|
7
|
+
if (!ctx?.hasUI) return;
|
|
8
|
+
|
|
9
|
+
ctx.ui.setWidget(
|
|
10
|
+
"pi-x-ide",
|
|
11
|
+
(_tui, theme) => ({
|
|
12
|
+
render(width: number): string[] {
|
|
13
|
+
const status = buildStatusLine(runtime, ctx.cwd);
|
|
14
|
+
const text = truncatePlainStatus(status, width);
|
|
15
|
+
const pad = " ".repeat(Math.max(0, width - text.length));
|
|
16
|
+
const color = isPendingEditorContext(runtime) ? "success" : "dim";
|
|
17
|
+
return [pad + theme.fg(color, text)];
|
|
18
|
+
},
|
|
19
|
+
invalidate() {},
|
|
20
|
+
}),
|
|
21
|
+
{ placement: "aboveEditor" },
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function clearIdeUi(runtime: PiIdeRuntime, ctx: ExtensionContext | undefined = runtime.ctx): void {
|
|
26
|
+
if (!ctx?.hasUI) return;
|
|
27
|
+
ctx.ui.setWidget("pi-x-ide", undefined);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function truncatePlainStatus(text: string, width: number): string {
|
|
31
|
+
if (width <= 0) return "";
|
|
32
|
+
if (text.length <= width) return text;
|
|
33
|
+
if (width <= 3) return ".".repeat(width);
|
|
34
|
+
return `${text.slice(0, width - 3)}...`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isPendingEditorContext(runtime: PiIdeRuntime): boolean {
|
|
38
|
+
return !!runtime.latestSelection && runtime.attachState === "pending";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildStatusLine(runtime: PiIdeRuntime, cwd?: string): string {
|
|
42
|
+
if (!runtime.enabled || runtime.connectionStatus === "disabled") return "IDE: off";
|
|
43
|
+
if (runtime.connectionStatus === "connecting") return "IDE: connecting";
|
|
44
|
+
if (runtime.connectionStatus === "error")
|
|
45
|
+
return `IDE: error${runtime.connectionMessage ? ` ${runtime.connectionMessage}` : ""}`;
|
|
46
|
+
if (runtime.connectionStatus !== "connected") return "IDE: disconnected";
|
|
47
|
+
|
|
48
|
+
const ide = runtime.connectedServer?.ide ?? runtime.currentCandidate?.lock.ide ?? "ide";
|
|
49
|
+
const selection = runtime.latestSelection;
|
|
50
|
+
if (!selection) return `IDE: ${ide} ✓`;
|
|
51
|
+
const rel = toRelativeDisplayPath(selection.filePath, selection.workspaceFolder, cwd);
|
|
52
|
+
const range = describeRanges(selection.ranges);
|
|
53
|
+
return `IDE: ${ide} ✓ ${rel}${range === "open file" ? "" : range} ${runtime.attachState}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildWidget(runtime: PiIdeRuntime, cwd?: string): string[] | undefined {
|
|
57
|
+
if (!runtime.enabled || runtime.connectionStatus === "disabled") return undefined;
|
|
58
|
+
if (
|
|
59
|
+
runtime.connectionStatus !== "connected" &&
|
|
60
|
+
runtime.connectionStatus !== "connecting" &&
|
|
61
|
+
runtime.connectionStatus !== "error"
|
|
62
|
+
) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
const ide = runtime.connectedServer?.name ?? runtime.currentCandidate?.lock.name ?? "IDE";
|
|
68
|
+
lines.push(`IDE: ${ide} (${runtime.connectionStatus})`);
|
|
69
|
+
|
|
70
|
+
if (runtime.currentCandidate) {
|
|
71
|
+
lines.push(`Workspace: ${runtime.currentCandidate.workspaceFolder}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (runtime.latestSelection) {
|
|
75
|
+
const selection = runtime.latestSelection;
|
|
76
|
+
lines.push(`File: ${toRelativeDisplayPath(selection.filePath, selection.workspaceFolder, cwd)}`);
|
|
77
|
+
lines.push(`Range: ${describeRanges(selection.ranges)}`);
|
|
78
|
+
lines.push(`Attach: ${runtime.attachState}`);
|
|
79
|
+
if (selection.receivedAt) lines.push(`Updated: ${new Date(selection.receivedAt).toLocaleTimeString()}`);
|
|
80
|
+
} else if (runtime.connectionMessage) {
|
|
81
|
+
lines.push(runtime.connectionMessage);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { EditorSelectionSnapshot, SelectionRange } from "./protocol";
|
|
2
|
+
import { toRelativeDisplayPath } from "./paths";
|
|
3
|
+
|
|
4
|
+
export type RangeFormat = "comma" | "dash";
|
|
5
|
+
|
|
6
|
+
export function rangeToLineSpan(range: SelectionRange): { startLine: number; endLine: number } {
|
|
7
|
+
return {
|
|
8
|
+
startLine: range.selection.start.line + 1,
|
|
9
|
+
endLine: range.selection.end.line + 1,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatLineSpan(range: SelectionRange, format: RangeFormat = "comma"): string {
|
|
14
|
+
const { startLine, endLine } = rangeToLineSpan(range);
|
|
15
|
+
if (startLine === endLine) return `#L${startLine}`;
|
|
16
|
+
return format === "dash" ? `#L${startLine}-L${endLine}` : `#L${startLine},${endLine}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatRangeMention(
|
|
20
|
+
snapshot: EditorSelectionSnapshot,
|
|
21
|
+
options: { cwd?: string; format?: RangeFormat } = {},
|
|
22
|
+
): string {
|
|
23
|
+
const rel = toRelativeDisplayPath(snapshot.filePath, snapshot.workspaceFolder, options.cwd);
|
|
24
|
+
const first = snapshot.ranges[0];
|
|
25
|
+
return first ? `@${rel}${formatLineSpan(first, options.format)}` : `@${rel}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ParsedRangeMention {
|
|
29
|
+
path: string;
|
|
30
|
+
startLine?: number;
|
|
31
|
+
endLine?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseRangeMention(input: string): ParsedRangeMention | undefined {
|
|
35
|
+
const match = input.trim().match(/^@(.+?)(?:#L(\d+)(?:(?:,|-L?)(\d+))?)?$/);
|
|
36
|
+
if (!match) return undefined;
|
|
37
|
+
const startLine = match[2] ? Number(match[2]) : undefined;
|
|
38
|
+
const endLine = match[3] ? Number(match[3]) : startLine;
|
|
39
|
+
if (startLine !== undefined && (!Number.isInteger(startLine) || startLine < 1)) return undefined;
|
|
40
|
+
if (endLine !== undefined && (!Number.isInteger(endLine) || endLine < 1)) return undefined;
|
|
41
|
+
return { path: match[1], startLine, endLine };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function describeRanges(ranges: SelectionRange[], format: RangeFormat = "comma"): string {
|
|
45
|
+
if (ranges.length === 0) return "open file";
|
|
46
|
+
if (ranges.length === 1) return formatLineSpan(ranges[0], format);
|
|
47
|
+
return ranges.map((range, index) => `${index + 1}:${formatLineSpan(range, format)}`).join(" ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function snapshotKey(snapshot: EditorSelectionSnapshot): string {
|
|
51
|
+
return JSON.stringify({
|
|
52
|
+
source: snapshot.source,
|
|
53
|
+
filePath: snapshot.filePath,
|
|
54
|
+
workspaceFolder: snapshot.workspaceFolder,
|
|
55
|
+
ranges: snapshot.ranges.map((range) => ({
|
|
56
|
+
start: range.selection.start,
|
|
57
|
+
end: range.selection.end,
|
|
58
|
+
textLength: range.text.length,
|
|
59
|
+
})),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatEditorContext(
|
|
64
|
+
snapshot: EditorSelectionSnapshot,
|
|
65
|
+
options: { cwd?: string; maxChars?: number } = {},
|
|
66
|
+
): string {
|
|
67
|
+
const rel = toRelativeDisplayPath(snapshot.filePath, snapshot.workspaceFolder, options.cwd);
|
|
68
|
+
const maxChars = options.maxChars ?? 24_000;
|
|
69
|
+
|
|
70
|
+
if (snapshot.ranges.length === 0) {
|
|
71
|
+
return `<system-reminder>\nThe user currently has \`${rel}\` open in ${snapshot.source}. This may or may not be relevant.\n</system-reminder>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sections: string[] = [];
|
|
75
|
+
let remaining = maxChars;
|
|
76
|
+
let truncated = false;
|
|
77
|
+
|
|
78
|
+
for (const [index, range] of snapshot.ranges.entries()) {
|
|
79
|
+
const { startLine, endLine } = rangeToLineSpan(range);
|
|
80
|
+
const label = snapshot.ranges.length === 1 ? "The user selected" : `Selection ${index + 1}`;
|
|
81
|
+
let text = range.text;
|
|
82
|
+
if (text.length > remaining) {
|
|
83
|
+
text = text.slice(0, Math.max(0, remaining));
|
|
84
|
+
truncated = true;
|
|
85
|
+
}
|
|
86
|
+
remaining -= text.length;
|
|
87
|
+
sections.push(
|
|
88
|
+
`${label} lines ${startLine}-${endLine} from \`${rel}\` in ${snapshot.source}:\n\n\`\`\`\n${text}\n\`\`\``,
|
|
89
|
+
);
|
|
90
|
+
if (remaining <= 0) break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const suffix = truncated ? "\n\n[Selection text truncated to keep the prompt size bounded.]" : "";
|
|
94
|
+
return `<system-reminder>\n${sections.join("\n\n")}\n\nThis may or may not be relevant.${suffix}\n</system-reminder>`;
|
|
95
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { LOCK_DIR_ENV } from "./protocol";
|
|
4
|
+
|
|
5
|
+
export function resolveLockDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
6
|
+
return env[LOCK_DIR_ENV] && env[LOCK_DIR_ENV].trim()
|
|
7
|
+
? resolve(env[LOCK_DIR_ENV])
|
|
8
|
+
: resolve(homedir(), ".pi", "pi-x-ide");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizePath(input: string): string {
|
|
12
|
+
return resolve(input);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isPathInsideOrEqual(parent: string, child: string): boolean {
|
|
16
|
+
const normalizedParent = normalizePath(parent);
|
|
17
|
+
const normalizedChild = normalizePath(child);
|
|
18
|
+
if (normalizedParent === normalizedChild) return true;
|
|
19
|
+
const rel = relative(normalizedParent, normalizedChild);
|
|
20
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function relationshipMatchLength(workspaceFolder: string, cwd: string): number {
|
|
24
|
+
const workspace = normalizePath(workspaceFolder);
|
|
25
|
+
const active = normalizePath(cwd);
|
|
26
|
+
|
|
27
|
+
// Best case: Pi is started inside the IDE workspace.
|
|
28
|
+
if (isPathInsideOrEqual(workspace, active)) {
|
|
29
|
+
return workspace.length + 10_000;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Accept the inverse relationship for monorepos where Pi starts above a nested workspace,
|
|
33
|
+
// but rank it lower than a workspace containing cwd.
|
|
34
|
+
if (isPathInsideOrEqual(active, workspace)) {
|
|
35
|
+
return active.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function toRelativeDisplayPath(filePath: string, workspaceFolder?: string, cwd?: string): string {
|
|
42
|
+
const base = workspaceFolder ?? cwd;
|
|
43
|
+
if (!base) return filePath;
|
|
44
|
+
const rel = relative(base, filePath);
|
|
45
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) return filePath;
|
|
46
|
+
return rel.split(sep).join("/");
|
|
47
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export const PROTOCOL_VERSION = 1;
|
|
2
|
+
export const LOCK_DIR_ENV = "PI_X_IDE_LOCK_DIR";
|
|
3
|
+
export const AUTH_HEADER = "x-pi-x-ide-authorization";
|
|
4
|
+
export const LOCK_FILE_EXTENSION = ".lock";
|
|
5
|
+
|
|
6
|
+
export type IdeSource = "vscode" | "zed" | "unknown";
|
|
7
|
+
export type Transport = "ws";
|
|
8
|
+
|
|
9
|
+
export interface IdeLockFile {
|
|
10
|
+
version: 1;
|
|
11
|
+
ide: IdeSource;
|
|
12
|
+
name: string;
|
|
13
|
+
transport: Transport;
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
authToken: string;
|
|
17
|
+
workspaceFolders: string[];
|
|
18
|
+
pid?: number;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LockFileCandidate {
|
|
24
|
+
path: string;
|
|
25
|
+
lock: IdeLockFile;
|
|
26
|
+
mtimeMs: number;
|
|
27
|
+
matchLength: number;
|
|
28
|
+
workspaceFolder: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface JsonRpcRequest<TParams = unknown> {
|
|
32
|
+
jsonrpc: "2.0";
|
|
33
|
+
id: number | string;
|
|
34
|
+
method: string;
|
|
35
|
+
params?: TParams;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface JsonRpcResponse<TResult = unknown> {
|
|
39
|
+
jsonrpc: "2.0";
|
|
40
|
+
id: number | string;
|
|
41
|
+
result?: TResult;
|
|
42
|
+
error?: { code: number; message: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface JsonRpcNotification<TParams = unknown> {
|
|
46
|
+
jsonrpc: "2.0";
|
|
47
|
+
method: string;
|
|
48
|
+
params?: TParams;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface InitializeParams {
|
|
52
|
+
protocolVersion: number;
|
|
53
|
+
client: {
|
|
54
|
+
name: "pi-x-ide";
|
|
55
|
+
version: string;
|
|
56
|
+
};
|
|
57
|
+
cwd: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface InitializeResult {
|
|
61
|
+
protocolVersion: number;
|
|
62
|
+
server: {
|
|
63
|
+
name: string;
|
|
64
|
+
version?: string;
|
|
65
|
+
ide: IdeSource;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Position {
|
|
70
|
+
line: number;
|
|
71
|
+
character: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SelectionRange {
|
|
75
|
+
text: string;
|
|
76
|
+
selection: {
|
|
77
|
+
start: Position;
|
|
78
|
+
end: Position;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface EditorSelectionSnapshot {
|
|
83
|
+
source: IdeSource;
|
|
84
|
+
filePath: string;
|
|
85
|
+
workspaceFolder?: string;
|
|
86
|
+
ranges: SelectionRange[];
|
|
87
|
+
receivedAt?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SelectionChangedParams extends EditorSelectionSnapshot {}
|
|
91
|
+
|
|
92
|
+
export interface SelectionClearedParams {
|
|
93
|
+
source: IdeSource;
|
|
94
|
+
reason: "no-active-editor";
|
|
95
|
+
receivedAt?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface AtMentionedParams extends EditorSelectionSnapshot {
|
|
99
|
+
rangeText: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type IdeNotification =
|
|
103
|
+
| JsonRpcNotification<SelectionChangedParams>
|
|
104
|
+
| JsonRpcNotification<SelectionClearedParams>
|
|
105
|
+
| JsonRpcNotification<AtMentionedParams>;
|
|
106
|
+
|
|
107
|
+
export type AttachState = "pending" | "sent" | "idle";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AtMentionedParams,
|
|
3
|
+
EditorSelectionSnapshot,
|
|
4
|
+
IdeLockFile,
|
|
5
|
+
IdeSource,
|
|
6
|
+
JsonRpcRequest,
|
|
7
|
+
SelectionChangedParams,
|
|
8
|
+
SelectionClearedParams,
|
|
9
|
+
SelectionRange,
|
|
10
|
+
} from "./protocol";
|
|
11
|
+
|
|
12
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isString(value: unknown): value is string {
|
|
17
|
+
return typeof value === "string";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
21
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isIdeSource(value: unknown): value is IdeSource {
|
|
25
|
+
return value === "vscode" || value === "zed" || value === "unknown";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isIdeLockFile(value: unknown): value is IdeLockFile {
|
|
29
|
+
if (!isRecord(value)) return false;
|
|
30
|
+
return (
|
|
31
|
+
value.version === 1 &&
|
|
32
|
+
isIdeSource(value.ide) &&
|
|
33
|
+
isString(value.name) &&
|
|
34
|
+
value.transport === "ws" &&
|
|
35
|
+
isString(value.host) &&
|
|
36
|
+
isFiniteNumber(value.port) &&
|
|
37
|
+
value.port > 0 &&
|
|
38
|
+
value.port <= 65_535 &&
|
|
39
|
+
isString(value.authToken) &&
|
|
40
|
+
Array.isArray(value.workspaceFolders) &&
|
|
41
|
+
value.workspaceFolders.every(isString) &&
|
|
42
|
+
(value.pid === undefined || isFiniteNumber(value.pid)) &&
|
|
43
|
+
isString(value.createdAt) &&
|
|
44
|
+
isString(value.updatedAt)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isPosition(value: unknown): value is { line: number; character: number } {
|
|
49
|
+
return (
|
|
50
|
+
isRecord(value) &&
|
|
51
|
+
isFiniteNumber(value.line) &&
|
|
52
|
+
isFiniteNumber(value.character) &&
|
|
53
|
+
value.line >= 0 &&
|
|
54
|
+
value.character >= 0
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isSelectionRange(value: unknown): value is SelectionRange {
|
|
59
|
+
if (!isRecord(value) || !isString(value.text) || !isRecord(value.selection)) return false;
|
|
60
|
+
return isPosition(value.selection.start) && isPosition(value.selection.end);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isEditorSelectionSnapshot(value: unknown): value is EditorSelectionSnapshot {
|
|
64
|
+
if (!isRecord(value)) return false;
|
|
65
|
+
return (
|
|
66
|
+
isIdeSource(value.source) &&
|
|
67
|
+
isString(value.filePath) &&
|
|
68
|
+
(value.workspaceFolder === undefined || isString(value.workspaceFolder)) &&
|
|
69
|
+
Array.isArray(value.ranges) &&
|
|
70
|
+
value.ranges.every(isSelectionRange) &&
|
|
71
|
+
(value.receivedAt === undefined || isFiniteNumber(value.receivedAt))
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isSelectionChangedParams(value: unknown): value is SelectionChangedParams {
|
|
76
|
+
return isEditorSelectionSnapshot(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isSelectionClearedParams(value: unknown): value is SelectionClearedParams {
|
|
80
|
+
return (
|
|
81
|
+
isRecord(value) &&
|
|
82
|
+
isIdeSource(value.source) &&
|
|
83
|
+
value.reason === "no-active-editor" &&
|
|
84
|
+
(value.receivedAt === undefined || isFiniteNumber(value.receivedAt))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isAtMentionedParams(value: unknown): value is AtMentionedParams {
|
|
89
|
+
return isEditorSelectionSnapshot(value) && isString((value as unknown as Record<string, unknown>).rangeText);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseJsonObject(input: string): Record<string, unknown> | undefined {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(input) as unknown;
|
|
95
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function parseLockFileContent(content: string): IdeLockFile | undefined {
|
|
102
|
+
const parsed = parseJsonObject(content);
|
|
103
|
+
return isIdeLockFile(parsed) ? parsed : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isJsonRpcRequest(value: unknown): value is JsonRpcRequest {
|
|
107
|
+
return (
|
|
108
|
+
isRecord(value) &&
|
|
109
|
+
value.jsonrpc === "2.0" &&
|
|
110
|
+
(isString(value.id) || isFiniteNumber(value.id)) &&
|
|
111
|
+
isString(value.method)
|
|
112
|
+
);
|
|
113
|
+
}
|
package/src/shared/ws.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import type { RawData } from "ws";
|
|
3
|
+
|
|
4
|
+
export function decodeRawData(raw: RawData): string {
|
|
5
|
+
if (Array.isArray(raw)) return Buffer.concat(raw).toString("utf8");
|
|
6
|
+
if (Buffer.isBuffer(raw)) return raw.toString("utf8");
|
|
7
|
+
return Buffer.from(raw).toString("utf8");
|
|
8
|
+
}
|