瀏覽代碼

Enchancement(vscode ext): Download the latest binary and setup ext.

Signed-off-by: Mulenga Bowa <mulengabowa53@gmail.com>
Mulenga Bowa 4 年之前
父節點
當前提交
0df1e74985

+ 2 - 2
.github/workflows/test.yml

@@ -136,8 +136,8 @@ jobs:
     - uses: actions/download-artifact@master
       with:
         name: solang-linux
-        path: /home/runner/.cargo/bin/
-    - run: chmod 755 $HOME/.cargo/bin/solang
+        path: /home/runner/.config/Code/User/globalStorage/solang.solang/
+    - run: chmod 755 $HOME/.config/Code/User/globalStorage/solang.solang/solang
     - name: Install Node.js
       uses: actions/setup-node@v1
       with:

+ 6 - 6
vscode/.eslintrc.json

@@ -8,12 +8,12 @@
     "plugins": [
         "@typescript-eslint"
     ],
+    "extends": [
+        "eslint:recommended",
+        "plugin:@typescript-eslint/recommended"
+    ],
     "rules": {
-        "@typescript-eslint/class-name-casing": "warn",
-        "@typescript-eslint/semi": "warn",
-        "curly": "warn",
-        "eqeqeq": "warn",
-        "no-throw-literal": "warn",
-        "semi": "off"
+        "@typescript-eslint/explicit-module-boundary-types": "off",
+        "@typescript-eslint/no-empty-function": "warn"
     }
 }

+ 12 - 11
vscode/package.json

@@ -21,12 +21,6 @@
 			"type": "object",
 			"title:": "Solang Solidity Compiler",
 			"properties": {
-				"solang.languageServerExecutable": {
-					"scope": "resource",
-					"type": "string",
-					"default": "~/.cargo/bin/solang",
-					"description": "Executable for the Solang Solidity Compiler"
-				},
 				"solang.target": {
 					"scope": "window",
 					"type": "string",
@@ -38,6 +32,11 @@
 					],
 					"default": "substrate",
 					"description": "Chain to build for. The Solidity language changes in subtle ways depending on the target."
+				},
+				"solang.updates.askBeforeDownload": {
+					"type": "boolean",
+					"default": true,
+					"description": "Whether to ask for permission before downloading any files from the Internet,"
 				}
 			}
 		},
@@ -75,11 +74,12 @@
 		"vscode:prepublish": "npm run compile",
 		"compile": "tsc -p ./",
 		"lint": "eslint src --ext ts",
+		"lint:fix": "eslint src --ext ts --fix",
 		"watch": "tsc -watch -p ./",
-		"pretest": "npm run compile && npm run lint",
-		"test": "node ./out/test/runTest.js"
+		"test": "NODE_ENV=test node ./out/test/runTest.js"
 	},
 	"dependencies": {
+		"node-fetch": "^2.6.1",
 		"vscode-languageclient": "^6.1.3",
 		"vscode-languageserver": "^6.1.1",
 		"vscode-languageserver-protocol": "=3.15.3"
@@ -88,10 +88,11 @@
 		"@types/glob": "^7.1.1",
 		"@types/mocha": "^7.0.2",
 		"@types/node": "^12.12.0",
+		"@types/node-fetch": "^2.5.8",
 		"@types/vscode": "^1.43.0",
-		"@typescript-eslint/eslint-plugin": "^2.30.0",
-		"@typescript-eslint/parser": "^2.30.0",
-		"eslint": "^6.8.0",
+		"@typescript-eslint/eslint-plugin": "^4.15.0",
+		"@typescript-eslint/parser": "^4.15.0",
+		"eslint": ">=7.0.0",
 		"glob": "^7.1.6",
 		"mocha": "^7.1.2",
 		"typescript": "^3.8.3",

+ 66 - 74
vscode/src/client/extension.ts

@@ -1,98 +1,90 @@
 // The module 'vscode' contains the VS Code extensibility API
 // Import the module and reference it with the alias vscode in your code below
 import * as vscode from 'vscode';
-import * as path from 'path';
-import { homedir } from 'os';
-import * as cp from 'child_process';
 import * as rpc from 'vscode-jsonrpc';
-
-import {
-	LanguageClient,
-	LanguageClientOptions,
-	ServerOptions,
-	TransportKind,
-	InitializeRequest,
-	InitializeParams,
-	DefinitionRequest,
-	Executable,
-	ExecutableOptions
-} from 'vscode-languageclient';
-
-import {
-	workspace,
-	WorkspaceFolder
-} from 'vscode';
-
-
-let diagcollect: vscode.DiagnosticCollection;
-
-
-function expandPathResolving(path: string) {
-	if (path.startsWith('~/')) {
-		return path.replace('~', homedir());
-	}
-	return path;
-}
+import { promises as fs } from 'fs';
+import { join } from 'path';
+import { LanguageClient, LanguageClientOptions, ServerOptions, Executable } from 'vscode-languageclient';
+import expandPathResolving from '../utils/expandPathResolving';
+import getServer from '../utils/getServer';
+import isValidExecutable from '../utils/isValidExecutable';
+import { setServerPath } from '../utils/serverPath';
 
 // this method is called when your extension is activated
 // your extension is activated the very first time the command is executed
-export function activate(context: vscode.ExtensionContext) {
-
-	const config = workspace.getConfiguration('solang');
+export async function activate(context: vscode.ExtensionContext) {
+  await tryActivate(context).catch((err) => {
+    void vscode.window.showErrorMessage(`Cannot activate solang: ${err.message}`);
+    throw err;
+  });
+}
 
-	let command: string = config.get('languageServerExecutable') || '~/.cargo/bin/solang';
-	let target: string = config.get('target') || 'substrate';
+async function tryActivate(context: vscode.ExtensionContext) {
+  await fs.mkdir(context.globalStoragePath, { recursive: true });
 
-	// Use the console to output diagnostic information (console.log) and errors (console.error)
-	// This line of code will only be executed once when your extension is activated
-	console.log('Congratulations, your extension "solang" is now active!');
+  const path = await bootstrapServer(context);
+  await bootstrapExtension(context, path);
+}
 
-	diagcollect = vscode.languages.createDiagnosticCollection('solidity');
+async function bootstrapExtension(context: vscode.ExtensionContext, serverpath: string) {
+  const config = vscode.workspace.getConfiguration('solang');
+  const target: string = config.get('target') || 'substrate';
 
-	context.subscriptions.push(diagcollect);
+  // Use the console to output diagnostic information (console.log) and errors (console.error)
+  // This line of code will only be executed once when your extension is activated
+  console.log('Congratulations, your extension "solang" is now active!');
 
-	let connection = rpc.createMessageConnection(
-		new rpc.StreamMessageReader(process.stdout),
-		new rpc.StreamMessageWriter(process.stdin)
-	);
+  const diagnosticCollection = vscode.languages.createDiagnosticCollection('solidity');
 
-	connection.listen();
+  context.subscriptions.push(diagnosticCollection);
 
-	const sop: Executable = {
-		command: expandPathResolving(command),
-		args: ['--language-server', '--target', target],
-	};
+  const connection = rpc.createMessageConnection(
+    new rpc.StreamMessageReader(process.stdout),
+    new rpc.StreamMessageWriter(process.stdin)
+  );
 
-	const serverOptions: ServerOptions = sop;
+  connection.listen();
 
-	const clientoptions: LanguageClientOptions = {
-		documentSelector: [
-			{ language: 'solidity', scheme: 'file' },
-			{ language: 'solidity', scheme: 'untitled' },
-		]
-	};
+  const sop: Executable = {
+    command: expandPathResolving(serverpath),
+    args: ['--language-server', '--target', target],
+  };
 
-	const init: InitializeParams = {
-		rootUri: null,
-		processId: 1,
-		capabilities: {},
-		workspaceFolders: null,
-	};
+  const serverOptions: ServerOptions = sop;
 
-	const params = {
-		"textDocument": { "uri": "file://temp" },
-		"position": { "line": 1, "character": 1 }
-	};
+  const clientOptions: LanguageClientOptions = {
+    documentSelector: [
+      { language: 'solidity', scheme: 'file' },
+      { language: 'solidity', scheme: 'untitled' },
+    ],
+  };
 
+  const client = new LanguageClient('solidity', 'Solang Solidity Compiler', serverOptions, clientOptions).start();
 
-	let clientdispos = new LanguageClient(
-		'solidity',
-		'Solang Solidity Compiler',
-		serverOptions,
-		clientoptions).start();
+  context.subscriptions.push(client);
+}
 
-	context.subscriptions.push(clientdispos);
+async function bootstrapServer(context: vscode.ExtensionContext) {
+  let path
+  if (process.env.NODE_ENV === 'test') {
+    path = join(context.globalStoragePath, 'solang')
+  } else {
+    path = await getServer(context);
+  }
+  
+  if (!path) {
+    throw new Error('Solang Language Server is not available.');
+  }
+
+  console.log('Using server binary at', path);
+
+  if (!isValidExecutable(path)) {
+    setServerPath(context, undefined);
+    throw new Error(`Failed to execute ${path} --version`);
+  }
+
+  return path;
 }
 
 // this method is called when your extension is deactivated
-export function deactivate() { }
+export function deactivate() {}

+ 14 - 41
vscode/src/server/server.ts

@@ -1,54 +1,27 @@
-import {
-    createConnection,
-    Diagnostic,
-    Range,
-    DiagnosticSeverity,
-    InitializeRequest,
-    InitializeResult,
-    InitializeParams,
-    DefinitionRequest,
-    RequestType,
-    TextDocument
-} from 'vscode-languageserver';
-
+import { createConnection, InitializeResult, DefinitionRequest } from 'vscode-languageserver';
 import * as rpc from 'vscode-jsonrpc';
-import { DocumentLink } from 'vscode';
 
-let connection = createConnection(
-    new rpc.StreamMessageReader(process.stdin),
-    new rpc.StreamMessageWriter(process.stdout)
+const connection = createConnection(
+  new rpc.StreamMessageReader(process.stdin),
+  new rpc.StreamMessageWriter(process.stdout)
 );
 
-//const connection = createConnection();
-
-//connection.console.log(`Sample server running in node ${process.version}`);
-
-connection.onInitialize((params: InitializeParams) => {
-    const result: InitializeResult = {
-        capabilities: {},
-    };
-    return result;
+connection.onInitialize(() => {
+  const result: InitializeResult = {
+    capabilities: {},
+  };
+  return result;
 });
 
 connection.onInitialized(() => {
-    connection.client.register(DefinitionRequest.type, undefined);
+  connection.client.register(DefinitionRequest.type, undefined);
 });
 
-function validate(): void {
-    connection.sendDiagnostics({
-        uri: '1',
-        version: 1,
-        diagnostics: [
-            Diagnostic.create(Range.create(0,0,0, 10), 'Something is wrong here', DiagnosticSeverity.Warning)
-        ]
-    });
-}
-
-let notif = new rpc.NotificationType<string, void>('test notif');
+const notif = new rpc.NotificationType<string, void>('test notif');
 
 connection.onNotification(notif, (param: string) => {
-    console.log('notified\n');
-    console.log(param);
+  console.log('notified\n');
+  console.log(param);
 });
 
-connection.listen();
+connection.listen();

+ 13 - 13
vscode/src/test/runTest.ts

@@ -3,21 +3,21 @@ import * as path from 'path';
 import { runTests } from 'vscode-test';
 
 async function main() {
-	try {
-		// The folder containing the Extension Manifest package.json
-		// Passed to `--extensionDevelopmentPath`
-		const extensionDevelopmentPath = path.resolve(__dirname, '../../');
+  try {
+    // The folder containing the Extension Manifest package.json
+    // Passed to `--extensionDevelopmentPath`
+    const extensionDevelopmentPath = path.resolve(__dirname, '../../');
 
-		// The path to test runner
-		// Passed to --extensionTestsPath
-		const extensionTestsPath = path.resolve(__dirname, './suite/index');
+    // The path to test runner
+    // Passed to --extensionTestsPath
+    const extensionTestsPath = path.resolve(__dirname, './suite/index');
 
-		// Download VS Code, unzip it and run the integration test
-		await runTests({ extensionDevelopmentPath, extensionTestsPath });
-	} catch (err) {
-		console.error('Failed to run tests');
-		process.exit(1);
-	}
+    // Download VS Code, unzip it and run the integration test
+    await runTests({ extensionDevelopmentPath, extensionTestsPath });
+  } catch (err) {
+    console.error('Failed to run tests');
+    process.exit(1);
+  }
 }
 
 main();

+ 107 - 78
vscode/src/test/suite/extension.test.ts

@@ -9,103 +9,132 @@ import { get } from 'http';
 // import * as myExtension from '../../extension';
 
 suite('Extension Test Suite', function () {
-	vscode.window.showInformationMessage('Start all tests.');
-
-	const docUri = getDocUri('applyedits.sol');
-
-	this.timeout(20000);
-	const diagnosdoc1 = getDocUri('one.sol');
-	test('Testing for Row and Col pos.', async () => {
-		await testdiagnos(diagnosdoc1, [
-			{ message: 'unrecognised token `aa\', expected ";", "="', range: toRange(5, 0, 5, 2), severity: vscode.DiagnosticSeverity.Error, source: 'solidity' }
-		]
-		);
-	});
-
-	this.timeout(20000);
-	const diagnosdoc2 = getDocUri('two.sol');
-	test('Testing for diagnostic errors.', async () => {
-		await testdiagnos(diagnosdoc2, [
-			{
-				message: 'unrecognised token `}\', expected "!", "(", "+", "++", "-", "--", "[", "address", "bool", "bytes", "delete", "false", "function", "mapping", "new", "payable", "string", "this", "true", "~", Bytes, Int, Uint, address, hexnumber, hexstring, identifier, number, string',
-				range: toRange(13, 1, 13, 2), severity: vscode.DiagnosticSeverity.Error, source: 'solidity'
-			}
-		]
-		);
-	});
-
-	this.timeout(20000);
-	const diagnosdoc3 = getDocUri('three.sol');
-	test('Testing for diagnostic info.', async () => {
-		await testdiagnos(diagnosdoc3, [
-		]);
-	});
-
-	this.timeout(20000);
-	const diagnosdoc4 = getDocUri('four.sol');
-	test('Testing for diagnostics warnings.', async () => {
-		await testdiagnos(diagnosdoc4, [
-			{ message: 'unknown pragma ‘foo’ with value ‘bar’ ignored', range: toRange(0, 7, 0, 14), severity: vscode.DiagnosticSeverity.Warning, source: `solidity` },
-			{ message: 'function can be declared ‘pure’', range: toRange(3, 5, 3, 40), severity: vscode.DiagnosticSeverity.Warning, source: `solidity` },
-		]);
-	});
-
-	// Tests for hover.
-	this.timeout(20000);
-	const hoverdoc1 = getDocUri('hover1.sol');
-	test('Testing for Hover.', async () => {
-		await testhover(hoverdoc1);
-	});
+  vscode.window.showInformationMessage('Start all tests.');
+
+  const docUri = getDocUri('applyedits.sol');
+
+  this.timeout(20000);
+  const diagnosdoc1 = getDocUri('one.sol');
+  test('Testing for Row and Col pos.', async () => {
+    await testdiagnos(diagnosdoc1, [
+      {
+        message: 'unrecognised token `aa\', expected ";", "="',
+        range: toRange(5, 0, 5, 2),
+        severity: vscode.DiagnosticSeverity.Error,
+        source: 'solidity',
+      },
+    ]);
+  });
+
+  this.timeout(20000);
+  const diagnosdoc2 = getDocUri('two.sol');
+  test('Testing for diagnostic errors.', async () => {
+    await testdiagnos(diagnosdoc2, [
+      {
+        message:
+          'unrecognised token `}\', expected "!", "(", "+", "++", "-", "--", "[", "address", "bool", "bytes", "delete", "false", "function", "mapping", "new", "payable", "string", "this", "true", "~", Bytes, Int, Uint, address, hexnumber, hexstring, identifier, number, string',
+        range: toRange(13, 1, 13, 2),
+        severity: vscode.DiagnosticSeverity.Error,
+        source: 'solidity',
+      },
+    ]);
+  });
+
+  this.timeout(20000);
+  const diagnosdoc3 = getDocUri('three.sol');
+  test('Testing for diagnostic info.', async () => {
+    await testdiagnos(diagnosdoc3, []);
+  });
+
+  this.timeout(20000);
+  const diagnosdoc4 = getDocUri('four.sol');
+  test('Testing for diagnostics warnings.', async () => {
+    await testdiagnos(diagnosdoc4, [
+      {
+        message: 'unknown pragma ‘foo’ with value ‘bar’ ignored',
+        range: toRange(0, 7, 0, 14),
+        severity: vscode.DiagnosticSeverity.Warning,
+        source: `solidity`,
+      },
+      {
+        message: 'function can be declared ‘pure’',
+        range: toRange(3, 5, 3, 40),
+        severity: vscode.DiagnosticSeverity.Warning,
+        source: `solidity`,
+      },
+    ]);
+  });
+
+  // Tests for hover.
+  this.timeout(20000);
+  const hoverdoc1 = getDocUri('hover1.sol');
+  test('Testing for Hover.', async () => {
+    await testhover(hoverdoc1);
+  });
 });
 
 function toRange(lineno1: number, charno1: number, lineno2: number, charno2: number) {
-	const start = new vscode.Position(lineno1, charno1);
-	const end = new vscode.Position(lineno2, charno2);
-	return new vscode.Range(start, end);
+  const start = new vscode.Position(lineno1, charno1);
+  const end = new vscode.Position(lineno2, charno2);
+  return new vscode.Range(start, end);
 }
 
 async function testhover(docUri: vscode.Uri) {
-	await activate(docUri);
+  await activate(docUri);
 
-	var pos1 = new vscode.Position(74, 14);
+  const pos1 = new vscode.Position(74, 14);
 
-	let actualhover = await vscode.commands.executeCommand('vscode.executeHoverProvider', docUri, pos1) as vscode.Hover[];
+  const actualhover = (await vscode.commands.executeCommand(
+    'vscode.executeHoverProvider',
+    docUri,
+    pos1
+  )) as vscode.Hover[];
 
-	let contentarr1 = actualhover[0].contents as vscode.MarkdownString[];
+  const contentarr1 = actualhover[0].contents as vscode.MarkdownString[];
 
-	assert.equal(contentarr1[0].value, '(mapping(address => uint256))');
+  assert.equal(contentarr1[0].value, '(mapping(address => uint256))');
 
-	var pos2 = new vscode.Position(78, 19);
+  const pos2 = new vscode.Position(78, 19);
 
-	let actualhover2 = await vscode.commands.executeCommand('vscode.executeHoverProvider', docUri, pos2) as vscode.Hover[];
+  const actualhover2 = (await vscode.commands.executeCommand(
+    'vscode.executeHoverProvider',
+    docUri,
+    pos2
+  )) as vscode.Hover[];
 
-	let contentarr2 = actualhover2[0].contents as vscode.MarkdownString[];
+  const contentarr2 = actualhover2[0].contents as vscode.MarkdownString[];
 
-	assert.equal(contentarr2[0].value, '```\nevent SimpleAuction.HighestBidIncreased {\n\taddress bidder,\n\tuint256 amount\n};\n```\n');
+  assert.equal(
+    contentarr2[0].value,
+    '```\nevent SimpleAuction.HighestBidIncreased {\n\taddress bidder,\n\tuint256 amount\n};\n```\n'
+  );
 
-	var pos3 = new vscode.Position(53, 13);
+  const pos3 = new vscode.Position(53, 13);
 
-	let actualhover3 = await vscode.commands.executeCommand('vscode.executeHoverProvider', docUri, pos3) as vscode.Hover[];
+  const actualhover3 = (await vscode.commands.executeCommand(
+    'vscode.executeHoverProvider',
+    docUri,
+    pos3
+  )) as vscode.Hover[];
 
-	let contentarr3 = actualhover3[0].contents as vscode.MarkdownString[];
+  const contentarr3 = actualhover3[0].contents as vscode.MarkdownString[];
 
-	assert.equal(contentarr3[0].value, '[built-in]  void require (bool): Abort execution if argument evaulates to false');
+  assert.equal(contentarr3[0].value, '[built-in]  void require (bool): Abort execution if argument evaulates to false');
 }
 
 async function testdiagnos(docUri: vscode.Uri, expecteddiag: vscode.Diagnostic[]) {
-	await activate(docUri);
-
-	let actualDiagnostics = vscode.languages.getDiagnostics(docUri);
-
-	if (actualDiagnostics) {
-		expecteddiag.forEach((expectedDiagnostic, i) => {
-			const actualDiagnostic = actualDiagnostics[i];
-			assert.equal(actualDiagnostic.message, expectedDiagnostic.message);
-			assert.deepEqual(actualDiagnostic.range, expectedDiagnostic.range);
-			assert.equal(actualDiagnostic.severity, expectedDiagnostic.severity);
-		});
-	}
-	else {
-		console.error('the diagnostics are incorrect', actualDiagnostics);
-	}
+  await activate(docUri);
+
+  const actualDiagnostics = vscode.languages.getDiagnostics(docUri);
+
+  if (actualDiagnostics) {
+    expecteddiag.forEach((expectedDiagnostic, i) => {
+      const actualDiagnostic = actualDiagnostics[i];
+      assert.equal(actualDiagnostic.message, expectedDiagnostic.message);
+      assert.deepEqual(actualDiagnostic.range, expectedDiagnostic.range);
+      assert.equal(actualDiagnostic.severity, expectedDiagnostic.severity);
+    });
+  } else {
+    console.error('the diagnostics are incorrect', actualDiagnostics);
+  }
 }

+ 31 - 36
vscode/src/test/suite/helper.ts

@@ -7,55 +7,50 @@ export let documentEol: string;
 export let platformEol: string;
 
 export async function activate(docUri: vscode.Uri) {
-	// The extensionId is `publisher.name` from package.json
-
-	let ext: vscode.Extension<any> | undefined = vscode.extensions.getExtension('solang.solang');
-
-	let extn_act;
-
-	if(ext){
-	try {
-		extn_act = await ext.activate();
-	}
-	catch(e){
-		console.error(e);
-	}
-	}
-	else{
-		console.error('extension is undefined');
-	}
-
-	try {
-		doc = await vscode.workspace.openTextDocument(docUri);
-		editor = await vscode.window.showTextDocument(doc);
-		await sleep(5000);
-	} catch (e) {
-		console.error(e);
-	}
+  // The extensionId is `publisher.name` from package.json
+
+  const ext: vscode.Extension<any> | undefined = vscode.extensions.getExtension('solang.solang');
+
+  let extn_act;
+
+  if (ext) {
+    try {
+      extn_act = await ext.activate();
+    } catch (e) {
+      console.error(e);
+    }
+  } else {
+    console.error('extension is undefined');
+  }
+
+  try {
+    doc = await vscode.workspace.openTextDocument(docUri);
+    editor = await vscode.window.showTextDocument(doc);
+    await sleep(5000);
+  } catch (e) {
+    console.error(e);
+  }
 }
 
 async function sleep(ms: number) {
-	return new Promise(resolve => setTimeout(resolve, ms));
+  return new Promise((resolve) => setTimeout(resolve, ms));
 }
 
 export const getDocPath = (p: string) => {
-	return path.resolve(__dirname, '../../../src/testFixture', p);
+  return path.resolve(__dirname, '../../../src/testFixture', p);
 };
 
 export const getDocUri = (p: string) => {
-	return vscode.Uri.file(getDocPath(p));
+  return vscode.Uri.file(getDocPath(p));
 };
 
 export async function setTestContent(content: string): Promise<boolean> {
-	const all = new vscode.Range(
-		doc.positionAt(0),
-		doc.positionAt(doc.getText().length)
-	);
-	return editor.edit(eb => eb.replace(all, content));
+  const all = new vscode.Range(doc.positionAt(0), doc.positionAt(doc.getText().length));
+  return editor.edit((eb) => eb.replace(all, content));
 }
 
-export async function getedits(){
-	const chang = doc.lineAt(0);
+export async function getedits() {
+  const chang = doc.lineAt(0);
 
-	return chang;
+  return chang;
 }

+ 28 - 28
vscode/src/test/suite/index.ts

@@ -3,36 +3,36 @@ import * as Mocha from 'mocha';
 import * as glob from 'glob';
 
 export function run(): Promise<void> {
-	// Create the mocha test
-	const mocha = new Mocha({
-		ui: 'tdd',
-		color: true
-	});
+  // Create the mocha test
+  const mocha = new Mocha({
+    ui: 'tdd',
+    color: true,
+  });
 
-	const testsRoot = path.resolve(__dirname, '../../../');
+  const testsRoot = path.resolve(__dirname, '../../../');
 
-	return new Promise((c, e) => {
-		glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
-			if (err) {
-				return e(err);
-			}
+  return new Promise((c, e) => {
+    glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
+      if (err) {
+        return e(err);
+      }
 
-			// Add files to the test suite
-			files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
+      // Add files to the test suite
+      files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
 
-			try {
-				// Run the mocha test
-				mocha.run(failures => {
-					if (failures > 0) {
-						e(new Error(`${failures} tests failed.`));
-					} else {
-						c();
-					}
-				});
-			} catch (err) {
-				console.error(err);
-				e(err);
-			}
-		});
-	});
+      try {
+        // Run the mocha test
+        mocha.run((failures) => {
+          if (failures > 0) {
+            e(new Error(`${failures} tests failed.`));
+          } else {
+            c();
+          }
+        });
+      } catch (err) {
+        console.error(err);
+        e(err);
+      }
+    });
+  });
 }

+ 42 - 0
vscode/src/utils/download.ts

@@ -0,0 +1,42 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+import * as crypto from 'crypto';
+import { downloadFile } from './downloadFile';
+import { promises as fs } from 'fs';
+
+interface DownloadOptions {
+  url: string;
+  mode: number;
+  dest: string;
+  progressTitle: string;
+}
+
+export default async function download(opts: DownloadOptions) {
+  const dest = path.parse(opts.dest);
+  const randomHex = crypto.randomBytes(5).toString('hex');
+  const tempFile = path.join(dest.dir, `${dest.name}${randomHex}`);
+
+  await vscode.window.withProgress(
+    {
+      location: vscode.ProgressLocation.Notification,
+      cancellable: false,
+      title: opts.progressTitle,
+    },
+    async (progress) => {
+      let lastPercentage = 0;
+      await downloadFile(opts.url, tempFile, opts.mode, (readBytes, totalBytes) => {
+        const newPercentage = Math.round((readBytes / totalBytes) * 100);
+        if (newPercentage !== lastPercentage) {
+          progress.report({
+            message: `${newPercentage.toFixed(0)}%`,
+            increment: newPercentage - lastPercentage,
+          });
+
+          lastPercentage = newPercentage;
+        }
+      });
+    }
+  );
+
+  await fs.rename(tempFile, opts.dest);
+}

+ 56 - 0
vscode/src/utils/downloadFile.ts

@@ -0,0 +1,56 @@
+import fetch from 'node-fetch';
+import { PathLike, createWriteStream } from 'fs';
+import * as stream from 'stream';
+import * as util from 'util';
+import { assert } from 'console';
+
+const pipeline = util.promisify(stream.pipeline);
+
+export async function downloadFile(
+  url: string,
+  destFilePath: PathLike,
+  mode: number | undefined,
+  onProgress: (readBytes: number, totalBytes: number) => void
+) {
+  const res = await fetch(url);
+
+  if (!res.ok) {
+    console.error('Error', res.status, 'while downloading file from', url);
+    console.error({ body: await res.text(), headers: res.headers });
+
+    throw new Error(`Got response ${res.status} when trying to download a file.`);
+  }
+
+  const totalBytes = Number(res.headers.get('content-length'));
+  assert(!Number.isNaN(totalBytes), 'Sanity check of content-length protocol');
+
+  console.debug('Downloading file of', totalBytes, 'bytes size from', url, 'to', destFilePath);
+
+  let readBytes = 0;
+  res.body.on('data', (chunk: Buffer) => {
+    readBytes += chunk.length;
+    onProgress(readBytes, totalBytes);
+  });
+
+  const destFileStream = createWriteStream(destFilePath, { mode });
+  const srcStream = res.body;
+
+  await pipeline(srcStream, destFileStream);
+
+  // Don't apply the workaround in fixed versions of nodejs, since the process
+  // freezes on them, the process waits for no-longer emitted `close` event.
+  // The fix was applied in commit 7eed9d6bcc in v13.11.0
+  // See the nodejs changelog:
+  // https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V13.md
+  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  const [, major, minor] = /v(\d+)\.(\d+)\.(\d+)/.exec(process.version)!;
+  if (+major > 13 || (+major === 13 && +minor >= 11)) {
+    return;
+  }
+
+  await new Promise<void>((resolve) => {
+    destFileStream.on('close', resolve);
+    destFileStream.destroy();
+    // This workaround is awaiting to be removed when vscode moves to newer nodejs version:
+  });
+}

+ 27 - 0
vscode/src/utils/downloadWithRetryDialog.ts

@@ -0,0 +1,27 @@
+import * as vscode from 'vscode';
+
+export default async function downloadWithRetryDialog<T>(downloadFunc: () => Promise<T>): Promise<T> {
+  // eslint-disable-next-line no-constant-condition
+  while (true) {
+    try {
+      return await downloadFunc();
+    } catch (e) {
+      const selected = await vscode.window.showErrorMessage(
+        'Failed to download: ' + e.message,
+        {},
+        {
+          title: 'Retry download',
+          retry: true,
+        },
+        {
+          title: 'Dismiss',
+        }
+      );
+
+      if (selected?.retry) {
+        continue;
+      }
+      throw e;
+    }
+  }
+}

+ 8 - 0
vscode/src/utils/expandPathResolving.ts

@@ -0,0 +1,8 @@
+import { homedir } from 'os';
+
+export default function expandPathResolving(path: string): string {
+  if (path.startsWith('~/')) {
+    return path.replace('~', homedir());
+  }
+  return path;
+}

+ 15 - 0
vscode/src/utils/fetchLatestRelease.ts

@@ -0,0 +1,15 @@
+import fetch from 'node-fetch';
+
+export default async function fetchLatestRelease() {
+  const RELEASE_URL = 'https://api.github.com/repos/hyperledger-labs/solang/releases/latest';
+  const response = await fetch(RELEASE_URL);
+
+  if (!response.ok) {
+    console.error('Error fetching artifact release info');
+
+    throw new Error(`Got response ${response.status} when trying to fetch release info`);
+  }
+
+  const release = await response.json();
+  return release;
+}

+ 16 - 0
vscode/src/utils/getPlatform.ts

@@ -0,0 +1,16 @@
+export default function getPlatform(): string | undefined {
+  switch (`${process.arch} ${process.platform}`) {
+    case 'ia32 win32':
+    case 'x64 win32':
+    case 'arm64 win32':
+      return 'solang.exe';
+    case 'x64 linux':
+    case 'arm64 linux':
+      return 'solang-linux';
+    case 'x64 darwin':
+    case 'arm64 darwin':
+      return 'solang-mac';
+    default:
+      return;
+  }
+}

+ 74 - 0
vscode/src/utils/getServer.ts

@@ -0,0 +1,74 @@
+import { assert } from 'console';
+import * as vscode from 'vscode';
+import * as path from 'path';
+import { promises as fs } from 'fs';
+import expandPathResolving from './expandPathResolving';
+import getPlatform from './getPlatform';
+import downloadWithRetryDialog from './downloadWithRetryDialog';
+import fetchLatestRelease from './fetchLatestRelease';
+import download from './download';
+import { getServerPath, setServerPath } from './serverPath';
+
+interface Artifact {
+  name: string;
+  browser_download_url: string;
+}
+
+export default async function getServer(context: vscode.ExtensionContext): Promise<string | undefined> {
+  const config = vscode.workspace.getConfiguration('solang');
+
+  const explicitPath = getServerPath(context);
+  if (explicitPath) {
+    if (explicitPath.startsWith('~/')) {
+      return expandPathResolving(explicitPath);
+    }
+    return explicitPath;
+  }
+
+  const platfrom = getPlatform();
+  if (platfrom === undefined) {
+    await vscode.window.showErrorMessage("Unfortunately we don't ship binaries for your platform yet.");
+    return undefined;
+  }
+
+  const dest = path.join(context.globalStoragePath, platfrom);
+  const exists = await fs.stat(dest).then(
+    () => true,
+    () => false
+  );
+  if (!exists) {
+    await context.globalState.update('serverVersion', undefined);
+  }
+
+  if (config.get('updates.askBeforeDownload')) {
+    const userResponse = await vscode.window.showInformationMessage(
+      'Language server for solang is not installed.',
+      'Download now'
+    );
+
+    if (userResponse !== 'Download now') {
+      return dest;
+    }
+  }
+
+  const release = await downloadWithRetryDialog(async () => {
+    return await fetchLatestRelease();
+  });
+  const version = release.tag_name;
+
+  const artifact = release.assets.find((artifact: Artifact) => artifact.name === platfrom);
+  assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
+
+  await downloadWithRetryDialog(async () => {
+    await download({
+      url: artifact.browser_download_url,
+      dest,
+      progressTitle: `Downloading Solang Solidity Compiler version ${version}`,
+      mode: 0o755,
+    });
+  });
+
+  await setServerPath(context, dest);
+
+  return dest;
+}

+ 7 - 0
vscode/src/utils/isValidExecutable.ts

@@ -0,0 +1,7 @@
+import { spawnSync } from 'child_process';
+
+export default function isValidExecutable(path: string): boolean {
+  console.debug('Checking availability of a binary at', path);
+  const res = spawnSync(path, ['--version'], { encoding: 'utf8' });
+  return res.status === 0;
+}

+ 9 - 0
vscode/src/utils/serverPath.ts

@@ -0,0 +1,9 @@
+import * as vscode from 'vscode';
+
+export function getServerPath(context: vscode.ExtensionContext): string | undefined {
+  return context.globalState.get('languageServerExecutable');
+}
+
+export function setServerPath(context: vscode.ExtensionContext, path: string | undefined) {
+  return context.globalState.update('languageServerExecutable', path);
+}