Skip to content

Commit bd1b746

Browse files
author
Mikhail Arkhipov
authored
Propagate files exclusion to the language server (#1977)
* LS symbol providers * Provide exclude files option to LS * Document Jedi flag * Casing * Add more exclusions * File exclusion test * PR feedback * Comment * Keep container around * Test
1 parent f2c98fd commit bd1b746

File tree

9 files changed

+183
-42
lines changed

9 files changed

+183
-42
lines changed

CONTRIBUTING - PYTHON_ANALYSIS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Visual Studio 2017:
3333
4. Delete contents of the *analysis* folder in the Python Extension folder
3434
5. Copy *.dll, *.pdb, *.json fron *Python/BuildOutput/VsCode/raw* to *analysis*
3535
6. In VS Code set setting *python.downloadCodeAnalysis* to *false*
36+
7. In VS Code set setting *python.jediEnabled* to *false*
3637

3738
### Debugging code in Python Extension to VS Code
3839
Folow regular TypeScript debugging steps

src/client/activation/analysis.ts

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33

44
import { inject, injectable } from 'inversify';
55
import * as path from 'path';
6-
import { ExtensionContext, OutputChannel } from 'vscode';
6+
import { OutputChannel, Uri } from 'vscode';
77
import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient';
8-
import { IApplicationShell, ICommandManager } from '../common/application/types';
8+
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types';
99
import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../common/constants';
1010
import { createDeferred, Deferred } from '../common/helpers';
1111
import { IFileSystem, IPlatformService } from '../common/platform/types';
1212
import { StopWatch } from '../common/stopWatch';
1313
import { IConfigurationService, IExtensionContext, IOutputChannel } from '../common/types';
14-
import { IEnvironmentVariablesProvider } from '../common/variables/types';
1514
import { IInterpreterService } from '../interpreter/contracts';
1615
import { IServiceContainer } from '../ioc/types';
1716
import {
@@ -21,7 +20,7 @@ import {
2120
} from '../telemetry/constants';
2221
import { getTelemetryReporter } from '../telemetry/telemetry';
2322
import { AnalysisEngineDownloader } from './downloader';
24-
import { InterpreterDataService } from './interpreterDataService';
23+
import { InterpreterData, InterpreterDataService } from './interpreterDataService';
2524
import { PlatformData } from './platformData';
2625
import { IExtensionActivator } from './types';
2726

@@ -42,10 +41,13 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
4241
private readonly interpreterService: IInterpreterService;
4342
private readonly startupCompleted: Deferred<void>;
4443
private readonly disposables: Disposable[] = [];
44+
private readonly context: IExtensionContext;
45+
private readonly workspace: IWorkspaceService;
46+
private readonly root: Uri | undefined;
4547

4648
private languageClient: LanguageClient | undefined;
47-
private readonly context: ExtensionContext;
4849
private interpreterHash: string = '';
50+
private loadExtensionArgs: {} | undefined;
4951

5052
constructor(@inject(IServiceContainer) private readonly services: IServiceContainer) {
5153
this.context = this.services.get<IExtensionContext>(IExtensionContext);
@@ -55,32 +57,41 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
5557
this.fs = this.services.get<IFileSystem>(IFileSystem);
5658
this.platformData = new PlatformData(services.get<IPlatformService>(IPlatformService), this.fs);
5759
this.interpreterService = this.services.get<IInterpreterService>(IInterpreterService);
60+
this.workspace = this.services.get<IWorkspaceService>(IWorkspaceService);
61+
62+
// Currently only a single root. Multi-root support is future.
63+
this.root = this.workspace && this.workspace.hasWorkspaceFolders
64+
? this.workspace.workspaceFolders![0]!.uri : undefined;
5865

5966
this.startupCompleted = createDeferred<void>();
6067
const commandManager = this.services.get<ICommandManager>(ICommandManager);
68+
6169
this.disposables.push(commandManager.registerCommand(loadExtensionCommand,
6270
async (args) => {
6371
if (this.languageClient) {
6472
await this.startupCompleted.promise;
6573
this.languageClient.sendRequest('python/loadExtension', args);
74+
} else {
75+
this.loadExtensionArgs = args;
6676
}
6777
}
6878
));
6979
}
7080

7181
public async activate(): Promise<boolean> {
7282
this.sw.reset();
73-
const clientOptions = await this.getAnalysisOptions(this.context);
83+
const clientOptions = await this.getAnalysisOptions();
7484
if (!clientOptions) {
7585
return false;
7686
}
7787
this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.restartLanguageServer()));
78-
return this.startLanguageServer(this.context, clientOptions);
88+
return this.startLanguageServer(clientOptions);
7989
}
8090

8191
public async deactivate(): Promise<void> {
8292
if (this.languageClient) {
83-
await this.languageClient.stop();
93+
// Do not await on this
94+
this.languageClient.stop();
8495
}
8596
for (const d of this.disposables) {
8697
d.dispose();
@@ -100,7 +111,7 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
100111
}
101112
}
102113

103-
private async startLanguageServer(context: ExtensionContext, clientOptions: LanguageClientOptions): Promise<boolean> {
114+
private async startLanguageServer(clientOptions: LanguageClientOptions): Promise<boolean> {
104115
// Determine if we are running MSIL/Universal via dotnet or self-contained app.
105116

106117
const reporter = getTelemetryReporter();
@@ -109,22 +120,22 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
109120
const settings = this.configuration.getSettings();
110121
if (!settings.downloadCodeAnalysis) {
111122
// Depends on .NET Runtime or SDK. Typically development-only case.
112-
this.languageClient = this.createSimpleLanguageClient(context, clientOptions);
113-
await this.startLanguageClient(context);
123+
this.languageClient = this.createSimpleLanguageClient(clientOptions);
124+
await this.startLanguageClient();
114125
return true;
115126
}
116127

117-
const mscorlib = path.join(context.extensionPath, analysisEngineFolder, 'mscorlib.dll');
128+
const mscorlib = path.join(this.context.extensionPath, analysisEngineFolder, 'mscorlib.dll');
118129
if (!await this.fs.fileExists(mscorlib)) {
119130
const downloader = new AnalysisEngineDownloader(this.services, analysisEngineFolder);
120-
await downloader.downloadAnalysisEngine(context);
131+
await downloader.downloadAnalysisEngine(this.context);
121132
reporter.sendTelemetryEvent(PYTHON_ANALYSIS_ENGINE_DOWNLOADED);
122133
}
123134

124-
const serverModule = path.join(context.extensionPath, analysisEngineFolder, this.platformData.getEngineExecutableName());
125-
this.languageClient = this.createSelfContainedLanguageClient(context, serverModule, clientOptions);
135+
const serverModule = path.join(this.context.extensionPath, analysisEngineFolder, this.platformData.getEngineExecutableName());
136+
this.languageClient = this.createSelfContainedLanguageClient(serverModule, clientOptions);
126137
try {
127-
await this.startLanguageClient(context);
138+
await this.startLanguageClient();
128139
return true;
129140
} catch (ex) {
130141
this.appShell.showErrorMessage(`Language server failed to start. Error ${ex}`);
@@ -133,30 +144,34 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
133144
}
134145
}
135146

136-
private async startLanguageClient(context: ExtensionContext): Promise<void> {
147+
private async startLanguageClient(): Promise<void> {
137148
this.languageClient!.onReady()
138149
.then(() => {
139150
this.startupCompleted.resolve();
151+
if (this.loadExtensionArgs) {
152+
this.languageClient!.sendRequest('python/loadExtension', this.loadExtensionArgs);
153+
this.loadExtensionArgs = undefined;
154+
}
140155
})
141156
.catch(error => this.startupCompleted.reject(error));
142157

143-
context.subscriptions.push(this.languageClient!.start());
158+
this.context.subscriptions.push(this.languageClient!.start());
144159
if (isTestExecution()) {
145160
await this.startupCompleted.promise;
146161
}
147162
}
148163

149-
private createSimpleLanguageClient(context: ExtensionContext, clientOptions: LanguageClientOptions): LanguageClient {
164+
private createSimpleLanguageClient(clientOptions: LanguageClientOptions): LanguageClient {
150165
const commandOptions = { stdio: 'pipe' };
151-
const serverModule = path.join(context.extensionPath, analysisEngineFolder, this.platformData.getEngineDllName());
166+
const serverModule = path.join(this.context.extensionPath, analysisEngineFolder, this.platformData.getEngineDllName());
152167
const serverOptions: ServerOptions = {
153168
run: { command: dotNetCommand, args: [serverModule], options: commandOptions },
154169
debug: { command: dotNetCommand, args: [serverModule, '--debug'], options: commandOptions }
155170
};
156171
return new LanguageClient(PYTHON, languageClientName, serverOptions, clientOptions);
157172
}
158173

159-
private createSelfContainedLanguageClient(context: ExtensionContext, serverModule: string, clientOptions: LanguageClientOptions): LanguageClient {
174+
private createSelfContainedLanguageClient(serverModule: string, clientOptions: LanguageClientOptions): LanguageClient {
160175
const options = { stdio: 'pipe' };
161176
const serverOptions: ServerOptions = {
162177
run: { command: serverModule, rgs: [], options: options },
@@ -165,19 +180,22 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
165180
return new LanguageClient(PYTHON, languageClientName, serverOptions, clientOptions);
166181
}
167182

168-
private async getAnalysisOptions(context: ExtensionContext): Promise<LanguageClientOptions | undefined> {
183+
private async getAnalysisOptions(): Promise<LanguageClientOptions | undefined> {
169184
// tslint:disable-next-line:no-any
170185
const properties = new Map<string, any>();
186+
let interpreterData: InterpreterData | undefined;
187+
let pythonPath = '';
171188

172-
// Microsoft Python code analysis engine needs full path to the interpreter
173-
const interpreterDataService = new InterpreterDataService(context, this.services);
174-
const interpreterData = await interpreterDataService.getInterpreterData();
175-
if (!interpreterData) {
176-
const appShell = this.services.get<IApplicationShell>(IApplicationShell);
177-
appShell.showWarningMessage('Unable to determine path to Python interpreter. IntelliSense will be limited.');
189+
try {
190+
const interpreterDataService = new InterpreterDataService(this.context, this.services);
191+
interpreterData = await interpreterDataService.getInterpreterData();
192+
} catch (ex) {
193+
this.appShell.showErrorMessage('Unable to determine path to the Python interpreter. IntelliSense will be limited.');
178194
}
179195

196+
this.interpreterHash = interpreterData ? interpreterData.hash : '';
180197
if (interpreterData) {
198+
pythonPath = path.dirname(interpreterData.path);
181199
// tslint:disable-next-line:no-string-literal
182200
properties['InterpreterPath'] = interpreterData.path;
183201
// tslint:disable-next-line:no-string-literal
@@ -196,25 +214,17 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
196214
}
197215

198216
// tslint:disable-next-line:no-string-literal
199-
properties['DatabasePath'] = path.join(context.extensionPath, analysisEngineFolder);
200-
201-
const envProvider = this.services.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider);
202-
let pythonPath = (await envProvider.getEnvironmentVariables()).PYTHONPATH;
203-
this.interpreterHash = interpreterData ? interpreterData.hash : '';
217+
properties['DatabasePath'] = path.join(this.context.extensionPath, analysisEngineFolder);
204218

205219
// Make sure paths do not contain multiple slashes so file URIs
206220
// in VS Code (Node.js) and in the language server (.NET) match.
207221
// Note: for the language server paths separator is always ;
208222
searchPaths = searchPaths.split(path.delimiter).map(p => path.normalize(p)).join(';');
209-
pythonPath = pythonPath ? path.normalize(pythonPath) : '';
210-
211223
// tslint:disable-next-line:no-string-literal
212224
properties['SearchPaths'] = `${searchPaths};${pythonPath}`;
213-
const selector: string[] = [PYTHON];
214225

215-
// const searchExcludes = workspace.getConfiguration('search').get('exclude', null);
216-
// const filesExcludes = workspace.getConfiguration('files').get('exclude', null);
217-
// const watcherExcludes = workspace.getConfiguration('files').get('watcherExclude', null);
226+
const selector = [{ language: PYTHON, scheme: 'file' }];
227+
const excludeFiles = this.getExcludedFiles();
218228

219229
// Options to control the language client
220230
return {
@@ -236,8 +246,38 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
236246
maxDocumentationTextLength: 0
237247
},
238248
asyncStartup: true,
249+
excludeFiles: excludeFiles,
239250
testEnvironment: isTestExecution()
240251
}
241252
};
242253
}
254+
255+
private getExcludedFiles(): string[] {
256+
const list: string[] = ['**/Lib/**', '**/site-packages/**'];
257+
this.getVsCodeExcludeSection('search.exclude', list);
258+
this.getVsCodeExcludeSection('files.exclude', list);
259+
this.getVsCodeExcludeSection('files.watcherExclude', list);
260+
this.getPythonExcludeSection('linting.ignorePatterns', list);
261+
this.getPythonExcludeSection('workspaceSymbols.exclusionPattern', list);
262+
return list;
263+
}
264+
265+
private getVsCodeExcludeSection(setting: string, list: string[]): void {
266+
const states = this.workspace.getConfiguration(setting, this.root);
267+
if (states) {
268+
Object.keys(states)
269+
.filter(k => (k.indexOf('*') >= 0 || k.indexOf('/') >= 0) && states[k])
270+
.forEach(p => list.push(p));
271+
}
272+
}
273+
274+
private getPythonExcludeSection(setting: string, list: string[]): void {
275+
const pythonSettings = this.configuration.getSettings(this.root);
276+
const paths = pythonSettings && pythonSettings.linting ? pythonSettings.linting.ignorePatterns : undefined;
277+
if (paths && Array.isArray(paths)) {
278+
paths
279+
.filter(p => p && p.length > 0)
280+
.forEach(p => list.push(p));
281+
}
282+
}
243283
}

src/client/activation/downloader.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import * as fileSystem from 'fs';
55
import * as path from 'path';
66
import * as request from 'request';
77
import * as requestProgress from 'request-progress';
8-
import { ExtensionContext, OutputChannel, ProgressLocation, window } from 'vscode';
8+
import { OutputChannel, ProgressLocation, window } from 'vscode';
99
import { STANDARD_OUTPUT_CHANNEL } from '../common/constants';
1010
import { createDeferred, createTemporaryFile } from '../common/helpers';
1111
import { IFileSystem, IPlatformService } from '../common/platform/types';
12-
import { IOutputChannel } from '../common/types';
12+
import { IExtensionContext, IOutputChannel } from '../common/types';
1313
import { IServiceContainer } from '../ioc/types';
1414
import { HashVerifier } from './hashVerifier';
1515
import { PlatformData } from './platformData';
@@ -35,7 +35,7 @@ export class AnalysisEngineDownloader {
3535
this.platformData = new PlatformData(this.platform, this.fs);
3636
}
3737

38-
public async downloadAnalysisEngine(context: ExtensionContext): Promise<void> {
38+
public async downloadAnalysisEngine(context: IExtensionContext): Promise<void> {
3939
const platformString = await this.platformData.getPlatformName();
4040
const enginePackageFileName = `${downloadBaseFileName}-${platformString}.${downloadVersion}${downloadFileExtension}`;
4141

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from 'assert';
5+
import { Container } from 'inversify';
6+
import * as path from 'path';
7+
import { commands, ConfigurationTarget, languages, Position, TextDocument, window, workspace } from 'vscode';
8+
import { ConfigurationService } from '../../client/common/configuration/service';
9+
import '../../client/common/extensions';
10+
import { IConfigurationService } from '../../client/common/types';
11+
import { activated } from '../../client/extension';
12+
import { ServiceContainer } from '../../client/ioc/container';
13+
import { ServiceManager } from '../../client/ioc/serviceManager';
14+
import { IServiceContainer, IServiceManager } from '../../client/ioc/types';
15+
import { IsAnalysisEngineTest } from '../constants';
16+
import { closeActiveWindows } from '../initialize';
17+
18+
const wksPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'exclusions');
19+
const fileOne = path.join(wksPath, 'one.py');
20+
21+
// tslint:disable-next-line:max-func-body-length
22+
suite('Exclude files (Analysis Engine)', () => {
23+
let textDocument: TextDocument;
24+
let serviceManager: IServiceManager;
25+
let serviceContainer: IServiceContainer;
26+
let configService: IConfigurationService;
27+
28+
suiteSetup(async function () {
29+
if (!IsAnalysisEngineTest()) {
30+
// tslint:disable-next-line:no-invalid-this
31+
this.skip();
32+
}
33+
});
34+
setup(async () => {
35+
const cont = new Container();
36+
serviceContainer = new ServiceContainer(cont);
37+
serviceManager = new ServiceManager(cont);
38+
39+
serviceManager.addSingleton<IConfigurationService>(IConfigurationService, ConfigurationService);
40+
configService = serviceManager.get<IConfigurationService>(IConfigurationService);
41+
});
42+
suiteTeardown(closeActiveWindows);
43+
teardown(closeActiveWindows);
44+
45+
async function openFile(file: string): Promise<void> {
46+
textDocument = await workspace.openTextDocument(file);
47+
await activated;
48+
await window.showTextDocument(textDocument);
49+
// Make sure LS completes file loading and analysis.
50+
// In test mode it awaits for the completion before trying
51+
// to fetch data for completion, hover.etc.
52+
await commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, new Position(0, 0));
53+
}
54+
55+
async function setSetting(name: string, value: {} | undefined): Promise<void> {
56+
await configService.updateSettingAsync(name, value, undefined, ConfigurationTarget.Global);
57+
}
58+
59+
test('Default exclusions', async () => {
60+
await openFile(fileOne);
61+
const diag = languages.getDiagnostics();
62+
63+
const main = diag.filter(d => d[0].fsPath.indexOf('one.py') >= 0);
64+
assert.equal(main.length > 0, true);
65+
66+
const subdir = diag.filter(d => d[0].fsPath.indexOf('three.py') >= 0);
67+
assert.equal(subdir.length > 0, true);
68+
69+
const node_modules = diag.filter(d => d[0].fsPath.indexOf('node.py') >= 0);
70+
assert.equal(node_modules.length, 0);
71+
72+
const lib = diag.filter(d => d[0].fsPath.indexOf('fileLib.py') >= 0);
73+
assert.equal(lib.length, 0);
74+
75+
const sitePackages = diag.filter(d => d[0].fsPath.indexOf('sitePackages.py') >= 0);
76+
assert.equal(sitePackages.length, 0);
77+
});
78+
test('Exclude subfolder', async () => {
79+
await setSetting('linting.ignorePatterns', ['**/dir1/**']);
80+
81+
await openFile(fileOne);
82+
const diag = languages.getDiagnostics();
83+
84+
const main = diag.filter(d => d[0].fsPath.indexOf('one.py') >= 0);
85+
assert.equal(main.length > 0, true);
86+
87+
const subdir1 = diag.filter(d => d[0].fsPath.indexOf('dir1file.py') >= 0);
88+
assert.equal(subdir1.length, 0);
89+
90+
const subdir2 = diag.filter(d => d[0].fsPath.indexOf('dir2file.py') >= 0);
91+
assert.equal(subdir2.length, 0);
92+
93+
await setSetting('linting.ignorePatterns', undefined);
94+
});
95+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
b
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
for

0 commit comments

Comments
 (0)