Skip to content

Commit da83f18

Browse files
authored
Add starter LSP implementation, test extension (microsoft#268)
1 parent dce9735 commit da83f18

16 files changed

+1461
-23
lines changed

.vscode/launch.template.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// A launch configuration that compiles the extension and then opens it inside a new window
2+
{
3+
"version": "0.2.0",
4+
"configurations": [
5+
{
6+
"type": "extensionHost",
7+
"request": "launch",
8+
"name": "Launch VS Code extension",
9+
"runtimeExecutable": "${execPath}",
10+
"args": [
11+
"--disable-extension=vscode.typescript-language-features",
12+
"--disable-extension=ms-vscode.vscode-typescript-next",
13+
"--extensionDevelopmentPath=${workspaceRoot}/_extension"
14+
],
15+
"outFiles": [
16+
"${workspaceRoot}/_extension/dist/**/*.js"
17+
],
18+
"autoAttachChildProcesses": true,
19+
"preLaunchTask": "Watch for extension run"
20+
}
21+
]
22+
}

.vscode/tasks.json

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "Build",
6+
"type": "npm",
7+
"script": "build",
8+
"group": "build",
9+
"presentation": {
10+
"panel": "dedicated",
11+
"reveal": "never"
12+
},
13+
"problemMatcher": [
14+
"$go"
15+
]
16+
},
17+
{
18+
"label": "Watch",
19+
"type": "npm",
20+
"script": "build:watch",
21+
"group": "build",
22+
"presentation": {
23+
"panel": "dedicated",
24+
"reveal": "never"
25+
},
26+
"isBackground": true,
27+
"problemMatcher": {
28+
"owner": "custom",
29+
"fileLocation": "autoDetect",
30+
"source": "hereby",
31+
"applyTo": "closedDocuments",
32+
"pattern": {
33+
"regexp": ""
34+
},
35+
"background": {
36+
"activeOnStart": true,
37+
"beginsPattern": "\\[build:watch\\] changed due to",
38+
"endsPattern": "\\[build:watch\\] run complete"
39+
}
40+
}
41+
},
42+
{
43+
"label": "Compile extension",
44+
"type": "npm",
45+
"script": "extension:build",
46+
"group": "build",
47+
"presentation": {
48+
"panel": "dedicated",
49+
"reveal": "never"
50+
},
51+
"problemMatcher": [
52+
"$tsc"
53+
]
54+
},
55+
{
56+
"label": "Watch extension",
57+
"type": "npm",
58+
"script": "extension:watch",
59+
"isBackground": true,
60+
"group": {
61+
"kind": "build",
62+
"isDefault": true
63+
},
64+
"presentation": {
65+
"panel": "dedicated",
66+
"reveal": "never"
67+
},
68+
"problemMatcher": [
69+
"$tsc-watch"
70+
]
71+
},
72+
{
73+
"label": "Watch for extension run",
74+
"dependsOn": [
75+
"Watch extension",
76+
"Watch"
77+
]
78+
}
79+
]
80+
}

Herebyfile.mjs

+232-13
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
// @ts-check
22

3+
import chokidar from "chokidar";
34
import { $ as _$ } from "execa";
45
import { glob } from "glob";
56
import { task } from "hereby";
7+
import assert from "node:assert";
68
import crypto from "node:crypto";
79
import fs from "node:fs";
810
import path from "node:path";
911
import url from "node:url";
1012
import { parseArgs } from "node:util";
13+
import pc from "picocolors";
1114
import which from "which";
1215

1316
const __filename = url.fileURLToPath(new URL(import.meta.url));
@@ -71,22 +74,30 @@ function isInstalled(tool) {
7174
return !!which.sync(tool, { nothrow: true });
7275
}
7376

74-
export const generateLibs = task({
75-
name: "lib",
76-
run: async () => {
77-
await fs.promises.mkdir("./built/local", { recursive: true });
77+
const libsDir = "./internal/bundled/libs";
78+
const libsRegexp = /(?:^|[\\/])internal[\\/]bundled[\\/]libs[\\/]/;
7879

79-
const libsDir = "./internal/bundled/libs";
80-
const libs = await fs.promises.readdir(libsDir);
80+
async function generateLibs() {
81+
await fs.promises.mkdir("./built/local", { recursive: true });
8182

82-
await Promise.all(libs.map(async lib => {
83-
fs.promises.copyFile(`${libsDir}/${lib}`, `./built/local/${lib}`);
84-
}));
85-
},
83+
const libs = await fs.promises.readdir(libsDir);
84+
85+
await Promise.all(libs.map(async lib => {
86+
fs.promises.copyFile(`${libsDir}/${lib}`, `./built/local/${lib}`);
87+
}));
88+
}
89+
90+
export const lib = task({
91+
name: "lib",
92+
run: generateLibs,
8693
});
8794

88-
function buildExecutableToBuilt(packagePath) {
89-
return $`go build ${options.race ? ["-race"] : []} -tags=noembed -o ./built/local/ ${packagePath}`;
95+
/**
96+
* @param {string} packagePath
97+
* @param {AbortSignal} [abortSignal]
98+
*/
99+
function buildExecutableToBuilt(packagePath, abortSignal) {
100+
return $({ cancelSignal: abortSignal })`go build ${options.race ? ["-race"] : []} -tags=noembed -o ./built/local/ ${packagePath}`;
90101
}
91102

92103
export const tsgoBuild = task({
@@ -98,7 +109,7 @@ export const tsgoBuild = task({
98109

99110
export const tsgo = task({
100111
name: "tsgo",
101-
dependencies: [generateLibs, tsgoBuild],
112+
dependencies: [lib, tsgoBuild],
102113
});
103114

104115
export const local = task({
@@ -111,6 +122,41 @@ export const build = task({
111122
dependencies: [local],
112123
});
113124

125+
export const buildWatch = task({
126+
name: "build:watch",
127+
run: async () => {
128+
await watchDebounced("build:watch", async (paths, abortSignal) => {
129+
let libsChanged = false;
130+
let goChanged = false;
131+
132+
for (const p of paths) {
133+
if (libsRegexp.test(p)) {
134+
libsChanged = true;
135+
}
136+
else if (p.endsWith(".go")) {
137+
goChanged = true;
138+
}
139+
if (libsChanged && goChanged) {
140+
break;
141+
}
142+
}
143+
144+
if (libsChanged) {
145+
console.log("Generating libs...");
146+
await generateLibs();
147+
}
148+
149+
if (goChanged) {
150+
console.log("Building tsgo...");
151+
await buildExecutableToBuilt("./cmd/tsgo", abortSignal);
152+
}
153+
}, {
154+
paths: ["cmd", "internal"],
155+
ignored: path => /[\\/]testdata[\\/]/.test(path),
156+
});
157+
},
158+
});
159+
114160
export const cleanBuilt = task({
115161
name: "clean:built",
116162
hiddenFromTaskList: true,
@@ -311,3 +357,176 @@ function rimraf(p) {
311357
// The rimraf package uses maxRetries=10 on Windows, but Node's fs.rm does not have that special case.
312358
return fs.promises.rm(p, { recursive: true, force: true, maxRetries: process.platform === "win32" ? 10 : 0 });
313359
}
360+
361+
/** @typedef {{
362+
* name: string;
363+
* paths: string | string[];
364+
* ignored?: (path: string) => boolean;
365+
* run: (paths: Set<string>, abortSignal: AbortSignal) => void | Promise<unknown>;
366+
* }} WatchTask */
367+
void 0;
368+
369+
/**
370+
* @param {string} name
371+
* @param {(paths: Set<string>, abortSignal: AbortSignal) => void | Promise<unknown>} run
372+
* @param {object} options
373+
* @param {string | string[]} options.paths
374+
* @param {(path: string) => boolean} [options.ignored]
375+
* @param {string} [options.name]
376+
*/
377+
async function watchDebounced(name, run, options) {
378+
let watching = true;
379+
let running = true;
380+
let lastChangeTimeMs = Date.now();
381+
let changedDeferred = /** @type {Deferred<void>} */ (new Deferred());
382+
let abortController = new AbortController();
383+
384+
const debouncer = new Debouncer(1_000, endRun);
385+
const watcher = chokidar.watch(options.paths, {
386+
ignored: options.ignored,
387+
ignorePermissionErrors: true,
388+
alwaysStat: true,
389+
});
390+
// The paths that have changed since the last run.
391+
let paths = new Set();
392+
393+
process.on("SIGINT", endWatchMode);
394+
process.on("beforeExit", endWatchMode);
395+
watcher.on("all", onChange);
396+
397+
while (watching) {
398+
const promise = changedDeferred.promise;
399+
const token = abortController.signal;
400+
if (!token.aborted) {
401+
running = true;
402+
try {
403+
const thePaths = paths;
404+
paths = new Set();
405+
await run(thePaths, token);
406+
}
407+
catch {
408+
// ignore
409+
}
410+
running = false;
411+
}
412+
if (watching) {
413+
console.log(pc.yellowBright(`[${name}] run complete, waiting for changes...`));
414+
await promise;
415+
}
416+
}
417+
418+
console.log("end");
419+
420+
/**
421+
* @param {'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir' | 'all' | 'ready' | 'raw' | 'error'} eventName
422+
* @param {string} path
423+
* @param {fs.Stats | undefined} stats
424+
*/
425+
function onChange(eventName, path, stats) {
426+
switch (eventName) {
427+
case "change":
428+
case "unlink":
429+
case "unlinkDir":
430+
break;
431+
case "add":
432+
case "addDir":
433+
// skip files that are detected as 'add' but haven't actually changed since the last time we ran.
434+
if (stats && stats.mtimeMs <= lastChangeTimeMs) {
435+
return;
436+
}
437+
break;
438+
}
439+
beginRun(path);
440+
}
441+
442+
/**
443+
* @param {string} path
444+
*/
445+
function beginRun(path) {
446+
if (debouncer.empty) {
447+
console.log(pc.yellowBright(`[${name}] changed due to '${path}', restarting...`));
448+
if (running) {
449+
console.log(pc.yellowBright(`[${name}] aborting in-progress run...`));
450+
}
451+
abortController.abort();
452+
abortController = new AbortController();
453+
}
454+
455+
debouncer.enqueue();
456+
paths.add(path);
457+
}
458+
459+
function endRun() {
460+
lastChangeTimeMs = Date.now();
461+
changedDeferred.resolve();
462+
changedDeferred = /** @type {Deferred<void>} */ (new Deferred());
463+
}
464+
465+
function endWatchMode() {
466+
if (watching) {
467+
watching = false;
468+
console.log(pc.yellowBright(`[${name}] exiting watch mode...`));
469+
abortController.abort();
470+
watcher.close();
471+
}
472+
}
473+
}
474+
475+
/**
476+
* @template T
477+
*/
478+
export class Deferred {
479+
constructor() {
480+
/** @type {Promise<T>} */
481+
this.promise = new Promise((resolve, reject) => {
482+
this.resolve = resolve;
483+
this.reject = reject;
484+
});
485+
}
486+
}
487+
488+
export class Debouncer {
489+
/**
490+
* @param {number} timeout
491+
* @param {() => Promise<any> | void} action
492+
*/
493+
constructor(timeout, action) {
494+
this._timeout = timeout;
495+
this._action = action;
496+
}
497+
498+
get empty() {
499+
return !this._deferred;
500+
}
501+
502+
enqueue() {
503+
if (this._timer) {
504+
clearTimeout(this._timer);
505+
this._timer = undefined;
506+
}
507+
508+
if (!this._deferred) {
509+
this._deferred = new Deferred();
510+
}
511+
512+
this._timer = setTimeout(() => this.run(), 100);
513+
return this._deferred.promise;
514+
}
515+
516+
run() {
517+
if (this._timer) {
518+
clearTimeout(this._timer);
519+
this._timer = undefined;
520+
}
521+
522+
const deferred = this._deferred;
523+
assert(deferred);
524+
this._deferred = undefined;
525+
try {
526+
deferred.resolve(this._action());
527+
}
528+
catch (e) {
529+
deferred.reject(e);
530+
}
531+
}
532+
}

_extension/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.vsix
2+
dist/

0 commit comments

Comments
 (0)