Skip to content

Commit 80d84c2

Browse files
authored
fix(nextls): allow extension to force a download (#81)
In the case that the auto updater in Next LS has a bug, this patch allows the extension to force a fresh download through the extension. It sets a key in the extensions globalState, which is persisted across restarts. If the key isn't there, then it forces the download. To force a download a second time, you need to increment the key version, which is basically setting a new key to use as a sentinel.
1 parent 673e808 commit 80d84c2

File tree

9 files changed

+141
-21
lines changed

9 files changed

+141
-21
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ node_modules
44
.vscode-test/
55
*.vsix
66
test-bin
7+
.elixir-tools

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@
198198
"build-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node --target=node16",
199199
"build": "yarn build-base --sourcemap",
200200
"watch": "yarn build-base --sourcemap --watch",
201-
"pretest": "yarn typecheck && yarn run build",
201+
"pretest": "yarn typecheck && yarn compile-dist && yarn build",
202202
"test": "node ./out/test/runTest.js"
203203
},
204204
"dependencies": {
@@ -225,4 +225,4 @@
225225
"sinon": "^17.0.1",
226226
"typescript": "^4.9.5"
227227
}
228-
}
228+
}

src/commands/uninstall.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ import * as fsp from "fs/promises";
33
import * as path from "path";
44
import * as os from "os";
55

6+
let binName: string;
7+
8+
if (os.platform() === "win32") {
9+
binName = "nextls.exe";
10+
} else {
11+
binName = "nextls";
12+
}
13+
614
export const run = async (cacheDir: string) => {
715
if (cacheDir[0] === "~") {
816
cacheDir = path.join(os.homedir(), cacheDir.slice(1));
917
}
10-
const bin = path.join(cacheDir, "nextls");
18+
const bin = path.join(cacheDir, binName);
1119
await fsp
1220
.rm(bin)
1321
.then(

src/extension.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,29 @@ async function activateCredo(
101101
}
102102
}
103103

104+
// In case the auto updater gets busted, we want the ability to force a download.
105+
// By incremented the key here, we should be able to force a download when the extension updates.
106+
export function forceDownload(context: vscode.ExtensionContext): boolean {
107+
let forceDownload: boolean = context.globalState.get(
108+
"elixir-tools-force-next-ls-download-v1"
109+
);
110+
channel.info(
111+
`value of elixir-tools-force-next-ls-download-v1: ${forceDownload}`
112+
);
113+
if (forceDownload === undefined) {
114+
forceDownload = true;
115+
}
116+
117+
context.globalState.update("elixir-tools-force-next-ls-download-v1", false);
118+
119+
return forceDownload;
120+
}
121+
104122
async function activateNextLS(
105123
context: vscode.ExtensionContext,
106124
_mixfile: vscode.Uri
107125
) {
126+
channel.info("activating next ls");
108127
let config = vscode.workspace.getConfiguration("elixir-tools.nextLS");
109128

110129
if (config.get("enable")) {
@@ -113,12 +132,11 @@ async function activateNextLS(
113132
switch (config.get("adapter")) {
114133
case "stdio":
115134
let cacheDir: string = config.get("installationDirectory")!;
116-
117135
if (cacheDir[0] === "~") {
118136
cacheDir = path.join(os.homedir(), cacheDir.slice(1));
119137
}
120138
const command = await ensureNextLSDownloaded(cacheDir, {
121-
force: false,
139+
force: forceDownload(context),
122140
});
123141

124142
serverOptions = {
@@ -181,13 +199,18 @@ async function activateNextLS(
181199
}
182200
}
183201

184-
export async function activate(context: vscode.ExtensionContext) {
202+
export async function activate(
203+
context: vscode.ExtensionContext
204+
): Promise<vscode.ExtensionContext> {
185205
let files = await vscode.workspace.findFiles("mix.exs");
206+
channel.info(`files: ${files[0]}`);
186207

187208
if (files[0]) {
188209
await activateCredo(context, files[0]);
189210
await activateNextLS(context, files[0]);
190211
}
212+
213+
return context;
191214
}
192215

193216
export function deactivate() {
@@ -209,7 +232,12 @@ export async function ensureNextLSDownloaded(
209232
cacheDir: string,
210233
opts: { force?: boolean } = {}
211234
): Promise<string> {
212-
const bin = path.join(cacheDir, "nextls");
235+
let bin: string;
236+
if (os.platform() === "win32") {
237+
bin = path.join(cacheDir, "nextls.exe");
238+
} else {
239+
bin = path.join(cacheDir, "nextls");
240+
}
213241

214242
const shouldDownload = opts.force || (await isBinaryMissing(bin));
215243

@@ -221,6 +249,7 @@ export async function ensureNextLSDownloaded(
221249
const exe = getExe(platform);
222250
const url = `https://github.com/elixir-tools/next-ls/releases/latest/download/${exe}`;
223251

252+
console.log(`Starting download from ${url}`);
224253
channel.info(`Starting download from ${url}`);
225254

226255
await fetch(url).then((res) => {
@@ -232,10 +261,12 @@ export async function ensureNextLSDownloaded(
232261
file.on("error", reject);
233262
})
234263
.then(() => channel.info("Downloaded NextLS!"))
235-
.catch(() =>
236-
channel.error("Failed to write downloaded executable to a file")
237-
);
264+
.catch(() => {
265+
console.log("Failed to write downloaded executable to a file");
266+
channel.error("Failed to write downloaded executable to a file");
267+
});
238268
} else {
269+
console.log(`Failed to write download Next LS: status=${res.status}`);
239270
channel.error(`Failed to write download Next LS: status=${res.status}`);
240271
}
241272
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"elixir-tools.nextLS.installationDirectory": "./src/test/fixtures/basic/test-bin"
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
defmodule Basic do
2+
def run do
3+
Enum.map([:one, :two], &Function.identity/1)
4+
end
5+
end

src/test/fixtures/basic/mix.exs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
defmodule Basic.MixProject do
2+
use Mix.Project
3+
4+
@version "0.1.0"
5+
6+
def project do
7+
[
8+
app: :basic,
9+
description: "Basic app",
10+
version: @version,
11+
elixir: "~> 1.13",
12+
elixirc_paths: elixirc_paths(Mix.env()),
13+
start_permanent: Mix.env() == :prod,
14+
deps: deps()
15+
]
16+
end
17+
18+
# Run "mix help compile.app" to learn about applications.
19+
def application do
20+
[
21+
extra_applications: [:logger, :crypto]
22+
]
23+
end
24+
25+
defp elixirc_paths(:test), do: ["lib", "test/support"]
26+
defp elixirc_paths(_), do: ["lib"]
27+
28+
# Run "mix help deps" to learn about dependencies.
29+
defp deps do
30+
[]
31+
end
32+
end

src/test/runTest.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@ async function main() {
1111
// The path to test runner
1212
// Passed to --extensionTestsPath
1313
const extensionTestsPath = path.resolve(__dirname, "./suite/index");
14+
const testWorkspace = path.resolve(
15+
__dirname,
16+
"../../src/test/fixtures/basic"
17+
);
1418

1519
// Download VS Code, unzip it and run the integration test
16-
await runTests({ extensionDevelopmentPath, extensionTestsPath });
20+
await runTests({
21+
extensionDevelopmentPath,
22+
extensionTestsPath,
23+
launchArgs: [testWorkspace],
24+
});
1725
} catch (err) {
1826
console.error("Failed to run tests", err);
1927
process.exit(1);

src/test/suite/extension.test.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as assert from "assert";
22
import * as fs from "fs";
33
import * as path from "path";
4+
import * as os from "os";
45

56
// You can import and use all API from the 'vscode' module
67
// as well as import your extension to test it
@@ -9,20 +10,25 @@ import * as myExtension from "../../extension.js";
910
import * as uninstall from "../../commands/uninstall.js";
1011
import * as sinon from "sinon";
1112

13+
let binName: string;
14+
15+
if (os.platform() === "win32") {
16+
binName = "nextls.exe";
17+
} else {
18+
binName = "nextls";
19+
}
20+
1221
// TODO: should extract the tests to the directory of the file under test
1322
suite("Extension Test Suite", () => {
1423
vscode.window.showInformationMessage("Start all tests.");
1524
let showInformationMessage;
1625

1726
setup(function () {
1827
fs.rmSync("./test-bin", { recursive: true, force: true });
19-
showInformationMessage = sinon
20-
.stub(vscode.window, "showInformationMessage")
21-
.returns(
22-
new Promise((resolve) => {
23-
return resolve({ title: "Yes" });
24-
})
25-
);
28+
showInformationMessage = sinon.stub(
29+
vscode.window,
30+
"showInformationMessage"
31+
);
2632
});
2733

2834
teardown(function () {
@@ -34,18 +40,18 @@ suite("Extension Test Suite", () => {
3440
fs.mkdirSync("./test-bin", { recursive: true });
3541

3642
let result = await myExtension.ensureNextLSDownloaded("test-bin");
37-
assert.equal(path.normalize(result), path.normalize("test-bin/nextls"));
43+
assert.equal(path.normalize(result), path.normalize(`test-bin/${binName}`));
3844
});
3945

4046
test("uninstalls Next LS", async function () {
4147
fs.mkdirSync("./test-bin", { recursive: true });
42-
fs.writeFileSync("./test-bin/nextls", "hello word");
48+
fs.writeFileSync(`./test-bin/${binName}`, "hello word");
4349

4450
await uninstall.run("./test-bin");
4551

4652
assert.equal(
4753
showInformationMessage.getCall(0).args[0],
48-
`Uninstalled Next LS from ${path.normalize("test-bin/nextls")}`
54+
`Uninstalled Next LS from ${path.normalize(`test-bin/${binName}`)}`
4955
);
5056
});
5157

@@ -62,4 +68,30 @@ suite("Extension Test Suite", () => {
6268
/due to Error: ENOENT: no such file or directory, lstat/
6369
);
6470
});
71+
72+
if (os.platform() !== "win32") {
73+
// TODO: make a test for the opposite case. As of right now, I'm not entirely
74+
// sure how to set globalState inside a test before the extension activates.
75+
test("forces a download if the special key is not set", async function () {
76+
let fixpath = path.join(__dirname, "../../../src/test/fixtures/basic");
77+
let binpath = path.join(fixpath, "test-bin");
78+
fs.mkdirSync(path.normalize(binpath), { recursive: true });
79+
fs.writeFileSync(
80+
path.normalize(path.join(binpath, binName)),
81+
"hello world"
82+
);
83+
let ext = vscode.extensions.getExtension("elixir-tools.elixir-tools");
84+
85+
await ext.activate();
86+
87+
const doc = await vscode.workspace.openTextDocument(
88+
path.join(fixpath, "mix.exs")
89+
);
90+
await vscode.window.showTextDocument(doc);
91+
await new Promise((resolve) => setTimeout(resolve, 1000));
92+
93+
let nextls = fs.readFileSync(path.normalize(path.join(binpath, binName)));
94+
assert.notEqual("hello world", nextls);
95+
}).timeout(5000);
96+
}
6597
});

0 commit comments

Comments
 (0)