@saifymatteo/pi-x-ide 0.0.0-stage → 1.20.2
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/CHANGELOG.md +328 -0
- package/CONTRIBUTING.md +119 -0
- package/CONTRIBUTING.zh-CN.md +106 -0
- package/LICENSE +201 -0
- package/README.md +33 -2
- package/README.zh-CN.md +34 -0
- package/RELEASE.md +213 -0
- package/config.example.json +9 -0
- package/dist/src/pi/chunks/chunk-4RHJAL4S.js +200 -0
- package/dist/src/pi/chunks/chunk-UUXVCTOS.js +819 -0
- package/dist/src/pi/chunks/config-ui-LO3DVQFC.js +8073 -0
- package/dist/src/pi/chunks/runtime-services-BVQBAAWT.js +19569 -0
- package/dist/src/pi/commands.js +126 -0
- package/dist/src/pi/config-ui.js +127 -0
- package/dist/src/pi/connection.js +237 -0
- package/dist/src/pi/context.js +30 -0
- package/dist/src/pi/diagnostic-renderer.js +326 -0
- package/dist/src/pi/diagnostic-types.js +1 -0
- package/dist/src/pi/diagnostics.js +104 -0
- package/dist/src/pi/discovery.js +135 -0
- package/dist/src/pi/effect-boundary.js +5 -0
- package/dist/src/pi/ide-host.js +68 -0
- package/dist/src/pi/index.js +636 -0
- package/dist/src/pi/install.js +308 -0
- package/dist/src/pi/reconnect.js +57 -0
- package/dist/src/pi/runtime-loader.js +38 -0
- package/dist/src/pi/runtime-services.js +356 -0
- package/dist/src/pi/safety.js +61 -0
- package/dist/src/pi/selection.js +18 -0
- package/dist/src/pi/state.js +13 -0
- package/dist/src/pi/ui.js +304 -0
- package/dist/src/pi/zed.js +409 -0
- package/dist/src/shared/config-options.js +45 -0
- package/dist/src/shared/config.js +183 -0
- package/dist/src/shared/display-width.js +99 -0
- package/dist/src/shared/effect-errors.js +36 -0
- package/dist/src/shared/effect-runtime.js +36 -0
- package/dist/src/shared/effect-schema.js +120 -0
- package/dist/src/shared/errors.js +48 -0
- package/dist/src/shared/format.js +92 -0
- package/dist/src/shared/ide-server.js +167 -0
- package/dist/src/shared/jsonrpc-guard.js +12 -0
- package/dist/src/shared/lock-file-heartbeat.js +58 -0
- package/dist/src/shared/lock-file.js +50 -0
- package/dist/src/shared/paths.js +79 -0
- package/dist/src/shared/platform.js +57 -0
- package/dist/src/shared/protocol.js +3 -0
- package/dist/src/shared/schema.js +37 -0
- package/dist/src/shared/version.js +1 -0
- package/dist/src/shared/ws.js +8 -0
- package/docs/explanation/architecture.md +123 -0
- package/docs/explanation/discovery.md +113 -0
- package/docs/explanation/effect.md +127 -0
- package/docs/how-to/install-jetbrains.md +58 -0
- package/docs/how-to/install-neovim.md +138 -0
- package/docs/how-to/install-vscode.md +75 -0
- package/docs/how-to/install-zed.md +70 -0
- package/docs/how-to/troubleshoot-connection.md +103 -0
- package/docs/how-to/use-wsl2.md +90 -0
- package/docs/reference/commands.md +59 -0
- package/docs/reference/configuration.md +101 -0
- package/docs/reference/feature-parity.md +26 -0
- package/docs/reference/protocol.md +71 -0
- package/docs/specs/ide-protocol.md +215 -0
- package/docs/tutorials/get-started.md +133 -0
- package/docs/zh-CN/explanation/architecture.md +93 -0
- package/docs/zh-CN/explanation/discovery.md +71 -0
- package/docs/zh-CN/explanation/effect.md +90 -0
- package/docs/zh-CN/how-to/install-jetbrains.md +49 -0
- package/docs/zh-CN/how-to/install-neovim.md +126 -0
- package/docs/zh-CN/how-to/install-vscode.md +67 -0
- package/docs/zh-CN/how-to/install-zed.md +56 -0
- package/docs/zh-CN/how-to/troubleshoot-connection.md +85 -0
- package/docs/zh-CN/how-to/use-wsl2.md +75 -0
- package/docs/zh-CN/reference/commands.md +54 -0
- package/docs/zh-CN/reference/configuration.md +94 -0
- package/docs/zh-CN/reference/feature-parity.md +21 -0
- package/docs/zh-CN/reference/protocol.md +54 -0
- package/docs/zh-CN/tutorials/get-started.md +101 -0
- package/package.json +86 -4
- package/schemas/config.json +77 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const ESC = String.fromCharCode(27);
|
|
2
|
+
const ANSI_PATTERN = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
3
|
+
const EMOJI_VARIATION_SELECTOR = 0xfe0f;
|
|
4
|
+
const COMBINING_ENCLOSING_KEYCAP = 0x20e3;
|
|
5
|
+
const graphemeSegmenter = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : undefined;
|
|
6
|
+
export function visibleWidth(input) {
|
|
7
|
+
let width = 0;
|
|
8
|
+
for (const grapheme of graphemes(stripAnsi(input)))
|
|
9
|
+
width += graphemeWidth(grapheme);
|
|
10
|
+
return width;
|
|
11
|
+
}
|
|
12
|
+
export function truncateToWidth(input, maxWidth, ellipsis = "...", pad = false) {
|
|
13
|
+
if (maxWidth <= 0)
|
|
14
|
+
return "";
|
|
15
|
+
const inputWidth = visibleWidth(input);
|
|
16
|
+
if (inputWidth <= maxWidth)
|
|
17
|
+
return padToWidth(input, maxWidth, pad);
|
|
18
|
+
const suffix = fitSuffix(ellipsis, maxWidth);
|
|
19
|
+
const suffixWidth = visibleWidth(suffix);
|
|
20
|
+
const contentLimit = Math.max(0, maxWidth - suffixWidth);
|
|
21
|
+
let output = "";
|
|
22
|
+
let used = 0;
|
|
23
|
+
for (const grapheme of graphemes(stripAnsi(input))) {
|
|
24
|
+
const width = graphemeWidth(grapheme);
|
|
25
|
+
if (used + width > contentLimit)
|
|
26
|
+
break;
|
|
27
|
+
output += grapheme;
|
|
28
|
+
used += width;
|
|
29
|
+
}
|
|
30
|
+
return padToWidth(output + suffix, maxWidth, pad);
|
|
31
|
+
}
|
|
32
|
+
function stripAnsi(input) {
|
|
33
|
+
return input.replace(ANSI_PATTERN, "");
|
|
34
|
+
}
|
|
35
|
+
function* graphemes(input) {
|
|
36
|
+
if (graphemeSegmenter) {
|
|
37
|
+
for (const segment of graphemeSegmenter.segment(input))
|
|
38
|
+
yield segment.segment;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
yield* input;
|
|
42
|
+
}
|
|
43
|
+
function padToWidth(input, maxWidth, pad) {
|
|
44
|
+
if (!pad)
|
|
45
|
+
return input;
|
|
46
|
+
return input + " ".repeat(Math.max(0, maxWidth - visibleWidth(input)));
|
|
47
|
+
}
|
|
48
|
+
function fitSuffix(ellipsis, maxWidth) {
|
|
49
|
+
if (visibleWidth(ellipsis) <= maxWidth)
|
|
50
|
+
return ellipsis;
|
|
51
|
+
let output = "";
|
|
52
|
+
let used = 0;
|
|
53
|
+
for (const grapheme of graphemes(stripAnsi(ellipsis))) {
|
|
54
|
+
const width = graphemeWidth(grapheme);
|
|
55
|
+
if (used + width > maxWidth)
|
|
56
|
+
break;
|
|
57
|
+
output += grapheme;
|
|
58
|
+
used += width;
|
|
59
|
+
}
|
|
60
|
+
return output;
|
|
61
|
+
}
|
|
62
|
+
function graphemeWidth(grapheme) {
|
|
63
|
+
if (grapheme.length === 0)
|
|
64
|
+
return 0;
|
|
65
|
+
const codePoints = [...grapheme].map((char) => char.codePointAt(0)).filter((value) => value !== undefined);
|
|
66
|
+
if (codePoints.every(isZeroWidthCodePoint))
|
|
67
|
+
return 0;
|
|
68
|
+
if (codePoints.every(isControlCodePoint))
|
|
69
|
+
return 0;
|
|
70
|
+
if (codePoints.includes(EMOJI_VARIATION_SELECTOR) || codePoints.includes(COMBINING_ENCLOSING_KEYCAP))
|
|
71
|
+
return 2;
|
|
72
|
+
return codePoints.some(isWideCodePoint) ? 2 : 1;
|
|
73
|
+
}
|
|
74
|
+
function isZeroWidthCodePoint(codePoint) {
|
|
75
|
+
return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
76
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
77
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
78
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
79
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f) ||
|
|
80
|
+
codePoint === 0x200d ||
|
|
81
|
+
codePoint === 0xfe0e ||
|
|
82
|
+
codePoint === 0xfe0f);
|
|
83
|
+
}
|
|
84
|
+
function isControlCodePoint(codePoint) {
|
|
85
|
+
return codePoint <= 0x001f || (codePoint >= 0x007f && codePoint <= 0x009f);
|
|
86
|
+
}
|
|
87
|
+
function isWideCodePoint(codePoint) {
|
|
88
|
+
return ((codePoint >= 0x1100 && codePoint <= 0x11ff) ||
|
|
89
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
|
|
90
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
91
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
92
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
93
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
94
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
95
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
96
|
+
(codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff) ||
|
|
97
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1faff) ||
|
|
98
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd));
|
|
99
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as Data from "effect/Data";
|
|
2
|
+
function describeUnknown(value) {
|
|
3
|
+
return value instanceof Error ? value.message : String(value);
|
|
4
|
+
}
|
|
5
|
+
export class LockFileParseError extends Data.TaggedError("LockFileParseError") {
|
|
6
|
+
get message() {
|
|
7
|
+
return this.path ? `LockFileParseError: ${this.reason} (${this.path})` : `LockFileParseError: ${this.reason}`;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class LockDirReadError extends Data.TaggedError("LockDirReadError") {
|
|
11
|
+
get message() {
|
|
12
|
+
return `LockDirReadError: failed to read ${this.path}: ${describeUnknown(this.cause)}`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class IdeConnectTimeoutError extends Data.TaggedError("IdeConnectTimeoutError") {
|
|
16
|
+
get message() {
|
|
17
|
+
return `IdeConnectTimeoutError: ${this.name} at ${this.host}:${this.port}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class IdeConnectError extends Data.TaggedError("IdeConnectError") {
|
|
21
|
+
get message() {
|
|
22
|
+
return `IdeConnectError: ${this.host}:${this.port}: ${describeUnknown(this.cause)}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class ConfigParseError extends Data.TaggedError("ConfigParseError") {
|
|
26
|
+
get message() {
|
|
27
|
+
return `ConfigParseError: ${this.reason} (${this.path})`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class InstallCommandError extends Data.TaggedError("InstallCommandError") {
|
|
31
|
+
get message() {
|
|
32
|
+
const exitPart = this.code === undefined ? "" : ` exited ${String(this.code)}`;
|
|
33
|
+
const detail = this.stderr.trim() || this.stdout?.trim() || "command failed";
|
|
34
|
+
return `InstallCommandError: ${this.cli}${exitPart}: ${detail}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as Cause from "effect/Cause";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import { logExtensionError } from "./errors.js";
|
|
5
|
+
function reportBoundaryFailure(scope, error, onError) {
|
|
6
|
+
if (onError)
|
|
7
|
+
onError(error);
|
|
8
|
+
else
|
|
9
|
+
logExtensionError(scope, error);
|
|
10
|
+
}
|
|
11
|
+
function failureFromCause(cause) {
|
|
12
|
+
return Cause.squash(cause);
|
|
13
|
+
}
|
|
14
|
+
export async function runEffect(scope, effect, onError) {
|
|
15
|
+
const exit = await Effect.runPromiseExit(effect);
|
|
16
|
+
if (Exit.isSuccess(exit)) {
|
|
17
|
+
return exit.value;
|
|
18
|
+
}
|
|
19
|
+
reportBoundaryFailure(scope, failureFromCause(exit.cause), onError);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export function runEffectSync(scope, effect, onError) {
|
|
23
|
+
const exit = Effect.runSyncExit(effect);
|
|
24
|
+
if (Exit.isSuccess(exit)) {
|
|
25
|
+
return exit.value;
|
|
26
|
+
}
|
|
27
|
+
reportBoundaryFailure(scope, failureFromCause(exit.cause), onError);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
export async function runEffectOrThrow(effect) {
|
|
31
|
+
const exit = await Effect.runPromiseExit(effect);
|
|
32
|
+
if (Exit.isSuccess(exit)) {
|
|
33
|
+
return exit.value;
|
|
34
|
+
}
|
|
35
|
+
throw failureFromCause(exit.cause);
|
|
36
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as Either from "effect/Either";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
const FiniteNumber = Schema.Number.pipe(Schema.finite());
|
|
4
|
+
const NonNegativeFinite = FiniteNumber.pipe(Schema.greaterThanOrEqualTo(0));
|
|
5
|
+
const PortNumber = FiniteNumber.pipe(Schema.greaterThan(0), Schema.lessThanOrEqualTo(65_535));
|
|
6
|
+
export const PositionSchema = Schema.Struct({
|
|
7
|
+
line: NonNegativeFinite,
|
|
8
|
+
character: NonNegativeFinite,
|
|
9
|
+
});
|
|
10
|
+
export const SelectionRangeSchema = Schema.Struct({
|
|
11
|
+
text: Schema.String,
|
|
12
|
+
selection: Schema.Struct({
|
|
13
|
+
start: PositionSchema,
|
|
14
|
+
end: PositionSchema,
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
export const IdeSourceSchema = Schema.Literal("vscode", "zed", "nvim", "jetbrains", "unknown");
|
|
18
|
+
export const IdeLockFileSchema = Schema.Struct({
|
|
19
|
+
version: Schema.Literal(1),
|
|
20
|
+
ide: IdeSourceSchema,
|
|
21
|
+
name: Schema.String,
|
|
22
|
+
transport: Schema.Literal("ws"),
|
|
23
|
+
host: Schema.String,
|
|
24
|
+
port: PortNumber,
|
|
25
|
+
authToken: Schema.String,
|
|
26
|
+
workspaceFolders: Schema.mutable(Schema.Array(Schema.String)),
|
|
27
|
+
pid: Schema.optional(FiniteNumber),
|
|
28
|
+
runningInWindows: Schema.optional(Schema.Boolean),
|
|
29
|
+
createdAt: Schema.String,
|
|
30
|
+
updatedAt: Schema.String,
|
|
31
|
+
});
|
|
32
|
+
export const EditorSelectionSnapshotSchema = Schema.Struct({
|
|
33
|
+
source: IdeSourceSchema,
|
|
34
|
+
filePath: Schema.String,
|
|
35
|
+
workspaceFolder: Schema.optional(Schema.String),
|
|
36
|
+
ranges: Schema.mutable(Schema.Array(SelectionRangeSchema)),
|
|
37
|
+
receivedAt: Schema.optional(FiniteNumber),
|
|
38
|
+
});
|
|
39
|
+
export const SelectionChangedParamsSchema = EditorSelectionSnapshotSchema;
|
|
40
|
+
export const SelectionClearedParamsSchema = Schema.Struct({
|
|
41
|
+
source: IdeSourceSchema,
|
|
42
|
+
reason: Schema.Literal("no-active-editor"),
|
|
43
|
+
receivedAt: Schema.optional(FiniteNumber),
|
|
44
|
+
});
|
|
45
|
+
export const AtMentionedParamsSchema = Schema.Struct({
|
|
46
|
+
source: IdeSourceSchema,
|
|
47
|
+
filePath: Schema.String,
|
|
48
|
+
workspaceFolder: Schema.optional(Schema.String),
|
|
49
|
+
ranges: Schema.mutable(Schema.Array(SelectionRangeSchema)),
|
|
50
|
+
receivedAt: Schema.optional(FiniteNumber),
|
|
51
|
+
rangeText: Schema.String,
|
|
52
|
+
});
|
|
53
|
+
const IdeDiagnosticCodeSchema = Schema.Union(Schema.String, FiniteNumber, Schema.Struct({
|
|
54
|
+
value: Schema.Union(Schema.String, FiniteNumber),
|
|
55
|
+
target: Schema.optional(Schema.String),
|
|
56
|
+
}));
|
|
57
|
+
const DiagnosticContextLineSchema = Schema.Struct({
|
|
58
|
+
line: NonNegativeFinite,
|
|
59
|
+
text: Schema.String,
|
|
60
|
+
isPrimary: Schema.Boolean,
|
|
61
|
+
});
|
|
62
|
+
const RangeSchema = Schema.Struct({
|
|
63
|
+
start: PositionSchema,
|
|
64
|
+
end: PositionSchema,
|
|
65
|
+
});
|
|
66
|
+
const IdeDiagnosticRelatedInformationSchema = Schema.Struct({
|
|
67
|
+
filePath: Schema.String,
|
|
68
|
+
range: RangeSchema,
|
|
69
|
+
message: Schema.String,
|
|
70
|
+
});
|
|
71
|
+
export const IdeDiagnosticSchema = Schema.Struct({
|
|
72
|
+
severity: Schema.Literal("error", "warning"),
|
|
73
|
+
message: Schema.String,
|
|
74
|
+
source: Schema.optional(Schema.String),
|
|
75
|
+
code: Schema.optional(IdeDiagnosticCodeSchema),
|
|
76
|
+
range: RangeSchema,
|
|
77
|
+
selectedText: Schema.String,
|
|
78
|
+
contextLines: Schema.mutable(Schema.Array(DiagnosticContextLineSchema)),
|
|
79
|
+
relatedInformation: Schema.optional(Schema.mutable(Schema.Array(IdeDiagnosticRelatedInformationSchema))),
|
|
80
|
+
});
|
|
81
|
+
export const DiagnosticFixRequestedParamsSchema = Schema.Struct({
|
|
82
|
+
action: Schema.optional(Schema.Literal("fix", "send-diagnostic")),
|
|
83
|
+
source: Schema.Literal("vscode"),
|
|
84
|
+
filePath: Schema.String,
|
|
85
|
+
workspaceFolder: Schema.optional(Schema.String),
|
|
86
|
+
documentVersion: Schema.optional(FiniteNumber),
|
|
87
|
+
triggerRange: RangeSchema,
|
|
88
|
+
diagnostics: Schema.mutable(Schema.Array(IdeDiagnosticSchema)).pipe(Schema.minItems(1)),
|
|
89
|
+
receivedAt: Schema.optional(FiniteNumber),
|
|
90
|
+
});
|
|
91
|
+
export const JsonRpcRequestSchema = Schema.Struct({
|
|
92
|
+
jsonrpc: Schema.Literal("2.0"),
|
|
93
|
+
id: Schema.Union(Schema.String, FiniteNumber),
|
|
94
|
+
method: Schema.String,
|
|
95
|
+
});
|
|
96
|
+
function decodeUnknown(schema, input) {
|
|
97
|
+
const result = Schema.decodeUnknownEither(schema)(input);
|
|
98
|
+
return Either.isRight(result) ? result.right : undefined;
|
|
99
|
+
}
|
|
100
|
+
export function decodeLockFile(input) {
|
|
101
|
+
return decodeUnknown(IdeLockFileSchema, input);
|
|
102
|
+
}
|
|
103
|
+
export function decodeEditorSelectionSnapshot(input) {
|
|
104
|
+
return decodeUnknown(EditorSelectionSnapshotSchema, input);
|
|
105
|
+
}
|
|
106
|
+
export function decodeSelectionChangedParams(input) {
|
|
107
|
+
return decodeEditorSelectionSnapshot(input);
|
|
108
|
+
}
|
|
109
|
+
export function decodeSelectionClearedParams(input) {
|
|
110
|
+
return decodeUnknown(SelectionClearedParamsSchema, input);
|
|
111
|
+
}
|
|
112
|
+
export function decodeAtMentionedParams(input) {
|
|
113
|
+
return decodeUnknown(AtMentionedParamsSchema, input);
|
|
114
|
+
}
|
|
115
|
+
export function decodeDiagnosticFixRequestedParams(input) {
|
|
116
|
+
return decodeUnknown(DiagnosticFixRequestedParamsSchema, input);
|
|
117
|
+
}
|
|
118
|
+
export function decodeJsonRpcRequest(input) {
|
|
119
|
+
return decodeUnknown(JsonRpcRequestSchema, input);
|
|
120
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
let extensionErrorReporter;
|
|
2
|
+
export function setExtensionErrorReporter(reporter) {
|
|
3
|
+
extensionErrorReporter = reporter;
|
|
4
|
+
}
|
|
5
|
+
export function errorMessage(error) {
|
|
6
|
+
return error instanceof Error ? error.message : String(error);
|
|
7
|
+
}
|
|
8
|
+
export function isStaleContextError(error) {
|
|
9
|
+
return errorMessage(error).includes("stale after session replacement or reload");
|
|
10
|
+
}
|
|
11
|
+
export function toError(error, prefix) {
|
|
12
|
+
if (error instanceof Error) {
|
|
13
|
+
if (!prefix)
|
|
14
|
+
return error;
|
|
15
|
+
return new Error(`${prefix}: ${error.message}`, { cause: error });
|
|
16
|
+
}
|
|
17
|
+
return new Error(prefix ? `${prefix}: ${String(error)}` : String(error));
|
|
18
|
+
}
|
|
19
|
+
export function formatExtensionError(scope, error) {
|
|
20
|
+
return `[pi-x-ide] ${scope}: ${errorMessage(error)}`;
|
|
21
|
+
}
|
|
22
|
+
export function logExtensionError(scope, error) {
|
|
23
|
+
extensionErrorReporter?.(scope, error);
|
|
24
|
+
}
|
|
25
|
+
export function safeRun(scope, action, onError) {
|
|
26
|
+
try {
|
|
27
|
+
return action();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (onError)
|
|
31
|
+
onError(error);
|
|
32
|
+
else
|
|
33
|
+
logExtensionError(scope, error);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function safeRunAsync(scope, action, onError) {
|
|
38
|
+
try {
|
|
39
|
+
return await action();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (onError)
|
|
43
|
+
onError(error);
|
|
44
|
+
else
|
|
45
|
+
logExtensionError(scope, error);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { toRelativeDisplayPath } from "./paths.js";
|
|
2
|
+
export const SYSTEM_REMINDER_TAG = "system-reminder";
|
|
3
|
+
export const SELECTED_CONTEXT_MARKER = "selected-context";
|
|
4
|
+
export function rangeToLineSpan(range) {
|
|
5
|
+
return {
|
|
6
|
+
startLine: range.selection.start.line + 1,
|
|
7
|
+
endLine: range.selection.end.line + 1,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function formatLineRange(range) {
|
|
11
|
+
const { startLine, endLine } = rangeToLineSpan(range);
|
|
12
|
+
return startLine === endLine ? `L${startLine}` : `L${startLine}-L${endLine}`;
|
|
13
|
+
}
|
|
14
|
+
export function formatLineSpan(range) {
|
|
15
|
+
return `#${formatLineRange(range)}`;
|
|
16
|
+
}
|
|
17
|
+
export function formatRangeMention(snapshot, options = {}) {
|
|
18
|
+
const rel = toRelativeDisplayPath(snapshot.filePath, snapshot.workspaceFolder, options.cwd, options.env);
|
|
19
|
+
const first = snapshot.ranges[0];
|
|
20
|
+
return first ? `@${rel}${formatLineSpan(first)}` : `@${rel}`;
|
|
21
|
+
}
|
|
22
|
+
export function parseRangeMention(input) {
|
|
23
|
+
const match = input.trim().match(/^@(.+?)(?:#L(\d+)(?:-L(\d+))?)?$/);
|
|
24
|
+
if (!match)
|
|
25
|
+
return undefined;
|
|
26
|
+
const startLine = match[2] ? Number(match[2]) : undefined;
|
|
27
|
+
const endLine = match[3] ? Number(match[3]) : startLine;
|
|
28
|
+
for (const value of [startLine, endLine]) {
|
|
29
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 1))
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return { path: match[1], startLine, endLine };
|
|
33
|
+
}
|
|
34
|
+
export function describeRanges(ranges) {
|
|
35
|
+
if (ranges.length === 0)
|
|
36
|
+
return "open file";
|
|
37
|
+
if (ranges.length === 1)
|
|
38
|
+
return formatLineSpan(ranges[0]);
|
|
39
|
+
return ranges.map((range, index) => `${index + 1}:${formatLineSpan(range)}`).join(" ");
|
|
40
|
+
}
|
|
41
|
+
export function snapshotKey(snapshot) {
|
|
42
|
+
return JSON.stringify({
|
|
43
|
+
source: snapshot.source,
|
|
44
|
+
filePath: snapshot.filePath,
|
|
45
|
+
workspaceFolder: snapshot.workspaceFolder,
|
|
46
|
+
ranges: snapshot.ranges.map((range) => ({
|
|
47
|
+
start: range.selection.start,
|
|
48
|
+
end: range.selection.end,
|
|
49
|
+
textLength: range.text.length,
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function formatEditorContextMessage(snapshot, options = {}) {
|
|
54
|
+
const filePath = toRelativeDisplayPath(snapshot.filePath, snapshot.workspaceFolder, undefined, options.env).replaceAll("`", "\\`");
|
|
55
|
+
const lines = ["### IDE Context", `- **IDE**: ${snapshot.source}`, `- **File**: \`${filePath}\``];
|
|
56
|
+
if (snapshot.ranges.length > 0) {
|
|
57
|
+
lines.push(`- **Selection**: ${snapshot.ranges.map(formatLineRange).join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
export function formatEditorContext(snapshot, options = {}) {
|
|
62
|
+
const filePath = toRelativeDisplayPath(snapshot.filePath, undefined, undefined, options.env);
|
|
63
|
+
const maxChars = options.maxChars ?? 24_000;
|
|
64
|
+
if (snapshot.ranges.length === 0) {
|
|
65
|
+
return `<${SYSTEM_REMINDER_TAG}><${SELECTED_CONTEXT_MARKER}>
|
|
66
|
+
The user currently has \`${filePath}\` open in ${snapshot.source}. This may or may not be relevant.
|
|
67
|
+
</${SELECTED_CONTEXT_MARKER}></${SYSTEM_REMINDER_TAG}>
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
const sections = [];
|
|
71
|
+
let remaining = maxChars;
|
|
72
|
+
let truncated = false;
|
|
73
|
+
for (const [index, range] of snapshot.ranges.entries()) {
|
|
74
|
+
const label = snapshot.ranges.length === 1 ? "The user selected" : `Selection ${index + 1}`;
|
|
75
|
+
let text = range.text;
|
|
76
|
+
if (text.length > remaining) {
|
|
77
|
+
text = text.slice(0, Math.max(0, remaining));
|
|
78
|
+
truncated = true;
|
|
79
|
+
}
|
|
80
|
+
remaining -= text.length;
|
|
81
|
+
const rangeStr = formatLineRange(range);
|
|
82
|
+
sections.push(`${label} ${rangeStr} from \`${filePath}\` in ${snapshot.source}:\n\`\`\`\n${text}\n\`\`\``);
|
|
83
|
+
if (remaining <= 0)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
const suffix = truncated ? "\n[Selection text truncated to keep the prompt size bounded.]" : "";
|
|
87
|
+
return `<${SYSTEM_REMINDER_TAG}><${SELECTED_CONTEXT_MARKER}>
|
|
88
|
+
${sections.join("\n")}
|
|
89
|
+
This may or may not be relevant.${suffix}
|
|
90
|
+
</${SELECTED_CONTEXT_MARKER}></${SYSTEM_REMINDER_TAG}>
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
3
|
+
import { AUTH_HEADER, PROTOCOL_VERSION, } from "./protocol.js";
|
|
4
|
+
import { isJsonRpcRequest } from "./jsonrpc-guard.js";
|
|
5
|
+
import { logExtensionError } from "./errors.js";
|
|
6
|
+
import { decodeRawData } from "./ws.js";
|
|
7
|
+
export class IdeWebSocketServer {
|
|
8
|
+
authToken;
|
|
9
|
+
serverInfo;
|
|
10
|
+
getInitialSelection;
|
|
11
|
+
httpServer;
|
|
12
|
+
wss;
|
|
13
|
+
sockets = new Set();
|
|
14
|
+
constructor(authToken, serverInfo, getInitialSelection) {
|
|
15
|
+
this.authToken = authToken;
|
|
16
|
+
this.serverInfo = serverInfo;
|
|
17
|
+
this.getInitialSelection = getInitialSelection;
|
|
18
|
+
}
|
|
19
|
+
get port() {
|
|
20
|
+
const address = this.httpServer?.address();
|
|
21
|
+
if (!address || typeof address === "string")
|
|
22
|
+
return 0;
|
|
23
|
+
return address.port;
|
|
24
|
+
}
|
|
25
|
+
get clientCount() {
|
|
26
|
+
return this.openSockets.length;
|
|
27
|
+
}
|
|
28
|
+
async start() {
|
|
29
|
+
this.httpServer = createServer();
|
|
30
|
+
this.wss = new WebSocketServer({
|
|
31
|
+
server: this.httpServer,
|
|
32
|
+
verifyClient: ({ req }, done) => {
|
|
33
|
+
const header = req.headers[AUTH_HEADER];
|
|
34
|
+
const token = Array.isArray(header) ? header[0] : header;
|
|
35
|
+
done(token === this.authToken, token === this.authToken ? undefined : 401, "Unauthorized");
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
this.wss.on("connection", (socket) => {
|
|
39
|
+
this.sockets.add(socket);
|
|
40
|
+
socket.on("close", () => this.sockets.delete(socket));
|
|
41
|
+
socket.on("error", () => this.sockets.delete(socket));
|
|
42
|
+
socket.on("message", (raw) => {
|
|
43
|
+
try {
|
|
44
|
+
this.handleMessage(socket, decodeRawData(raw));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logExtensionError("IDE WebSocket server message", error);
|
|
48
|
+
this.sockets.delete(socket);
|
|
49
|
+
socket.close();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
await new Promise((resolve, reject) => {
|
|
54
|
+
this.httpServer.once("error", reject);
|
|
55
|
+
this.httpServer.listen(0, "127.0.0.1", () => {
|
|
56
|
+
this.httpServer.off("error", reject);
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
return this.port;
|
|
61
|
+
}
|
|
62
|
+
broadcast(value) {
|
|
63
|
+
let text;
|
|
64
|
+
try {
|
|
65
|
+
text = JSON.stringify(value);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
logExtensionError("IDE WebSocket server broadcast serialization", error);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const socket of this.openSockets)
|
|
72
|
+
this.sendText(socket, text, "broadcast");
|
|
73
|
+
}
|
|
74
|
+
sendToFirstClient(value) {
|
|
75
|
+
const [socket] = this.openSockets;
|
|
76
|
+
if (!socket)
|
|
77
|
+
return false;
|
|
78
|
+
let text;
|
|
79
|
+
try {
|
|
80
|
+
text = JSON.stringify(value);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
logExtensionError("IDE WebSocket server send serialization", error);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return this.sendText(socket, text, "targeted send");
|
|
87
|
+
}
|
|
88
|
+
async stop() {
|
|
89
|
+
for (const socket of this.sockets)
|
|
90
|
+
socket.close();
|
|
91
|
+
this.sockets.clear();
|
|
92
|
+
await Promise.all([
|
|
93
|
+
new Promise((resolve) => this.wss?.close(() => resolve()) ?? resolve()),
|
|
94
|
+
new Promise((resolve) => this.httpServer?.close(() => resolve()) ?? resolve()),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
handleMessage(socket, text) {
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(text);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!isJsonRpcRequest(parsed))
|
|
106
|
+
return;
|
|
107
|
+
if (parsed.method !== "initialize")
|
|
108
|
+
return;
|
|
109
|
+
const ide = this.serverInfo.ide ?? "vscode";
|
|
110
|
+
const result = {
|
|
111
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
112
|
+
server: {
|
|
113
|
+
name: this.serverInfo.name,
|
|
114
|
+
version: this.serverInfo.version,
|
|
115
|
+
ide,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
this.sendValue(socket, { jsonrpc: "2.0", id: parsed.id, result }, "initialize response");
|
|
119
|
+
const snapshot = this.getInitialSelection?.();
|
|
120
|
+
this.sendValue(socket, {
|
|
121
|
+
jsonrpc: "2.0",
|
|
122
|
+
method: snapshot ? "selection_changed" : "selection_cleared",
|
|
123
|
+
params: snapshot
|
|
124
|
+
? {
|
|
125
|
+
...snapshot,
|
|
126
|
+
receivedAt: Date.now(),
|
|
127
|
+
}
|
|
128
|
+
: {
|
|
129
|
+
source: ide,
|
|
130
|
+
reason: "no-active-editor",
|
|
131
|
+
receivedAt: Date.now(),
|
|
132
|
+
},
|
|
133
|
+
}, "initial selection");
|
|
134
|
+
}
|
|
135
|
+
sendValue(socket, value, label) {
|
|
136
|
+
let text;
|
|
137
|
+
try {
|
|
138
|
+
text = JSON.stringify(value);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
logExtensionError(`IDE WebSocket server ${label} serialization`, error);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return this.sendText(socket, text, label);
|
|
145
|
+
}
|
|
146
|
+
sendText(socket, text, label) {
|
|
147
|
+
try {
|
|
148
|
+
socket.send(text, (error) => {
|
|
149
|
+
if (!error)
|
|
150
|
+
return;
|
|
151
|
+
logExtensionError(`IDE WebSocket server ${label}`, error);
|
|
152
|
+
this.sockets.delete(socket);
|
|
153
|
+
socket.close();
|
|
154
|
+
});
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
logExtensionError(`IDE WebSocket server ${label}`, error);
|
|
159
|
+
this.sockets.delete(socket);
|
|
160
|
+
socket.close();
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
get openSockets() {
|
|
165
|
+
return [...this.sockets].filter((socket) => socket.readyState === WebSocket.OPEN);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function isFiniteNumber(value) {
|
|
5
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
6
|
+
}
|
|
7
|
+
export function isJsonRpcRequest(value) {
|
|
8
|
+
return (isRecord(value) &&
|
|
9
|
+
value.jsonrpc === "2.0" &&
|
|
10
|
+
(typeof value.id === "string" || isFiniteNumber(value.id)) &&
|
|
11
|
+
typeof value.method === "string");
|
|
12
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const LOCK_FILE_HEARTBEAT_INTERVAL_MS = 15 * 60 * 1000;
|
|
2
|
+
export function startLockFileHeartbeat(refresh, options = {}) {
|
|
3
|
+
const intervalMs = options.intervalMs ?? LOCK_FILE_HEARTBEAT_INTERVAL_MS;
|
|
4
|
+
let terminal = false;
|
|
5
|
+
let terminalPromise;
|
|
6
|
+
let pendingCount = 0;
|
|
7
|
+
let queueTail = Promise.resolve();
|
|
8
|
+
const contain = async (work) => {
|
|
9
|
+
try {
|
|
10
|
+
await work();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const runRefresh = async () => {
|
|
16
|
+
try {
|
|
17
|
+
await refresh();
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
await contain(() => options.onError?.(error));
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const accept = () => {
|
|
24
|
+
pendingCount += 1;
|
|
25
|
+
const request = queueTail.then(async () => {
|
|
26
|
+
try {
|
|
27
|
+
await runRefresh();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
pendingCount -= 1;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
queueTail = request;
|
|
34
|
+
return request;
|
|
35
|
+
};
|
|
36
|
+
const timer = setInterval(() => {
|
|
37
|
+
if (terminal || pendingCount > 0)
|
|
38
|
+
return;
|
|
39
|
+
void accept();
|
|
40
|
+
}, intervalMs);
|
|
41
|
+
timer.unref();
|
|
42
|
+
const handle = {
|
|
43
|
+
refreshNow() {
|
|
44
|
+
if (terminalPromise)
|
|
45
|
+
return terminalPromise;
|
|
46
|
+
return accept();
|
|
47
|
+
},
|
|
48
|
+
stop() {
|
|
49
|
+
if (!terminalPromise) {
|
|
50
|
+
terminal = true;
|
|
51
|
+
clearInterval(timer);
|
|
52
|
+
terminalPromise = queueTail;
|
|
53
|
+
}
|
|
54
|
+
return terminalPromise;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return handle;
|
|
58
|
+
}
|