Skip to content

Commit 5170e89

Browse files
authored
Fix DocumentSelector for multi-folder workspace (#688)
* Fix DocumentSelector for multi-folder workspace * address eslint errors * ci: run linter automatically * Address review feedback
1 parent 583bdf5 commit 5170e89

5 files changed

Lines changed: 298 additions & 182 deletions

File tree

.github/workflows/test.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ on:
99
- main
1010

1111
jobs:
12+
lint:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout Repo
17+
uses: actions/checkout@v2
18+
- name: Use Node.js 14.x
19+
uses: actions/setup-node@v1
20+
with:
21+
node-version: 14.x
22+
- name: npm install
23+
run: npm install
24+
- name: lint
25+
run: npm run lint
26+
1227
test:
1328
strategy:
1429
matrix:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@
200200
"package": "vsce package",
201201
"compile": "tsc -b",
202202
"watch": "tsc -b -w",
203+
"lint": "eslint .",
203204
"test-compile": "tsc -p ./",
204205
"test": "npm run test-compile && node ./out/test/runTest.js"
205206
},

src/clientHandler.ts

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
import * as vscode from 'vscode';
2+
import ShortUniqueId from 'short-unique-id';
3+
import {
4+
Executable,
5+
LanguageClient,
6+
LanguageClientOptions,
7+
ServerOptions,
8+
State,
9+
DocumentSelector,
10+
RevealOutputChannelOn
11+
} from 'vscode-languageclient/node';
12+
import {
13+
config,
14+
getFolderName,
15+
getWorkspaceFolder,
16+
normalizeFolderName,
17+
sortedWorkspaceFolders
18+
} from './vscodeUtils';
19+
import * as path from 'path';
20+
import TelemetryReporter from 'vscode-extension-telemetry';
21+
22+
export interface TerraformLanguageClient {
23+
commandPrefix: string,
24+
client: LanguageClient
25+
}
26+
27+
const MULTI_FOLDER_CLIENT = "";
28+
const clients: Map<string, TerraformLanguageClient> = new Map();
29+
30+
/**
31+
* ClientHandler maintains lifecycles of language clients
32+
* based on the server's capabilities (whether multi-folder
33+
* workspaces are supported).
34+
*/
35+
export class ClientHandler {
36+
private shortUid: ShortUniqueId;
37+
private pathToBinary: string;
38+
private supportsMultiFolders = true;
39+
40+
constructor(private context: vscode.ExtensionContext, private reporter: TelemetryReporter ) {
41+
this.shortUid = new ShortUniqueId();
42+
this.pathToBinary = config('terraform').get('languageServer.pathToBinary');
43+
if (this.pathToBinary) {
44+
this.reporter.sendTelemetryEvent('usePathToBinary');
45+
} else {
46+
const installPath = path.join(context.extensionPath, 'lsp');
47+
this.pathToBinary = path.join(installPath, 'terraform-ls');
48+
}
49+
}
50+
51+
public startClients(folders?: string[]): vscode.Disposable[] {
52+
const disposables: vscode.Disposable[] = [];
53+
54+
if (this.supportsMultiFolders) {
55+
if (this.getClient()?.client.needsStart()) {
56+
console.log(`No need to start another client for ${folders}`)
57+
return disposables;
58+
}
59+
60+
console.log('Starting client');
61+
62+
const tfClient = this.createTerraformClient();
63+
tfClient.client.onReady().then(async () => {
64+
this.reporter.sendTelemetryEvent('startClient');
65+
const multiFoldersSupported = tfClient.client.initializeResult.capabilities.workspace?.workspaceFolders?.supported;
66+
console.log(`Multi-folder support: ${multiFoldersSupported}`);
67+
68+
if (!multiFoldersSupported) {
69+
// restart is needed to launch folder-focused instances
70+
console.log('Restarting clients as folder-focused');
71+
await this.stopClients(folders);
72+
this.supportsMultiFolders = false;
73+
this.startClients(folders);
74+
}
75+
});
76+
77+
disposables.push(tfClient.client.start());
78+
clients.set(MULTI_FOLDER_CLIENT, tfClient);
79+
80+
return disposables;
81+
}
82+
83+
if (folders && folders.length > 0) {
84+
for (const folder of folders) {
85+
if (!clients.has(folder)) {
86+
console.log(`Starting client for ${folder}`);
87+
const folderClient = this.createTerraformClient(folder);
88+
folderClient.client.onReady().then(() => {
89+
this.reporter.sendTelemetryEvent('startClient');
90+
});
91+
92+
disposables.push(folderClient.client.start());
93+
clients.set(folder, folderClient);
94+
} else {
95+
console.log(`Client for folder: ${folder} already started`);
96+
}
97+
}
98+
}
99+
return disposables;
100+
}
101+
102+
private createTerraformClient(location?: string): TerraformLanguageClient {
103+
const cmd = this.pathToBinary;
104+
const binaryName = cmd.split('/').pop();
105+
106+
const serverArgs: string[] = config('terraform').get('languageServer.args');
107+
const experimentalFeatures = config('terraform-ls').get('experimentalFeatures');
108+
109+
let channelName = `${binaryName}`;
110+
let id = `languageServer`
111+
let name = `Language Server`
112+
let wsFolder: vscode.WorkspaceFolder;
113+
let rootModulePaths: string[];
114+
let excludeModulePaths: string[];
115+
let documentSelector: DocumentSelector;
116+
let outputChannel: vscode.OutputChannel;
117+
if (location) {
118+
channelName = `${binaryName}: ${location}`;
119+
id = `languageServer/${location}`
120+
name = `Language Server: ${location}`
121+
wsFolder = getWorkspaceFolder(location);
122+
documentSelector = [
123+
{ scheme: 'file', language: 'terraform', pattern: `${wsFolder.uri.fsPath}/**/*` },
124+
{ scheme: 'file', language: 'terraform-vars', pattern: `${wsFolder.uri.fsPath}/**/*` }
125+
]
126+
rootModulePaths = config('terraform-ls', wsFolder).get('rootModules');
127+
excludeModulePaths = config('terraform-ls', wsFolder).get('excludeRootModules');
128+
outputChannel = vscode.window.createOutputChannel(channelName);
129+
outputChannel.appendLine(`Launching language server: ${cmd} ${serverArgs.join(' ')} for folder: ${location}`);
130+
} else {
131+
documentSelector = [
132+
{ scheme: 'file', language: 'terraform' },
133+
{ scheme: 'file', language: 'terraform-vars' }
134+
]
135+
rootModulePaths = config('terraform-ls').get('rootModules');
136+
excludeModulePaths = config('terraform-ls').get('excludeRootModules');
137+
outputChannel = vscode.window.createOutputChannel(channelName);
138+
outputChannel.appendLine(`Launching language server: ${cmd} ${serverArgs.join(' ')}`);
139+
}
140+
141+
if (rootModulePaths.length > 0 && excludeModulePaths.length > 0) {
142+
throw new Error('Only one of rootModules and excludeRootModules can be set at the same time, please remove the conflicting config and reload');
143+
}
144+
145+
const commandPrefix = this.shortUid.seq();
146+
let initializationOptions = { commandPrefix, experimentalFeatures };
147+
if (rootModulePaths.length > 0) {
148+
initializationOptions = Object.assign(initializationOptions, { rootModulePaths });
149+
}
150+
if (excludeModulePaths.length > 0) {
151+
initializationOptions = Object.assign(initializationOptions, { excludeModulePaths });
152+
}
153+
154+
const executable: Executable = {
155+
command: cmd,
156+
args: serverArgs,
157+
options: {}
158+
};
159+
const serverOptions: ServerOptions = {
160+
run: executable,
161+
debug: executable
162+
};
163+
const clientOptions: LanguageClientOptions = {
164+
documentSelector: documentSelector,
165+
workspaceFolder: wsFolder,
166+
initializationOptions: initializationOptions,
167+
initializationFailedHandler: (error) => {
168+
this.reporter.sendTelemetryException(error);
169+
return false;
170+
},
171+
outputChannel: outputChannel,
172+
revealOutputChannelOn: RevealOutputChannelOn.Never,
173+
};
174+
175+
const client = new LanguageClient(
176+
id,
177+
name,
178+
serverOptions,
179+
clientOptions
180+
);
181+
182+
client.onDidChangeState((event) => {
183+
if (event.newState === State.Stopped) {
184+
clients.delete(location);
185+
this.reporter.sendTelemetryEvent('stopClient');
186+
}
187+
});
188+
189+
return {commandPrefix, client}
190+
}
191+
192+
public async stopClients(folders?: string[]): Promise<void[]> {
193+
const promises: Promise<void>[] = [];
194+
195+
if (this.supportsMultiFolders) {
196+
promises.push(this.stopClient(MULTI_FOLDER_CLIENT));
197+
return Promise.all(promises);
198+
}
199+
200+
if (!folders) {
201+
folders = [];
202+
for (const key of clients.keys()) {
203+
folders.push(key);
204+
}
205+
}
206+
207+
for (const folder of folders) {
208+
promises.push(this.stopClient(folder));
209+
}
210+
return Promise.all(promises);
211+
}
212+
213+
private async stopClient(folder: string): Promise<void> {
214+
if (!clients.has(folder)) {
215+
console.log(`Attempted to stop a client for folder: ${folder} but no client exists`);
216+
return;
217+
}
218+
219+
return clients.get(folder).client.stop().then(() => {
220+
if (folder === "") {
221+
console.log('Client stopped');
222+
return
223+
}
224+
console.log(`Client stopped for ${folder}`);
225+
}).then(() => {
226+
const ok = clients.delete(folder);
227+
if (ok) {
228+
if (folder === "") {
229+
console.log('Client deleted');
230+
return
231+
}
232+
console.log(`Client deleted for ${folder}`);
233+
}
234+
});
235+
}
236+
237+
public getClient(document?: vscode.Uri): TerraformLanguageClient {
238+
if (this.supportsMultiFolders) {
239+
return clients.get(MULTI_FOLDER_CLIENT);
240+
}
241+
242+
return clients.get(this.clientName(document.toString()));
243+
}
244+
245+
private clientName(folderName: string, workspaceFolders: readonly string[] = sortedWorkspaceFolders()): string {
246+
folderName = normalizeFolderName(folderName);
247+
const outerFolder = workspaceFolders.find(element => folderName.startsWith(element));
248+
// If this folder isn't nested, the found item will be itself
249+
if (outerFolder && (outerFolder !== folderName)) {
250+
folderName = getFolderName(getWorkspaceFolder(outerFolder));
251+
}
252+
return folderName;
253+
}
254+
}

0 commit comments

Comments
 (0)