Skip to content

Commit

Permalink
feat: Fallback to WebAssembly language server automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
slavek-kucera authored Mar 15, 2024
1 parent 6749e73 commit 5230327
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 104 deletions.
1 change: 1 addition & 0 deletions clients/vscode-hlasmplugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#### Added
- New document outline implementation
- Fallback to WebAssembly language server automatically

## [1.12.0](https://github.com/eclipse-che4z/che-che4z-lsp-for-hlasm/compare/1.11.1...1.12.0) (2024-03-05)

Expand Down
137 changes: 88 additions & 49 deletions clients/vscode-hlasmplugin/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { blockCommentCommand, CommentOption, lineCommentCommand } from './commen
import { HLASMCodeActionsProvider } from './hlasmCodeActionsProvider';
import { hlasmplugin_folder, hlasmplugin_folder_filter, bridge_json_filter } from './constants';
import { ConfigurationsHandler } from './configurationsHandler';
import { getLanguageClientMiddleware } from './languageClientMiddleware';
import { HlasmPluginMiddleware, getLanguageClientMiddleware } from './languageClientMiddleware';
import { HLASMExternalFiles } from './hlasmExternalFiles';
import { HLASMExternalFilesFtp } from './hlasmExternalFilesFtp';
import { HLASMExternalConfigurationProvider, HLASMExternalConfigurationProviderHandler } from './hlasmExternalConfigurationProvider';
Expand All @@ -42,6 +42,7 @@ import { pickUser } from './uiUtils';
import { activateBranchDecorator } from './branchDecorator';
import { asError } from './helpers';
import { registerListingServices } from './hlasmListingServices';
import { MementoKey } from './mementoKeys';

export const EXTENSION_ID = "broadcommfd.hlasm-language-support";

Expand Down Expand Up @@ -75,17 +76,25 @@ const getCacheInfo = async (uri: vscode.Uri, fs: vscode.FileSystem) => {
}
}

function whenString(x: any): string | undefined {
if (typeof x === 'string')
return x;
else
return undefined;
}

/**
* ACTIVATION
* activates the extension
*/
export async function activate(context: vscode.ExtensionContext): Promise<HlasmExtension> {
const serverVariant = getConfig<ServerVariant>('serverVariant', 'native');
const version = whenString(context.extension.packageJSON?.version);

const telemetry = createTelemetry();
context.subscriptions.push(telemetry);

telemetry.reportEvent("hlasm.activated", {
telemetry.reportEvent('hlasm.activated', {
server_variant: serverVariant.toString(),
showBranchInformation: getConfig<boolean>('showBranchInformation', true).toString(),
});
Expand All @@ -112,43 +121,17 @@ export async function activate(context: vscode.ExtensionContext): Promise<HlasmE
middleware: middleware,
};

//client init
const hlasmpluginClient = await createLanguageServer(serverVariant, clientOptions, context.extensionUri);

context.subscriptions.push(hlasmpluginClient);
context.subscriptions.push(hlasmpluginClient.onDidChangeState(e => e.newState === vscodelc.State.Starting && middleware.resetFirstOpen()));

clientErrorHandler.defaultHandler = hlasmpluginClient.createDefaultErrorHandler();

const extConfProvider = new HLASMExternalConfigurationProvider(hlasmpluginClient);
context.subscriptions.push(extConfProvider);

// The objectToString is necessary, because telemetry reporter only takes objects with
// string properties and there are some boolean that we receive from the language server
hlasmpluginClient.onTelemetry((object) => { telemetry.reportEvent(object.method_name, objectToString(object.properties), object.measurements) });

const extConfProvider = new HLASMExternalConfigurationProvider();
const extFiles = new HLASMExternalFiles(
externalFilesScheme,
hlasmpluginClient,
vscode.workspace.fs,
await getCacheInfo(vscode.Uri.joinPath(context.globalStorageUri, 'external.files.cache'), vscode.workspace.fs)
);
context.subscriptions.push(extFiles);

//give the server some time to start listening when using TCP
if (serverVariant === 'tcp')
await sleep(2000);

try {
await hlasmpluginClient.start();
}
catch (e) {
if (serverVariant === 'native')
offerSwitchToWasmClient();

telemetry.reportException(asError(e));
throw e;
}
const hlasmpluginClient = await startLanguageServerWithFallback({
version, serverVariant, clientOptions, context, telemetry, clientErrorHandler, middleware, extConfProvider, extFiles,
});

// register all commands and objects to context
await registerDebugSupport(context, hlasmpluginClient);
Expand All @@ -169,23 +152,79 @@ export async function activate(context: vscode.ExtensionContext): Promise<HlasmE
return api;
}

function offerSwitchToWasmClient() {
const use_wasm = 'Switch to WASM version';
vscode.window.showWarningMessage('The language server did not start.', ...[use_wasm, 'Ignore']).then((value) => {
if (value === use_wasm) {
vscode.workspace.getConfiguration('hlasm').update('serverVariant', 'wasm', vscode.ConfigurationTarget.Global).then(
() => {
const reload = 'Reload window';
vscode.window.showInformationMessage('User settings updated.', ...[reload]).then((value) => {
if (value === reload)
vscode.commands.executeCommand('workbench.action.reloadWindow');
})
},
(error) => {
vscode.window.showErrorMessage(error);
});
}
});
type LangStartOptions = {
version: string | undefined,
serverVariant: ServerVariant;
clientOptions: vscodelc.LanguageClientOptions;
context: vscode.ExtensionContext;
telemetry: Telemetry,
clientErrorHandler: LanguageClientErrorHandler,
middleware: HlasmPluginMiddleware,
extConfProvider: HLASMExternalConfigurationProvider,
extFiles: HLASMExternalFiles,
}

async function startLanguageServerWithFallback(opts: LangStartOptions) {
let lsResult = await startLanguageServer(opts);
if (!(lsResult instanceof Error))
return lsResult;

if (opts.serverVariant === 'wasm') {
opts.telemetry.reportException(lsResult);
throw lsResult;
}

const lastCrashVersion = opts.context.globalState.get(MementoKey.LastCrashVersion);
if (opts.version)
await opts.context.globalState.update(MementoKey.LastCrashVersion, opts.version);

if (!opts.version || opts.version !== lastCrashVersion)
vscode.window.showWarningMessage('The language server did not start. Switching to WASM version.');

opts.telemetry.reportEvent('hlasm.wasmFallback');

opts.serverVariant = 'wasm';
lsResult = await startLanguageServer(opts);

if (!(lsResult instanceof Error))
return lsResult;

opts.telemetry.reportException(lsResult);
throw lsResult;
}

async function startLanguageServer(opts: LangStartOptions): Promise<vscodelc.BaseLanguageClient | Error> {
const disposables: vscode.Disposable[] = [];

//client init
const hlasmpluginClient = await createLanguageServer(opts.serverVariant, opts.clientOptions, opts.context.extensionUri);

disposables.push(hlasmpluginClient.onDidChangeState(e => e.newState === vscodelc.State.Starting && opts.middleware.resetFirstOpen()));

opts.clientErrorHandler.defaultHandler = hlasmpluginClient.createDefaultErrorHandler();

// The objectToString is necessary, because telemetry reporter only takes objects with
// string properties and there are some boolean that we receive from the language server
disposables.push(hlasmpluginClient.onTelemetry((object) => { opts.telemetry.reportEvent(object.method_name, objectToString(object.properties), object.measurements) }));

disposables.push(opts.extConfProvider.attach(hlasmpluginClient));
disposables.push(opts.extFiles.attach(hlasmpluginClient));

//give the server some time to start listening when using TCP
if (opts.serverVariant === 'tcp')
await sleep(2000);

try {
await hlasmpluginClient.start();
opts.context.subscriptions.push(hlasmpluginClient, ...disposables);
return hlasmpluginClient;
}
catch (e) {
const err = asError(e);
disposables.reverse().forEach(x => x.dispose());
opts.clientErrorHandler.defaultHandler = undefined;
return err;
}
}

async function registerEditHelpers(context: vscode.ExtensionContext) {
Expand Down
7 changes: 3 additions & 4 deletions clients/vscode-hlasmplugin/src/ftpCreds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import * as vscode from 'vscode';
import { askUser, pickUser } from './uiUtils';
import { AccessOptions } from 'basic-ftp';
import { MementoKey } from './mementoKeys';

export enum connectionSecurityLevel {
"rejectUnauthorized",
Expand Down Expand Up @@ -106,10 +107,8 @@ export async function gatherConnectionInfo(lastInput: {
return { host, port, user, password, hostInput, securityLevel, zowe: false };
}

const mementoKey = "hlasm.downloadDependencies";

export function getLastRunConfig(context: vscode.ExtensionContext) {
let lastRun = context.globalState.get(mementoKey, { host: '', user: '', jobcard: '' });
let lastRun = context.globalState.get(MementoKey.DownloadDependencies, { host: '', user: '', jobcard: '' });
return {
host: '' + (lastRun.host || ''),
user: '' + (lastRun.user || ''),
Expand All @@ -123,4 +122,4 @@ interface DownloadDependenciesInputMemento {
jobcard: string;
};

export const updateLastRunConfig = (context: vscode.ExtensionContext, lastInput: DownloadDependenciesInputMemento) => context.globalState.update(mementoKey, lastInput);
export const updateLastRunConfig = (context: vscode.ExtensionContext, lastInput: DownloadDependenciesInputMemento) => context.globalState.update(MementoKey.DownloadDependencies, lastInput);
Original file line number Diff line number Diff line change
Expand Up @@ -189,20 +189,21 @@ export interface ConfigurationProviderRegistration {
invalidate(uri: vscode.Uri | null): PromiseLike<void> | void;
};

type ChannelType = {
onRequest<R, E>(method: string, handler: vscodelc.GenericRequestHandler<R, E>): vscode.Disposable;
sendNotification(method: string, params: any): Promise<void>;
}

export class HLASMExternalConfigurationProvider {
private toDispose: vscode.Disposable[] = [];
private requestHandlers: HLASMExternalConfigurationProviderHandler[] = [];

constructor(
private channel: {
onRequest<R, E>(method: string, handler: vscodelc.GenericRequestHandler<R, E>): vscode.Disposable;
sendNotification(method: string, params: any): Promise<void>;
}) {
this.toDispose.push(this.channel.onRequest('external_configuration_request', (...params: any[]) => this.handleRawMessage(...params)));
}

dispose() {
this.toDispose.forEach(x => x.dispose());
private channel?: ChannelType = undefined;

public attach(channel: ChannelType) {
this.channel = channel;
return vscode.Disposable.from(
{ dispose: () => { this.channel = undefined; } },
channel.onRequest('external_configuration_request', (...params: any[]) => this.handleRawMessage(...params))
);
}

private async handleRequest(uri: vscode.Uri): Promise<ExternalConfigurationResponse | vscodelc.ResponseError> {
Expand Down Expand Up @@ -233,7 +234,7 @@ export class HLASMExternalConfigurationProvider {
}

private invalidateConfiguration(uri: vscode.Uri | null) {
this.channel.sendNotification('invalidate_external_configuration', uri ? { uri: uri.toString() } : {});
this.channel?.sendNotification('invalidate_external_configuration', uri ? { uri: uri.toString() } : {});
}

public addHandler(h: HLASMExternalConfigurationProviderHandler): ConfigurationProviderRegistration {
Expand Down
35 changes: 23 additions & 12 deletions clients/vscode-hlasmplugin/src/hlasmExternalFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,28 +155,40 @@ interface ClientInstance<ConnectArgs, ReadArgs extends ClientUriDetails, ListArg

function asFragment(s: string) { return s ? '#' + s : ''; }

export class HLASMExternalFiles {
private toDispose: vscode.Disposable[] = [];
type ChannelType = {
onNotification(method: string, handler: vscodelc.GenericNotificationHandler): vscode.Disposable;
sendNotification<P>(type: vscodelc.NotificationType<P>, params?: P): Promise<void>;
sendNotification(method: string, params: any): Promise<void>;
};

export class HLASMExternalFiles {
private memberLists = new Map<string, CacheEntry<string[]>>();
private memberContent = new Map<string, CacheEntry<string>>();

private pendingRequests = new Set<{ topic: string }>();

private clients = new Map<string, ClientInstance<any, any, any>>();

private channel?: ChannelType = undefined;

constructor(
private magicScheme: string,
private channel: {
onNotification(method: string, handler: vscodelc.GenericNotificationHandler): vscode.Disposable;
sendNotification<P>(type: vscodelc.NotificationType<P>, params?: P): Promise<void>;
sendNotification(method: string, params: any): Promise<void>;
},
private fs: vscode.FileSystem,
private cache?: { uri: vscode.Uri }) {
this.toDispose.push(this.channel.onNotification('external_file_request', params => this.handleRawMessage(params).then(
msg => { if (msg) this.channel.sendNotification('external_file_response', msg); }
)));
}

public attach(channel: {
onNotification(method: string, handler: vscodelc.GenericNotificationHandler): vscode.Disposable;
sendNotification<P>(type: vscodelc.NotificationType<P>, params?: P): Promise<void>;
sendNotification(method: string, params: any): Promise<void>;
}) {
this.channel = channel;
return vscode.Disposable.from(
{ dispose: () => { this.channel = undefined; } },
channel.onNotification('external_file_request', params => this.handleRawMessage(params).then(
msg => { if (msg) channel.sendNotification('external_file_response', msg); }
))
);
}

setClient<ConnectArgs, ReadArgs extends ClientUriDetails, ListArgs extends ClientUriDetails>(
Expand Down Expand Up @@ -304,7 +316,7 @@ export class HLASMExternalFiles {
}

private notifyAllWorkspaces(service: string, all: boolean) {
this.channel.sendNotification(vscodelc.DidChangeWatchedFilesNotification.type, {
this.channel?.sendNotification(vscodelc.DidChangeWatchedFilesNotification.type, {
changes: (vscode.workspace.workspaceFolders || []).map(w => {
return {
uri: `${this.magicScheme}:/${service}${asFragment(uriFriendlyBase16Encode(w.uri.toString()))}`,
Expand Down Expand Up @@ -394,7 +406,6 @@ export class HLASMExternalFiles {
public reset() { this.pendingRequests.clear(); }

dispose() {
this.toDispose.forEach(x => x.dispose());
[...this.clients.values()].forEach(x => x.dispose());
}

Expand Down
18 changes: 18 additions & 0 deletions clients/vscode-hlasmplugin/src/mementoKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2024 Broadcom.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*/

export const enum MementoKey {
DownloadDependencies = 'hlasm.downloadDependencies',
LastCrashVersion = 'hlasm.lastCrashVersion',
}
Loading

0 comments on commit 5230327

Please sign in to comment.