@liushihao456/pi-emacs 0.1.2 → 0.1.3
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/index.ts +196 -17
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { rgPath } from "@vscode/ripgrep";
|
|
6
6
|
import { readdirSync, statSync } from "node:fs";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
7
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import path, { isAbsolute, resolve } from "node:path";
|
|
10
10
|
import {
|
|
@@ -25,7 +25,11 @@ type Theme = {
|
|
|
25
25
|
|
|
26
26
|
type EmacsState = {
|
|
27
27
|
startedEmacsServer: boolean;
|
|
28
|
+
serverName: string;
|
|
29
|
+
legacyDefaultServer?: boolean;
|
|
28
30
|
serverStartPromise?: Promise<void>;
|
|
31
|
+
watchdogPid?: number;
|
|
32
|
+
processShutdownHandlerVersion?: number;
|
|
29
33
|
lastEditedFile?: string;
|
|
30
34
|
};
|
|
31
35
|
|
|
@@ -48,8 +52,14 @@ type FileEntry = {
|
|
|
48
52
|
|
|
49
53
|
const state: EmacsState = ((globalThis as any).__piEmacsExtensionState ??= {
|
|
50
54
|
startedEmacsServer: false,
|
|
55
|
+
serverName: `pi-emacs-${process.pid}`,
|
|
51
56
|
});
|
|
52
57
|
|
|
58
|
+
if (!state.serverName) {
|
|
59
|
+
state.legacyDefaultServer = state.startedEmacsServer;
|
|
60
|
+
state.serverName = `pi-emacs-${process.pid}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
const FILE_EXPLORER_MAX_VISIBLE = 8;
|
|
54
64
|
const PROJECT_PICKER_MAX_VISIBLE = 12;
|
|
55
65
|
|
|
@@ -116,8 +126,151 @@ function execText(command: string, args: string[], options: SpawnOptions = {}) {
|
|
|
116
126
|
});
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
function emacsClientServerArgs(args: string[]) {
|
|
130
|
+
return ["-s", state.serverName, ...args];
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
function emacsClient(args: string[], options: SpawnOptions = {}) {
|
|
120
|
-
return run("emacsclient", args, options);
|
|
134
|
+
return run("emacsclient", emacsClientServerArgs(args), options);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function emacsClientSync(args: string[], timeoutMs = 5000) {
|
|
138
|
+
return spawnSync("emacsclient", emacsClientServerArgs(args), {
|
|
139
|
+
stdio: "ignore",
|
|
140
|
+
timeout: timeoutMs,
|
|
141
|
+
env: process.env,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sleep(ms: number) {
|
|
146
|
+
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function pidAlive(pid?: number) {
|
|
150
|
+
if (!pid) return false;
|
|
151
|
+
try {
|
|
152
|
+
process.kill(pid, 0);
|
|
153
|
+
return true;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function emacsWatchdogScript() {
|
|
160
|
+
return `
|
|
161
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
162
|
+
const parentPid = Number(process.argv[1]);
|
|
163
|
+
const serverName = process.argv[2];
|
|
164
|
+
const startDaemon = process.argv[3] === "1";
|
|
165
|
+
let stopping = false;
|
|
166
|
+
let child;
|
|
167
|
+
function parentAlive() {
|
|
168
|
+
try {
|
|
169
|
+
process.kill(parentPid, 0);
|
|
170
|
+
return true;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return error && error.code === "EPERM";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function killServer() {
|
|
176
|
+
spawnSync("emacsclient", ["-s", serverName, "--eval", "(kill-emacs)"], {
|
|
177
|
+
stdio: "ignore",
|
|
178
|
+
timeout: 5000,
|
|
179
|
+
env: process.env,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function stop() {
|
|
183
|
+
if (stopping) return;
|
|
184
|
+
stopping = true;
|
|
185
|
+
killServer();
|
|
186
|
+
try {
|
|
187
|
+
if (child && child.pid) process.kill(child.pid, "SIGTERM");
|
|
188
|
+
} catch {}
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
try {
|
|
191
|
+
if (child && child.pid) process.kill(child.pid, "SIGKILL");
|
|
192
|
+
} catch {}
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}, 1000);
|
|
195
|
+
}
|
|
196
|
+
if (startDaemon) {
|
|
197
|
+
child = spawn("emacs", ["--fg-daemon=" + serverName], {
|
|
198
|
+
stdio: "ignore",
|
|
199
|
+
env: process.env,
|
|
200
|
+
});
|
|
201
|
+
child.on("error", () => process.exit(1));
|
|
202
|
+
child.on("exit", () => process.exit(0));
|
|
203
|
+
}
|
|
204
|
+
let ticks = 0;
|
|
205
|
+
setInterval(() => {
|
|
206
|
+
if (!parentAlive()) {
|
|
207
|
+
stop();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!startDaemon && ++ticks % 10 === 0) {
|
|
211
|
+
const result = spawnSync("emacsclient", ["-s", serverName, "--eval", "(emacs-pid)"], {
|
|
212
|
+
stdio: "ignore",
|
|
213
|
+
timeout: 1000,
|
|
214
|
+
env: process.env,
|
|
215
|
+
});
|
|
216
|
+
if (result.status !== 0) process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
}, 1000);
|
|
219
|
+
process.on("SIGTERM", stop);
|
|
220
|
+
process.on("SIGHUP", stop);
|
|
221
|
+
process.on("SIGINT", stop);
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
type WatchdogMode = "start-daemon" | "watch-existing";
|
|
226
|
+
|
|
227
|
+
function startEmacsWatchdog(mode: WatchdogMode) {
|
|
228
|
+
const startDaemon = mode === "start-daemon";
|
|
229
|
+
if (!startDaemon && pidAlive(state.watchdogPid)) return;
|
|
230
|
+
const watchdog = spawn(
|
|
231
|
+
process.execPath,
|
|
232
|
+
[
|
|
233
|
+
"-e",
|
|
234
|
+
emacsWatchdogScript(),
|
|
235
|
+
String(process.pid),
|
|
236
|
+
state.serverName,
|
|
237
|
+
startDaemon ? "1" : "0",
|
|
238
|
+
],
|
|
239
|
+
{
|
|
240
|
+
detached: true,
|
|
241
|
+
stdio: "ignore",
|
|
242
|
+
env: process.env,
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
state.watchdogPid = watchdog.pid;
|
|
246
|
+
watchdog.unref();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function waitForEmacsServer(timeoutMs: number) {
|
|
250
|
+
const deadline = Date.now() + timeoutMs;
|
|
251
|
+
let lastError: unknown;
|
|
252
|
+
while (Date.now() < deadline) {
|
|
253
|
+
try {
|
|
254
|
+
await emacsClient(["--eval", "(emacs-pid)"], { timeoutMs: 1000 });
|
|
255
|
+
return;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
lastError = error;
|
|
258
|
+
await sleep(250);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
throw lastError instanceof Error
|
|
262
|
+
? lastError
|
|
263
|
+
: new Error("emacs daemon did not start");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function stopLegacyDefaultServerIfNeeded() {
|
|
267
|
+
if (!state.legacyDefaultServer) return;
|
|
268
|
+
spawnSync("emacsclient", ["--eval", "(kill-emacs)"], {
|
|
269
|
+
stdio: "ignore",
|
|
270
|
+
timeout: 5000,
|
|
271
|
+
env: process.env,
|
|
272
|
+
});
|
|
273
|
+
state.legacyDefaultServer = false;
|
|
121
274
|
}
|
|
122
275
|
|
|
123
276
|
function withTerminalMouse(expression: string) {
|
|
@@ -144,14 +297,12 @@ function findFileExpression(filePath: string) {
|
|
|
144
297
|
function diredExpression(cwd: string) {
|
|
145
298
|
return withTerminalMouse(
|
|
146
299
|
[
|
|
147
|
-
`(let* ((dir ${JSON.stringify(cwd)})`,
|
|
148
|
-
"(buf (dired-find-buffer-nocreate dir)))",
|
|
149
|
-
"(if buf",
|
|
150
300
|
"(progn",
|
|
151
|
-
"(
|
|
152
|
-
"(
|
|
153
|
-
"(
|
|
154
|
-
"(
|
|
301
|
+
"(mapc (lambda (b)",
|
|
302
|
+
"(when (eq (buffer-local-value 'major-mode b) 'dired-mode)",
|
|
303
|
+
"(kill-buffer b)))",
|
|
304
|
+
"(buffer-list))",
|
|
305
|
+
`(dired ${JSON.stringify(cwd)}))`,
|
|
155
306
|
].join(" "),
|
|
156
307
|
);
|
|
157
308
|
}
|
|
@@ -732,34 +883,60 @@ async function chooseProjectFile(
|
|
|
732
883
|
|
|
733
884
|
async function ensureEmacsServer() {
|
|
734
885
|
state.serverStartPromise ??= (async () => {
|
|
886
|
+
stopLegacyDefaultServerIfNeeded();
|
|
887
|
+
|
|
735
888
|
try {
|
|
736
889
|
await emacsClient(["--eval", "(emacs-pid)"], { timeoutMs: 2000 });
|
|
890
|
+
state.startedEmacsServer = true;
|
|
891
|
+
startEmacsWatchdog("watch-existing");
|
|
737
892
|
return;
|
|
738
893
|
} catch {
|
|
739
|
-
// No reachable server. Start
|
|
894
|
+
// No reachable named server. Start one under watchdog supervision.
|
|
740
895
|
}
|
|
741
896
|
|
|
742
|
-
|
|
743
|
-
await
|
|
897
|
+
startEmacsWatchdog("start-daemon");
|
|
898
|
+
await waitForEmacsServer(15000);
|
|
744
899
|
state.startedEmacsServer = true;
|
|
745
|
-
|
|
746
|
-
|
|
900
|
+
})().catch((error) => {
|
|
901
|
+
state.serverStartPromise = undefined;
|
|
902
|
+
throw error;
|
|
903
|
+
});
|
|
747
904
|
|
|
748
905
|
return state.serverStartPromise;
|
|
749
906
|
}
|
|
750
907
|
|
|
751
908
|
async function stopEmacsServer() {
|
|
752
|
-
if (!state.startedEmacsServer) return;
|
|
909
|
+
if (!state.startedEmacsServer && !state.serverStartPromise) return;
|
|
753
910
|
|
|
754
911
|
try {
|
|
755
912
|
await state.serverStartPromise;
|
|
756
913
|
} catch {
|
|
757
|
-
|
|
914
|
+
// Start failure means there may be nothing to stop.
|
|
758
915
|
}
|
|
759
916
|
|
|
760
|
-
|
|
917
|
+
try {
|
|
918
|
+
await emacsClient(["--eval", "(kill-emacs)"], { timeoutMs: 5000 });
|
|
919
|
+
} catch {
|
|
920
|
+
// Watchdog still handles parent-death cleanup if graceful stop fails.
|
|
921
|
+
}
|
|
761
922
|
state.startedEmacsServer = false;
|
|
762
923
|
state.serverStartPromise = undefined;
|
|
924
|
+
state.watchdogPid = undefined;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function stopEmacsServerSync() {
|
|
928
|
+
if (!state.startedEmacsServer && !state.serverStartPromise) return;
|
|
929
|
+
emacsClientSync(["--eval", "(kill-emacs)"]);
|
|
930
|
+
state.startedEmacsServer = false;
|
|
931
|
+
state.serverStartPromise = undefined;
|
|
932
|
+
state.watchdogPid = undefined;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function installProcessShutdownHandler() {
|
|
936
|
+
const handlerVersion = 2;
|
|
937
|
+
if (state.processShutdownHandlerVersion === handlerVersion) return;
|
|
938
|
+
state.processShutdownHandlerVersion = handlerVersion;
|
|
939
|
+
process.on("exit", () => stopEmacsServerSync());
|
|
763
940
|
}
|
|
764
941
|
|
|
765
942
|
async function openEmacsWithArgs(
|
|
@@ -831,6 +1008,8 @@ async function runProjectFindFile(ctx: ExtensionContext) {
|
|
|
831
1008
|
}
|
|
832
1009
|
|
|
833
1010
|
export default function (pi: ExtensionAPI) {
|
|
1011
|
+
installProcessShutdownHandler();
|
|
1012
|
+
|
|
834
1013
|
pi.on("session_start", () => {
|
|
835
1014
|
ensureEmacsServer().catch((error) => {
|
|
836
1015
|
console.error(`[emacs] failed to start server: ${error.message}`);
|