Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ jobs:
name: vscode-escript-native.win32-x64
path: native/build/Release/vscode-escript-native.win32-x64.node

# Kept out of the .vsix (see .vscodeignore) but retained here so a crash
# dump from a released build can be resolved to a function and line.
- name: Upload Debug Symbols
uses: actions/upload-artifact@v4
with:
name: vscode-escript-native.win32-x64.symbols
path: native/build/Release/vscode-escript-native.win32-x64.pdb
if-no-files-found: warn

mac:
name: Build and Test [Mac]
runs-on: macos-latest
Expand Down
7 changes: 2 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@
"name": "Launch Client",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
"outFiles": ["${workspaceRoot}/client/out/**/*.js"],
"preLaunchTask": {
"type": "npm",
"script": "watch"
}
"outFiles": ["${workspaceRoot}/dist/**/*.js"],
"preLaunchTask": "watch-all"
},
{
"name": "Attach to Language Server",
Expand Down
36 changes: 33 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,47 @@
"type": "npm",
"script": "watch",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
"presentation": {
"panel": "dedicated",
"reveal": "never"
},
"problemMatcher": {
"owner": "esbuild",
"fileLocation": "relative",
"pattern": {
"regexp": "^✘ \\[ERROR\\] (.*)$",
"message": 1
},
"background": {
"activeBegins": true,
"beginsPattern": "\\[watch\\] build started",
"endsPattern": "\\[watch\\] build finished"
}
}
},
{
"type": "npm",
"script": "watch:tsc",
"isBackground": true,
"presentation": {
"panel": "dedicated",
"reveal": "never"
},
"problemMatcher": [
"$tsc-watch"
]
},
{
"label": "watch-all",
"dependsOn": [
"npm: watch",
"npm: watch:tsc"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
}
]
}
43 changes: 30 additions & 13 deletions .vscodeignore
Original file line number Diff line number Diff line change
@@ -1,30 +1,47 @@
# Sources, build inputs and test material.
.vscode/**
.github/**
.gitignore
doc-assets/**/*
docs/**
**/*.ts
**/*.map
**/coverage
**/*.d.ts
.gitignore
**/coverage
**/tsconfig.json
**/tsconfig.base.json
.github/**
**/package-lock.json
**/*.tsbuildinfo
.gitmodules
native/jest.config.js
client/test/**
client/node_modules/**
!client/node_modules/vscode-jsonrpc/**
!client/node_modules/vscode-languageclient/**
!client/node_modules/vscode-languageserver-protocol/**
!client/node_modules/vscode-languageserver-types/**
!client/node_modules/{minimatch,brace-expansion,concat-map,balanced-match}/**
!client/node_modules/{semver,lru-cache,yallist}/**
server/test/**
native/test/**
client/jest.e2e.config.js
server/jest.config.js
jest.config.js
eslint.config.js
esbuild.mjs
grammars/*.yml

# The client and server ship as the two bundles in dist/. Everything the old
# unbundled layout needed -- tsc output next to three near-duplicate copies of
# the vscode-languageserver trees -- is dead weight now, so no negation rules
# are needed to carve exceptions out of node_modules.
client/out/**
server/out/**
native/out/**
node_modules/**
client/node_modules/**
server/node_modules/**
native/node_modules/**

# Native addon: ship the built binary, not the sources, the CMake tree or the
# ~40 MB PDB that Release builds now emit.
native/.clang-format
native/CMakeLists.txt
native/cpp/**
native/prebuilds/**
native/build/**
!native/build/Release/**
native/polserver/**
native/node_modules/**
native/build/**
!native/build/Release/*.node
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ https://github.com/polserver/vscode-escript/issues.
- Run the launch config.
- In the new Visual Studio Code [Extension Development Host], open a POL distro
folder that contains `pol.cfg` and `scripts/ecompile.cfg`.
- Check the "ECompile Language Server" channel in the Output pane.
- Check the "EScript Language Server" channel in the Output pane.

## NVIM LSP integration
Run `npm install && npm build` in this folder. This installs all necessary
Expand Down
4 changes: 3 additions & 1 deletion client/src/activatePolDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ class PolDebugConfigurationProvider implements vscode.DebugConfigurationProvider
config.pid = selectionPid;
}
} catch (e) {
vscode.window.showErrorMessage(`Could not get process list from debug server: ${e.message}`).then(_ => { });
const detail = e instanceof Error ? (e.stack ?? e.message) : String(e);
console.error(`Could not get process list from debug server: ${detail}`);
vscode.window.showErrorMessage(`Could not get process list from debug server: ${e instanceof Error ? e.message : String(e)}`).then(_ => { });
return undefined;
} finally {
try {
Expand Down
91 changes: 78 additions & 13 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,57 @@
* ------------------------------------------------------------------------------------------ */

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';
import { workspace, ExtensionContext, Uri, Disposable } from 'vscode';

import { activatePolDebug } from './activatePolDebug';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
State,
TransportKind
} from 'vscode-languageclient/node';

let client: LanguageClient;
let client: LanguageClient | undefined;

export function activate(context: ExtensionContext) {
/**
* Whether any workspace folder is a POL distribution.
*
* This is the same test the server applies in `onInitialize`: without both files
* it never calls `workspace.open()`, so `compilercfg` is never read and no
* analysis can work. Checking here as well means a workspace the server would
* have rejected does not get a forked Node process and a 2.6 MB native addon
* loaded into it first.
*
* It matters because the extension activates more widely than it can serve:
* `contributes.languages` binds `escriptcfg` to `.cfg`, so opening a `setup.cfg`
* in an unrelated project activates this extension.
*/
async function isPolWorkspace(): Promise<boolean> {
for (const folder of workspace.workspaceFolders ?? []) {
try {
await workspace.fs.stat(Uri.joinPath(folder.uri, 'pol.cfg'));
await workspace.fs.stat(Uri.joinPath(folder.uri, 'scripts', 'ecompile.cfg'));
return true;
} catch {
// Not a POL distribution; try the next folder.
}
}
return false;
}

function startClient(context: ExtensionContext): LanguageClient {
// The server is implemented in node
let serverModule = context.asAbsolutePath(
path.join('server', 'out', 'index.js')
const serverModule = context.asAbsolutePath(
path.join('dist', 'server.js')
);
// The debug options for the server
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
const args = [`--storageUri=${context.storageUri}`];

// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
let serverOptions: ServerOptions = {
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc, args },
debug: {
module: serverModule,
Expand All @@ -40,7 +65,7 @@ export function activate(context: ExtensionContext) {
};

// Options to control the language client
let clientOptions: LanguageClientOptions = {
const clientOptions: LanguageClientOptions = {
// Register the server for plain text documents
documentSelector: [{ scheme: 'file', language: 'escript' }],
synchronize: {
Expand All @@ -52,24 +77,64 @@ export function activate(context: ExtensionContext) {
}
};

// Create the language client and start the client.
client = new LanguageClient(
const started = new LanguageClient(
'escript',
'EScript Language Server',
serverOptions,
clientOptions
);

// Start the client. This will also launch the server
client.start();
started.start();

return started;
}

export async function activate(context: ExtensionContext) {
// Cheap and unconditional: registering the debug providers costs nothing and
// must happen before a 'pol' session is resolved.
activatePolDebug(context);

workspace.onDidChangeConfiguration(e => {
client.sendNotification('didChangeConfiguration', {
// Without this check every settings change in the window -- including
// unrelated extensions' -- pushes a config notification, which can
// restart the workspace cache build.
if (!e.affectsConfiguration('escript')) {
return;
}
client?.sendNotification('didChangeConfiguration', {
configuration: workspace.getConfiguration('escript')
});
}, undefined, context.subscriptions);

const startIfPol = async () => {
if (client || !await isPolWorkspace()) {
return;
}
// Checked again: the activation-time call and a folder event can both
// pass the first check before either probe resolves.
if (client) {
return;
}
client = startClient(context);
};

// A POL distribution can be added to the window after activation, so the
// check has to be repeated rather than made once at startup.
let folderWatch: Disposable | undefined = workspace.onDidChangeWorkspaceFolders(async () => {
await startIfPol();
if (client) {
folderWatch?.dispose();
folderWatch = undefined;
}
});
context.subscriptions.push({ dispose: () => folderWatch?.dispose() });

await startIfPol();
if (client) {
folderWatch.dispose();
folderWatch = undefined;
}
}

export function deactivate(): Thenable<void> | undefined {
Expand Down
53 changes: 53 additions & 0 deletions esbuild.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Bundles the extension host client and the language server.
*
* Before this, both shipped as raw `tsc` output next to their own
* `node_modules`, so the `.vsix` carried three near-identical copies of the
* `vscode-jsonrpc` / `vscode-languageserver-*` trees -- ~750 files and ~4.3 MB
* to support ~67 KB of project code -- and the server paid for resolving and
* parsing all of it on every boot.
*
* Two things are deliberately left external:
*
* - `vscode`, which is injected by the extension host and has no package.
* - `*.node`, the compiled addon. `native/src/index.ts` locates it at runtime
* through a computed `require()`, which esbuild leaves alone; the roots it
* searches account for this file's bundled location.
*/
import { build, context } from 'esbuild';

const watch = process.argv.includes('--watch');
const production = !watch;

/** @type {import('esbuild').BuildOptions} */
const common = {
bundle: true,
format: 'cjs',
platform: 'node',
target: 'node18',
external: ['vscode', '*.node'],
minify: false,
sourcemap: production ? false : 'inline',
logLevel: 'info'
};

const entries = [
{
...common,
entryPoints: ['client/src/extension.ts'],
outfile: 'dist/extension.js'
},
{
...common,
entryPoints: ['server/src/index.ts'],
outfile: 'dist/server.js'
}
];

if (watch) {
// The Extension Development Host loads `dist/`, so the debug launch's watch
// task has to rebuild it; `tsc -b -w` alone only refreshes `*/out`.
await Promise.all(entries.map(async options => (await context(options)).watch()));
} else {
await Promise.all(entries.map(options => build(options)));
}
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ module.exports = [
'native/node_modules/**',
'native/polserver/**',
'native/out/**',
// Generated bundles, not sources.
'dist/**',
'coverage',
]
}
Expand Down
Loading
Loading