Skip to content

Commit aa578fc

Browse files
authored
Use file system API for path comparisons when performing code navigation (microsoft#1812)
Fixes microsoft#1811
1 parent 9427d72 commit aa578fc

File tree

4 files changed

+94
-36
lines changed

4 files changed

+94
-36
lines changed

news/2 Fixes/1811.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Use file system API to perform file path comparisons when performing code navigation.
2+
(thanks to [bstaint](https://github.com/bstaint) for the initial patch)

src/client/activation/classic.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { DocumentFilter, languages } from 'vscode';
66
import { PYTHON } from '../common/constants';
77
import { IConfigurationService, IExtensionContext, ILogger } from '../common/types';
88
import { IShebangCodeLensProvider } from '../interpreter/contracts';
9-
import { IServiceManager } from '../ioc/types';
9+
import { IServiceContainer, IServiceManager } from '../ioc/types';
1010
import { JediFactory } from '../languageServices/jediProxyFactory';
1111
import { PythonCompletionItemProvider } from '../providers/completionProvider';
1212
import { PythonDefinitionProvider } from '../providers/definitionProvider';
@@ -46,7 +46,7 @@ export class ClassicExtensionActivator implements IExtensionActivator {
4646
context.subscriptions.push(languages.registerCompletionItemProvider(this.documentSelector, new PythonCompletionItemProvider(jediFactory, this.serviceManager), '.'));
4747
context.subscriptions.push(languages.registerCodeLensProvider(this.documentSelector, this.serviceManager.get<IShebangCodeLensProvider>(IShebangCodeLensProvider)));
4848

49-
const symbolProvider = new PythonSymbolProvider(jediFactory);
49+
const symbolProvider = new PythonSymbolProvider(this.serviceManager.get<IServiceContainer>(IServiceContainer), jediFactory);
5050
context.subscriptions.push(languages.registerDocumentSymbolProvider(this.documentSelector, symbolProvider));
5151

5252
const pythonSettings = this.serviceManager.get<IConfigurationService>(IConfigurationService).getSettings();

src/client/providers/symbolProvider.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,19 @@
22

33
import { CancellationToken, DocumentSymbolProvider, Location, Range, SymbolInformation, TextDocument, Uri } from 'vscode';
44
import { createDeferred, Deferred } from '../common/helpers';
5+
import { IFileSystem } from '../common/platform/types';
6+
import { IServiceContainer } from '../ioc/types';
57
import { JediFactory } from '../languageServices/jediProxyFactory';
68
import { captureTelemetry } from '../telemetry';
79
import { SYMBOL } from '../telemetry/constants';
810
import * as proxy from './jediProxy';
911

1012
export class PythonSymbolProvider implements DocumentSymbolProvider {
1113
private debounceRequest: Map<string, { timer: NodeJS.Timer; deferred: Deferred<SymbolInformation[]> }>;
12-
public constructor(private jediFactory: JediFactory, private readonly debounceTimeoutMs = 500) {
14+
private readonly fs: IFileSystem;
15+
public constructor(serviceContainer: IServiceContainer, private jediFactory: JediFactory, private readonly debounceTimeoutMs = 500) {
1316
this.debounceRequest = new Map<string, { timer: NodeJS.Timer; deferred: Deferred<SymbolInformation[]> }>();
14-
}
15-
private static parseData(document: TextDocument, data?: proxy.ISymbolResult): SymbolInformation[] {
16-
if (data) {
17-
const symbols = data.definitions.filter(sym => sym.fileName === document.fileName);
18-
return symbols.map(sym => {
19-
const symbol = sym.kind;
20-
const range = new Range(
21-
sym.range.startLine, sym.range.startColumn,
22-
sym.range.endLine, sym.range.endColumn);
23-
const uri = Uri.file(sym.fileName);
24-
const location = new Location(uri, range);
25-
return new SymbolInformation(sym.text, symbol, sym.container, location);
26-
});
27-
}
28-
return [];
17+
this.fs = serviceContainer.get<IFileSystem>(IFileSystem);
2918
}
3019
@captureTelemetry(SYMBOL)
3120
public provideDocumentSymbols(document: TextDocument, token: CancellationToken): Thenable<SymbolInformation[]> {
@@ -55,7 +44,7 @@ export class PythonSymbolProvider implements DocumentSymbolProvider {
5544
}
5645

5746
this.jediFactory.getJediProxyHandler<proxy.ISymbolResult>(document.uri).sendCommand(cmd, token)
58-
.then(data => PythonSymbolProvider.parseData(document, data))
47+
.then(data => this.parseData(document, data))
5948
.then(items => deferred.resolve(items))
6049
.catch(ex => deferred.reject(ex));
6150

@@ -89,6 +78,21 @@ export class PythonSymbolProvider implements DocumentSymbolProvider {
8978
}
9079

9180
return this.jediFactory.getJediProxyHandler<proxy.ISymbolResult>(document.uri).sendCommandNonCancellableCommand(cmd, token)
92-
.then(data => PythonSymbolProvider.parseData(document, data));
81+
.then(data => this.parseData(document, data));
82+
}
83+
private parseData(document: TextDocument, data?: proxy.ISymbolResult): SymbolInformation[] {
84+
if (data) {
85+
const symbols = data.definitions.filter(sym => this.fs.arePathsSame(sym.fileName, document.fileName));
86+
return symbols.map(sym => {
87+
const symbol = sym.kind;
88+
const range = new Range(
89+
sym.range.startLine, sym.range.startColumn,
90+
sym.range.endLine, sym.range.endColumn);
91+
const uri = Uri.file(sym.fileName);
92+
const location = new Location(uri, range);
93+
return new SymbolInformation(sym.text, symbol, sym.container, location);
94+
});
95+
}
96+
return [];
9397
}
9498
}

src/test/providers/symbolProvider.test.ts

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,39 @@
88
import { expect, use } from 'chai';
99
import * as TypeMoq from 'typemoq';
1010
import { CancellationToken, CancellationTokenSource, CompletionItemKind, DocumentSymbolProvider, SymbolKind, TextDocument, Uri } from 'vscode';
11+
import { IFileSystem } from '../../client/common/platform/types';
12+
import { IServiceContainer } from '../../client/ioc/types';
1113
import { JediFactory } from '../../client/languageServices/jediProxyFactory';
1214
import { IDefinition, ISymbolResult, JediProxyHandler } from '../../client/providers/jediProxy';
1315
import { PythonSymbolProvider } from '../../client/providers/symbolProvider';
16+
1417
const assertArrays = require('chai-arrays');
1518
use(assertArrays);
1619

1720
suite('Symbol Provider', () => {
18-
let symbolProvider: DocumentSymbolProvider;
21+
let serviceContainer: TypeMoq.IMock<IServiceContainer>;
1922
let jediHandler: TypeMoq.IMock<JediProxyHandler<ISymbolResult>>;
2023
let jediFactory: TypeMoq.IMock<JediFactory>;
24+
let fileSystem: TypeMoq.IMock<IFileSystem>;
25+
let provider: DocumentSymbolProvider;
26+
let uri: Uri;
27+
let doc: TypeMoq.IMock<TextDocument>;
2128
setup(() => {
29+
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
2230
jediFactory = TypeMoq.Mock.ofType(JediFactory);
2331
jediHandler = TypeMoq.Mock.ofType<JediProxyHandler<ISymbolResult>>();
2432

33+
fileSystem = TypeMoq.Mock.ofType<IFileSystem>();
34+
doc = TypeMoq.Mock.ofType<TextDocument>();
2535
jediFactory.setup(j => j.getJediProxyHandler(TypeMoq.It.isAny()))
2636
.returns(() => jediHandler.object);
37+
38+
serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object);
2739
});
2840

2941
async function testDocumentation(requestId: number, fileName: string, expectedSize: number, token?: CancellationToken, isUntitled = false) {
30-
const doc = TypeMoq.Mock.ofType<TextDocument>();
42+
fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
43+
.returns(() => true);
3144
token = token ? token : new CancellationTokenSource().token;
3245
const symbolResult = TypeMoq.Mock.ofType<ISymbolResult>();
3346

@@ -39,91 +52,130 @@ suite('Symbol Provider', () => {
3952
}
4053
];
4154

55+
uri = Uri.file(fileName);
56+
doc.setup(d => d.uri).returns(() => uri);
4257
doc.setup(d => d.fileName).returns(() => fileName);
4358
doc.setup(d => d.isUntitled).returns(() => isUntitled);
44-
doc.setup(d => d.uri).returns(() => Uri.file(fileName));
4559
doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => '');
4660
symbolResult.setup(c => c.requestId).returns(() => requestId);
4761
symbolResult.setup(c => c.definitions).returns(() => definitions);
4862
symbolResult.setup((c: any) => c.then).returns(() => undefined);
4963
jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
5064
.returns(() => Promise.resolve(symbolResult.object));
5165

52-
const items = await symbolProvider.provideDocumentSymbols(doc.object, token);
66+
const items = await provider.provideDocumentSymbols(doc.object, token);
5367
expect(items).to.be.array();
5468
expect(items).to.be.ofSize(expectedSize);
5569
}
5670

5771
test('Ensure symbols are returned', async () => {
58-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 0);
72+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0);
5973
await testDocumentation(1, __filename, 1);
6074
});
6175
test('Ensure symbols are returned (for untitled documents)', async () => {
62-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 0);
76+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0);
6377
await testDocumentation(1, __filename, 1, undefined, true);
6478
});
6579
test('Ensure symbols are returned with a debounce of 100ms', async () => {
66-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 0);
80+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0);
6781
await testDocumentation(1, __filename, 1);
6882
});
6983
test('Ensure symbols are returned with a debounce of 100ms (for untitled documents)', async () => {
70-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 0);
84+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0);
7185
await testDocumentation(1, __filename, 1, undefined, true);
7286
});
7387
test('Ensure symbols are not returned when cancelled', async () => {
74-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 0);
88+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0);
7589
const tokenSource = new CancellationTokenSource();
7690
tokenSource.cancel();
7791
await testDocumentation(1, __filename, 0, tokenSource.token);
7892
});
7993
test('Ensure symbols are not returned when cancelled (for untitled documents)', async () => {
80-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 0);
94+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0);
8195
const tokenSource = new CancellationTokenSource();
8296
tokenSource.cancel();
8397
await testDocumentation(1, __filename, 0, tokenSource.token, true);
8498
});
8599
test('Ensure symbols are returned only for the last request', async () => {
86-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 100);
100+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 100);
87101
await Promise.all([
88102
testDocumentation(1, __filename, 0),
89103
testDocumentation(2, __filename, 0),
90104
testDocumentation(3, __filename, 1)
91105
]);
92106
});
93107
test('Ensure symbols are returned for all the requests when the doc is untitled', async () => {
94-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 100);
108+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 100);
95109
await Promise.all([
96110
testDocumentation(1, __filename, 1, undefined, true),
97111
testDocumentation(2, __filename, 1, undefined, true),
98112
testDocumentation(3, __filename, 1, undefined, true)
99113
]);
100114
});
101115
test('Ensure symbols are returned for multiple documents', async () => {
102-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 0);
116+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0);
103117
await Promise.all([
104118
testDocumentation(1, 'file1', 1),
105119
testDocumentation(2, 'file2', 1)
106120
]);
107121
});
108122
test('Ensure symbols are returned for multiple untitled documents ', async () => {
109-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 0);
123+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0);
110124
await Promise.all([
111125
testDocumentation(1, 'file1', 1, undefined, true),
112126
testDocumentation(2, 'file2', 1, undefined, true)
113127
]);
114128
});
115129
test('Ensure symbols are returned for multiple documents with a debounce of 100ms', async () => {
116-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 100);
130+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 100);
117131
await Promise.all([
118132
testDocumentation(1, 'file1', 1),
119133
testDocumentation(2, 'file2', 1)
120134
]);
121135
});
122136
test('Ensure symbols are returned for multiple untitled documents with a debounce of 100ms', async () => {
123-
symbolProvider = new PythonSymbolProvider(jediFactory.object, 100);
137+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 100);
124138
await Promise.all([
125139
testDocumentation(1, 'file1', 1, undefined, true),
126140
testDocumentation(2, 'file2', 1, undefined, true)
127141
]);
128142
});
143+
test('Ensure IFileSystem.arePathsSame is used', async () => {
144+
doc.setup(d => d.getText())
145+
.returns(() => '')
146+
.verifiable(TypeMoq.Times.once());
147+
doc.setup(d => d.isDirty)
148+
.returns(() => true)
149+
.verifiable(TypeMoq.Times.once());
150+
doc.setup(d => d.fileName)
151+
.returns(() => __filename);
152+
153+
const symbols = TypeMoq.Mock.ofType<ISymbolResult>();
154+
symbols.setup((s: any) => s.then).returns(() => undefined);
155+
const definitions: IDefinition[] = [];
156+
for (let counter = 0; counter < 3; counter += 1) {
157+
const def = TypeMoq.Mock.ofType<IDefinition>();
158+
def.setup(d => d.fileName).returns(() => counter.toString());
159+
definitions.push(def.object);
160+
161+
fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isValue(counter.toString()), TypeMoq.It.isValue(__filename)))
162+
.returns(() => false)
163+
.verifiable(TypeMoq.Times.exactly(1));
164+
}
165+
symbols.setup(s => s.definitions)
166+
.returns(() => definitions)
167+
.verifiable(TypeMoq.Times.atLeastOnce());
168+
169+
jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
170+
.returns(() => Promise.resolve(symbols.object))
171+
.verifiable(TypeMoq.Times.once());
172+
173+
provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0);
174+
await provider.provideDocumentSymbols(doc.object, new CancellationTokenSource().token);
175+
176+
doc.verifyAll();
177+
symbols.verifyAll();
178+
fileSystem.verifyAll();
179+
jediHandler.verifyAll();
180+
});
129181
});

0 commit comments

Comments
 (0)