Skip to content

Commit

Permalink
Extract LanguageClient from ClientHandler (#1082)
Browse files Browse the repository at this point in the history
This extracts the LanguageClient creation from ClientHandler and puts it directly inside the activation point.

In order to implement hashicorp/terraform-ls#722 we need to add a new StaticFeature to register a client-side command for terraform-ls to call when it knows the `terraform.providers` view needs to be refreshed.

StaticFeatures are registered using the LanguageClient.registerFeature method, which means the ClientHandler class needs to create the StaticFeature. The StaticFeature needs both the LanguageClient and `terraform.providers` view created before it can be initialized. The LanguageClient is created by the ClientHandler class. This all results in a cyclic dependency.

We could resolve this cyclic dependency by adding more responsibility to ClientHandler, but this introduces more complexity to the class. This will become increasingly hard to deal with as more aspects like StaticFeature are added.

We resolve this by extracting the creation of LanguageClient and moving dependent features like the StaticFeatures to the main activation method. This puts all the parts that rely on each other in the same place where the data needed is located to make decisions about whether they are activated or not.

This builds on work done in:

- #1073
- #1074
- #1075
- #1079
  • Loading branch information
jpogran authored Apr 28, 2022
1 parent e119551 commit 2b50faf
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 218 deletions.
168 changes: 0 additions & 168 deletions src/clientHandler.ts

This file was deleted.

111 changes: 86 additions & 25 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import * as vscode from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { ExecuteCommandParams, ExecuteCommandRequest } from 'vscode-languageclient';
import { LanguageClient } from 'vscode-languageclient/node';
import {
DocumentSelector,
ExecuteCommandParams,
ExecuteCommandRequest,
LanguageClient,
LanguageClientOptions,
RevealOutputChannelOn,
State,
StaticFeature,
ServerOptions,
} from 'vscode-languageclient/node';
import { Utils } from 'vscode-uri';
import { ClientHandler } from './clientHandler';
import { clientSupportsCommand, getInitializationOptions, getServerExecutable } from './utils/clientHelpers';
import { GenerateBugReportCommand } from './commands/generateBugReport';
import { ModuleCallsDataProvider } from './providers/moduleCalls';
import { ModuleProvidersDataProvider } from './providers/moduleProviders';
import { ServerPath } from './utils/serverPath';
import { config, getActiveTextEditor, isTerraformFile } from './utils/vscode';
import { TelemetryFeature } from './features/telemetry';
import { ShowReferencesFeature } from './features/showReferences';
import { CustomSemanticTokens } from './features/semanticTokens';

const id = 'terraform';
const brand = `HashiCorp Terraform`;
const documentSelector: DocumentSelector = [
{ scheme: 'file', language: 'terraform' },
{ scheme: 'file', language: 'terraform-vars' },
];
const outputChannel = vscode.window.createOutputChannel(brand);
export let terraformStatus: vscode.StatusBarItem;

let reporter: TelemetryReporter;
let clientHandler: ClientHandler;
let client: LanguageClient;

export async function activate(context: vscode.ExtensionContext): Promise<void> {
const manifest = context.extension.packageJSON;
Expand Down Expand Up @@ -43,7 +60,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
vscode.ConfigurationTarget.Global,
);
}
return startLanguageServer();
return startLanguageServer(context);
}),
vscode.commands.registerCommand('terraform.disableLanguageServer', async () => {
if (enabled()) {
Expand Down Expand Up @@ -85,9 +102,48 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
}

const lsPath = new ServerPath(context);
clientHandler = new ClientHandler(lsPath, outputChannel, reporter, manifest);
if (lsPath.hasCustomBinPath()) {
reporter.sendTelemetryEvent('usePathToBinary');
}
const executable = await getServerExecutable(lsPath);
const serverOptions: ServerOptions = {
run: executable,
debug: executable,
};
outputChannel.appendLine(`Launching language server: ${executable.command} ${executable.args?.join(' ')}`);

const initializationOptions = getInitializationOptions();
const clientOptions: LanguageClientOptions = {
documentSelector: documentSelector,
initializationOptions: initializationOptions,
initializationFailedHandler: (error) => {
reporter.sendTelemetryException(error);
return false;
},
outputChannel: outputChannel,
revealOutputChannelOn: RevealOutputChannelOn.Never,
};

client = new LanguageClient(id, serverOptions, clientOptions);
client.onDidChangeState((event) => {
console.log(`Client: ${State[event.oldState]} --> ${State[event.newState]}`);
if (event.newState === State.Stopped) {
reporter.sendTelemetryEvent('stopClient');
}
});

const features: StaticFeature[] = [new CustomSemanticTokens(client, manifest)];
if (vscode.env.isTelemetryEnabled) {
features.push(new TelemetryFeature(client, reporter));
}
const codeLensReferenceCount = config('terraform').get<boolean>('codelens.referenceCount');
if (codeLensReferenceCount) {
features.push(new ShowReferencesFeature(client));
}

client.registerFeatures(features);

await startLanguageServer();
await startLanguageServer(context);

// these need the LS to function, so are only registered if enabled
context.subscriptions.push(
Expand All @@ -100,7 +156,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
defaultUri: workspaceFolders ? workspaceFolders[0]?.uri : undefined,
openLabel: 'Initialize',
});
const client = clientHandler.getClient();
if (selected && client) {
const moduleUri = selected[0];
const requestParams: ExecuteCommandParams = {
Expand All @@ -110,11 +165,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
await execWorkspaceCommand(client, requestParams);
}
}),
vscode.window.registerTreeDataProvider('terraform.modules', new ModuleCallsDataProvider(context, clientHandler)),
vscode.window.registerTreeDataProvider(
'terraform.providers',
new ModuleProvidersDataProvider(context, clientHandler),
),
vscode.window.registerTreeDataProvider('terraform.modules', new ModuleCallsDataProvider(context, client)),
vscode.window.registerTreeDataProvider('terraform.providers', new ModuleProvidersDataProvider(context, client)),
vscode.window.onDidChangeVisibleTextEditors(async (editors: readonly vscode.TextEditor[]) => {
const textEditor = editors.find((ed) => !!ed.viewColumn);
if (textEditor?.document === undefined) {
Expand All @@ -131,20 +183,19 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
}

export async function deactivate(): Promise<void> {
if (clientHandler === undefined) {
if (client === undefined) {
return;
}

return clientHandler.stopClient();
return client.stop();
}

export async function updateTerraformStatusBar(documentUri: vscode.Uri): Promise<void> {
const client = clientHandler.getClient();
if (client === undefined) {
return;
}

const initSupported = clientHandler.clientSupportsCommand(`terraform-ls.terraform.init`);
const initSupported = clientSupportsCommand(client.initializeResult, `terraform-ls.terraform.init`);
if (!initSupported) {
return;
}
Expand Down Expand Up @@ -174,9 +225,22 @@ export async function updateTerraformStatusBar(documentUri: vscode.Uri): Promise
}
}

async function startLanguageServer() {
async function startLanguageServer(ctx: vscode.ExtensionContext) {
try {
await clientHandler.startClient();
console.log('Starting client');

ctx.subscriptions.push(client.start());

await client.onReady();

reporter.sendTelemetryEvent('startClient');

const initializeResult = client.initializeResult;
if (initializeResult !== undefined) {
const multiFoldersSupported = initializeResult.capabilities.workspace?.workspaceFolders?.supported;
console.log(`Multi-folder support: ${multiFoldersSupported}`);
}

vscode.commands.executeCommand('setContext', 'terraform.showTreeViews', true);
} catch (error) {
console.log(error); // for test failure reporting
Expand All @@ -190,7 +254,7 @@ async function startLanguageServer() {

async function stopLanguageServer() {
try {
await clientHandler.stopClient();
await client?.stop();
vscode.commands.executeCommand('setContext', 'terraform.showTreeViews', false);
} catch (error) {
console.log(error); // for test failure reporting
Expand Down Expand Up @@ -227,7 +291,6 @@ async function modulesCallersCommand(languageClient: LanguageClient, moduleUri:
}

export async function moduleCallers(moduleUri: string): Promise<ModuleCallersResponse> {
const client = clientHandler.getClient();
if (client === undefined) {
return {
version: 0,
Expand All @@ -244,8 +307,6 @@ export async function moduleCallers(moduleUri: string): Promise<ModuleCallersRes
async function terraformCommand(command: string, languageServerExec = true): Promise<void> {
const textEditor = getActiveTextEditor();
if (textEditor) {
const languageClient = clientHandler.getClient();

const moduleUri = Utils.dirname(textEditor.document.uri);
const response = await moduleCallers(moduleUri.toString());

Expand All @@ -266,12 +327,12 @@ async function terraformCommand(command: string, languageServerExec = true): Pro
selectedModule = moduleUri.toString();
}

if (languageServerExec && languageClient) {
if (languageServerExec && client) {
const requestParams: ExecuteCommandParams = {
command: `terraform-ls.terraform.${command}`,
arguments: [`uri=${selectedModule}`],
};
return execWorkspaceCommand(languageClient, requestParams);
return execWorkspaceCommand(client, requestParams);
} else {
const terminalName = `Terraform ${selectedModule}`;
const moduleURI = vscode.Uri.parse(selectedModule);
Expand Down
Loading

0 comments on commit 2b50faf

Please sign in to comment.