Skip to content

Commit e6c9472

Browse files
committed
use new env variables service in jedi service
1 parent b65d7c0 commit e6c9472

File tree

6 files changed

+203
-61
lines changed

6 files changed

+203
-61
lines changed

src/client/common/decorators.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as _ from 'lodash';
2+
3+
type AsyncVoidAction = (...params: {}[]) => Promise<void>;
4+
type VoidAction = (...params: {}[]) => void;
5+
6+
/**
7+
* Debounces a function execution. Function must return either a void or a promise that resolves to a void.
8+
* @export
9+
* @param {number} [wait] Wait time.
10+
* @returns void
11+
*/
12+
export function debounce(wait?: number) {
13+
// tslint:disable-next-line:no-any no-function-expression
14+
return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor<VoidAction> | TypedPropertyDescriptor<AsyncVoidAction>) {
15+
const originalMethod = descriptor.value!;
16+
// tslint:disable-next-line:no-invalid-this no-any
17+
(descriptor as any).value = _.debounce(function () { return originalMethod.apply(this, arguments); }, wait);
18+
};
19+
}
20+
21+
/**
22+
* Swallows exceptions thrown by a function. Function must return either a void or a promise that resolves to a void.
23+
* @export
24+
* @param {string} [scopeName] Scope for the error message to be logged along with the error.
25+
* @returns void
26+
*/
27+
export function swallowExceptions(scopeName: string) {
28+
// tslint:disable-next-line:no-any no-function-expression
29+
return function (_target: any, propertyName: string, descriptor: TypedPropertyDescriptor<VoidAction> | TypedPropertyDescriptor<AsyncVoidAction>) {
30+
const originalMethod = descriptor.value!;
31+
const errorMessage = `Python Extension (Error in ${scopeName}, method:${propertyName}):`;
32+
// tslint:disable-next-line:no-any no-function-expression
33+
descriptor.value = function (...args: any[]) {
34+
try {
35+
// tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any
36+
const result = originalMethod.apply(this, args);
37+
38+
// If method being wrapped returns a promise then wait and swallow errors.
39+
if (result && typeof result.then === 'function' && typeof result.catch === 'function') {
40+
return (result as Promise<void>).catch(error => console.error(errorMessage, error));
41+
}
42+
} catch (error) {
43+
console.error(errorMessage, error);
44+
}
45+
};
46+
};
47+
}

src/client/common/variables/environmentVariablesProvider.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
import { inject, injectable } from 'inversify';
5-
import { Disposable, FileSystemWatcher, Uri, workspace } from 'vscode';
5+
import { Disposable, Event, EventEmitter, FileSystemWatcher, Uri, workspace } from 'vscode';
66
import { PythonSettings } from '../configSettings';
77
import { NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from '../platform/constants';
88
import { ICurrentProcess, IDisposableRegistry, IsWindows } from '../types';
@@ -13,22 +13,29 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid
1313
private cache = new Map<string, EnvironmentVariables>();
1414
private fileWatchers = new Map<string, FileSystemWatcher>();
1515
private disposables: Disposable[] = [];
16-
16+
private changeEventEmitter: EventEmitter<Uri | undefined>;
1717
constructor( @inject(IEnvironmentVariablesService) private envVarsService: IEnvironmentVariablesService,
1818
@inject(IDisposableRegistry) disposableRegistry: Disposable[], @inject(IsWindows) private isWidows: boolean,
1919
@inject(ICurrentProcess) private process: ICurrentProcess) {
2020
disposableRegistry.push(this);
21+
this.changeEventEmitter = new EventEmitter();
22+
}
23+
24+
public get onDidEnvironmentVariablesChange(): Event<Uri | undefined> {
25+
return this.changeEventEmitter.event;
2126
}
2227

2328
public dispose() {
29+
this.changeEventEmitter.dispose();
2430
this.fileWatchers.forEach(watcher => {
2531
watcher.dispose();
2632
});
2733
}
2834
public async getEnvironmentVariables(resource?: Uri): Promise<EnvironmentVariables> {
2935
const settings = PythonSettings.getInstance(resource);
3036
if (!this.cache.has(settings.envFile)) {
31-
this.createFileWatcher(settings.envFile);
37+
const workspaceFolderUri = this.getWorkspaceFolderUri(resource);
38+
this.createFileWatcher(settings.envFile, workspaceFolderUri);
3239
let mergedVars = await this.envVarsService.parseFile(settings.envFile);
3340
if (!mergedVars) {
3441
mergedVars = {};
@@ -41,14 +48,25 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid
4148
}
4249
return this.cache.get(settings.envFile)!;
4350
}
44-
private createFileWatcher(envFile: string) {
51+
private getWorkspaceFolderUri(resource?: Uri): Uri | undefined {
52+
if (!resource) {
53+
return;
54+
}
55+
const workspaceFolder = workspace.getWorkspaceFolder(resource!);
56+
return workspaceFolder ? workspaceFolder.uri : undefined;
57+
}
58+
private createFileWatcher(envFile: string, workspaceFolderUri?: Uri) {
4559
if (this.fileWatchers.has(envFile)) {
4660
return;
4761
}
4862
const envFileWatcher = workspace.createFileSystemWatcher(envFile);
4963
this.fileWatchers.set(envFile, envFileWatcher);
50-
this.disposables.push(envFileWatcher.onDidChange(() => this.cache.delete(envFile)));
51-
this.disposables.push(envFileWatcher.onDidCreate(() => this.cache.delete(envFile)));
52-
this.disposables.push(envFileWatcher.onDidDelete(() => this.cache.delete(envFile)));
64+
this.disposables.push(envFileWatcher.onDidChange(() => this.onEnvironmentFileChanged(envFile, workspaceFolderUri)));
65+
this.disposables.push(envFileWatcher.onDidCreate(() => this.onEnvironmentFileChanged(envFile, workspaceFolderUri)));
66+
this.disposables.push(envFileWatcher.onDidDelete(() => this.onEnvironmentFileChanged(envFile, workspaceFolderUri)));
67+
}
68+
private onEnvironmentFileChanged(envFile, workspaceFolderUri?: Uri) {
69+
this.cache.delete(envFile);
70+
this.changeEventEmitter.fire(workspaceFolderUri);
5371
}
5472
}

src/client/common/variables/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { Uri } from 'vscode';
4+
import { Event, Uri } from 'vscode';
55

66
export type EnvironmentVariables = Object & {
77
[key: string]: string;
@@ -38,5 +38,6 @@ export interface ISystemVariables {
3838
export const IEnvironmentVariablesProvider = Symbol('IEnvironmentVariablesProvider');
3939

4040
export interface IEnvironmentVariablesProvider {
41+
onDidEnvironmentVariablesChange: Event<Uri | undefined>;
4142
getEnvironmentVariables(resource?: Uri): Promise<EnvironmentVariables | undefined>;
4243
}

src/client/providers/jediProxy.ts

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as path from 'path';
77
import * as vscode from 'vscode';
88
import { Uri } from 'vscode';
99
import { PythonSettings } from '../common/configSettings';
10+
import { debounce, swallowExceptions } from '../common/decorators';
1011
import '../common/extensions';
1112
import { createDeferred, Deferred } from '../common/helpers';
1213
import { IPythonExecutionFactory } from '../common/process/types';
@@ -129,25 +130,24 @@ export class JediProxy implements vscode.Disposable {
129130
private proc: child_process.ChildProcess | null;
130131
private pythonSettings: PythonSettings;
131132
private cmdId: number = 0;
132-
private pythonProcessCWD = '';
133133
private lastKnownPythonInterpreter: string;
134134
private previousData = '';
135135
private commands = new Map<number, IExecutionCommand<ICommandResult>>();
136136
private commandQueue: number[] = [];
137137
private spawnRetryAttempts = 0;
138-
private lastKnownPythonPath: string;
139-
private additionalAutoCopletePaths: string[] = [];
138+
private additionalAutoCompletePaths: string[] = [];
140139
private workspacePath: string;
140+
private languageServerStarted: Deferred<void>;
141141
private initialized: Deferred<void>;
142-
143-
public constructor(extensionRootDir: string, workspacePath: string, private serviceContainer: IServiceContainer) {
142+
private environmentVariablesProvider: IEnvironmentVariablesProvider;
143+
public constructor(private extensionRootDir: string, workspacePath: string, private serviceContainer: IServiceContainer) {
144144
this.workspacePath = workspacePath;
145145
this.pythonSettings = PythonSettings.getInstance(vscode.Uri.file(workspacePath));
146146
this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath;
147-
this.pythonSettings.on('change', this.onPythonSettingsChanged.bind(this));
148-
vscode.workspace.onDidChangeConfiguration(this.onConfigChanged.bind(this));
149-
this.onConfigChanged();
150-
this.initialize(extensionRootDir);
147+
this.pythonSettings.on('change', () => this.pythonSettingsChangeHandler());
148+
this.initialized = createDeferred<void>();
149+
// tslint:disable-next-line:no-empty
150+
this.startLanguageServer().catch(() => { }).then(() => this.initialized.resolve());
151151
}
152152

153153
private static getProperty<T>(o: object, name: string): T {
@@ -166,15 +166,13 @@ export class JediProxy implements vscode.Disposable {
166166

167167
public async sendCommand<T extends ICommandResult>(cmd: ICommand<T>): Promise<T> {
168168
await this.initialized.promise;
169+
await this.languageServerStarted.promise;
169170
if (!this.proc) {
170171
return Promise.reject(new Error('Python proc not initialized'));
171172
}
172173
const executionCmd = <IExecutionCommand<T>>cmd;
173174
const payload = this.createPayload(executionCmd);
174175
executionCmd.deferred = createDeferred<T>();
175-
// if (typeof executionCmd.telemetryEvent === 'string') {
176-
// executionCmd.delays = new telemetryHelper.Delays();
177-
// }
178176
try {
179177
this.proc.stdin.write(`${JSON.stringify(payload)}\n`);
180178
this.commands.set(executionCmd.id, executionCmd);
@@ -193,25 +191,43 @@ export class JediProxy implements vscode.Disposable {
193191
}
194192

195193
// keep track of the directory so we can re-spawn the process.
196-
private initialize(dir: string) {
197-
this.pythonProcessCWD = dir;
198-
this.spawnProcess(path.join(dir, 'pythonFiles'))
194+
private initialize() {
195+
this.spawnProcess(path.join(this.extensionRootDir, 'pythonFiles'))
199196
.catch(ex => {
200-
if (this.initialized) {
201-
this.initialized.reject(ex);
197+
if (this.languageServerStarted) {
198+
this.languageServerStarted.reject(ex);
202199
}
203200
this.handleError('spawnProcess', ex);
204201
});
205202
}
206-
207-
// Check if settings changes.
208-
private onPythonSettingsChanged() {
203+
@swallowExceptions('JediProxy')
204+
private async pythonSettingsChangeHandler() {
209205
if (this.lastKnownPythonInterpreter === this.pythonSettings.pythonPath) {
210206
return;
211207
}
208+
this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath;
209+
this.additionalAutoCompletePaths = await this.buildAutoCompletePaths();
210+
this.restartLanguageServer();
211+
}
212+
@debounce(1500)
213+
@swallowExceptions('JediProxy')
214+
private async environmentVariablesChangeHandler() {
215+
const newAutoComletePaths = await this.buildAutoCompletePaths();
216+
if (this.additionalAutoCompletePaths.join(',') !== newAutoComletePaths.join(',')) {
217+
this.additionalAutoCompletePaths = newAutoComletePaths;
218+
this.restartLanguageServer();
219+
}
220+
}
221+
@swallowExceptions('JediProxy')
222+
private async startLanguageServer() {
223+
const newAutoComletePaths = await this.buildAutoCompletePaths();
224+
this.additionalAutoCompletePaths = newAutoComletePaths;
225+
this.restartLanguageServer();
226+
}
227+
private restartLanguageServer() {
212228
this.killProcess();
213229
this.clearPendingRequests();
214-
this.initialize(this.pythonProcessCWD);
230+
this.initialize();
215231
}
216232

217233
private clearPendingRequests() {
@@ -240,7 +256,10 @@ export class JediProxy implements vscode.Disposable {
240256

241257
// tslint:disable-next-line:max-func-body-length
242258
private async spawnProcess(cwd: string) {
243-
this.initialized = createDeferred<void>();
259+
if (this.languageServerStarted && !this.languageServerStarted.completed) {
260+
this.languageServerStarted.reject();
261+
}
262+
this.languageServerStarted = createDeferred<void>();
244263
const pythonProcess = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(Uri.file(this.workspacePath));
245264
const args = ['completion.py'];
246265
if (typeof this.pythonSettings.jediPath !== 'string' || this.pythonSettings.jediPath.length === 0) {
@@ -263,7 +282,7 @@ export class JediProxy implements vscode.Disposable {
263282
}
264283
const result = pythonProcess.execObservable(args, { cwd });
265284
this.proc = result.proc;
266-
this.initialized.resolve();
285+
this.languageServerStarted.resolve();
267286
this.proc.on('end', (end) => {
268287
logger.error('spawnProcess.end', `End - ${end}`);
269288
});
@@ -274,8 +293,8 @@ export class JediProxy implements vscode.Disposable {
274293
error.message.indexOf('This socket has been ended by the other party') >= 0) {
275294
this.spawnProcess(cwd)
276295
.catch(ex => {
277-
if (this.initialized) {
278-
this.initialized.reject(ex);
296+
if (this.languageServerStarted) {
297+
this.languageServerStarted.reject(ex);
279298
}
280299
this.handleError('spawnProcess', ex);
281300
});
@@ -533,14 +552,7 @@ export class JediProxy implements vscode.Disposable {
533552
return '';
534553
}
535554
}
536-
537-
private async onConfigChanged() {
538-
// We're only interested in changes to the python path.
539-
if (this.lastKnownPythonPath === this.pythonSettings.pythonPath) {
540-
return;
541-
}
542-
543-
this.lastKnownPythonPath = this.pythonSettings.pythonPath;
555+
private async buildAutoCompletePaths(): Promise<string[]> {
544556
const filePathPromises = [
545557
// Sysprefix.
546558
this.getPathFromPythonCommand(['-c', 'import sys;print(sys.prefix)']).catch(() => ''),
@@ -561,19 +573,27 @@ export class JediProxy implements vscode.Disposable {
561573
];
562574

563575
try {
564-
const environmentVariablesProvider = this.serviceContainer.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider);
565-
const pythonPaths = await environmentVariablesProvider.getEnvironmentVariables(Uri.file(this.workspacePath))
576+
const pythonPaths = await this.getEnvironmentVariablesProvider().getEnvironmentVariables(Uri.file(this.workspacePath))
566577
.then(customEnvironmentVars => customEnvironmentVars ? JediProxy.getProperty<string>(customEnvironmentVars, 'PYTHONPATH') : '')
567578
.then(pythonPath => (typeof pythonPath === 'string' && pythonPath.trim().length > 0) ? pythonPath.trim() : '')
568579
.then(pythonPath => pythonPath.split(path.delimiter).filter(item => item.trim().length > 0));
569-
580+
const resolvedPaths = pythonPaths
581+
.filter(pythonPath => !path.isAbsolute(pythonPath))
582+
.map(pythonPath => path.resolve(this.workspacePath, pythonPath));
570583
const filePaths = await Promise.all(filePathPromises);
571-
this.additionalAutoCopletePaths = filePaths.concat(pythonPaths).filter(p => p.length > 0);
584+
return filePaths.concat(...pythonPaths, ...resolvedPaths).filter(p => p.length > 0);
572585
} catch (ex) {
573586
console.error('Python Extension: jediProxy.filePaths', ex);
587+
return [];
574588
}
575589
}
576-
590+
private getEnvironmentVariablesProvider() {
591+
if (!this.environmentVariablesProvider) {
592+
this.environmentVariablesProvider = this.serviceContainer.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider);
593+
this.environmentVariablesProvider.onDidEnvironmentVariablesChange(this.environmentVariablesChangeHandler.bind(this));
594+
}
595+
return this.environmentVariablesProvider;
596+
}
577597
private getConfig() {
578598
// Add support for paths relative to workspace.
579599
const extraPaths = this.pythonSettings.autoComplete.extraPaths.map(extraPath => {
@@ -591,7 +611,7 @@ export class JediProxy implements vscode.Disposable {
591611
extraPaths.unshift(this.workspacePath);
592612
}
593613

594-
const distinctExtraPaths = extraPaths.concat(this.additionalAutoCopletePaths)
614+
const distinctExtraPaths = extraPaths.concat(this.additionalAutoCompletePaths)
595615
.filter(value => value.length > 0)
596616
.filter((value, index, self) => self.indexOf(value) === index);
597617

src/client/workspaceSymbols/generator.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,19 @@ export class Generator implements vscode.Disposable {
2525
public dispose() {
2626
this.disposables.forEach(d => d.dispose());
2727
}
28-
28+
public async generateWorkspaceTags(): Promise<void> {
29+
if (!this.pythonSettings.workspaceSymbols.enabled) {
30+
return;
31+
}
32+
return await this.generateTags({ directory: this.workspaceFolder.fsPath });
33+
}
2934
private buildCmdArgs(): string[] {
3035
const optionsFile = this.optionsFile.indexOf(' ') > 0 ? `"${this.optionsFile}"` : this.optionsFile;
3136
const exclusions = this.pythonSettings.workspaceSymbols.exclusionPatterns;
3237
const excludes = exclusions.length === 0 ? [] : exclusions.map(pattern => `--exclude=${pattern}`);
3338

3439
return [`--options=${optionsFile}`, '--languages=Python'].concat(excludes);
3540
}
36-
37-
public async generateWorkspaceTags(): Promise<void> {
38-
if (!this.pythonSettings.workspaceSymbols.enabled) {
39-
return;
40-
}
41-
return await this.generateTags({ directory: this.workspaceFolder.fsPath });
42-
}
4341
@captureTelemetry(WORKSPACE_SYMBOLS_BUILD)
4442
private generateTags(source: { directory?: string, file?: string }): Promise<void> {
4543
const tagFile = path.normalize(this.pythonSettings.workspaceSymbols.tagFilePath);
@@ -60,10 +58,10 @@ export class Generator implements vscode.Disposable {
6058
}
6159
outputFile = outputFile.indexOf(' ') > 0 ? `"${outputFile}"` : outputFile;
6260
args.push(`-o ${outputFile}`, '.');
63-
this.output.appendLine('-'.repeat(10) + 'Generating Tags' + '-'.repeat(10));
61+
this.output.appendLine(`${'-'.repeat(10)}Generating Tags${'-'.repeat(10)}`);
6462
this.output.appendLine(`${cmd} ${args.join(' ')}`);
6563
const promise = new Promise<void>((resolve, reject) => {
66-
let options: child_process.SpawnOptions = {
64+
const options: child_process.SpawnOptions = {
6765
cwd: source.directory
6866
};
6967

@@ -76,6 +74,7 @@ export class Generator implements vscode.Disposable {
7674
reject(error);
7775
});
7876
proc.stderr.on('data', (data: string) => {
77+
hasErrors = true;
7978
errorMsg += data;
8079
this.output.append(data);
8180
});
@@ -85,8 +84,7 @@ export class Generator implements vscode.Disposable {
8584
proc.on('exit', () => {
8685
if (hasErrors) {
8786
reject(errorMsg);
88-
}
89-
else {
87+
} else {
9088
resolve();
9189
}
9290
});

0 commit comments

Comments
 (0)