Skip to content

Commit 7601808

Browse files
authored
Add ability to navigate to tests (#4289)
* Ability to navigate to tests * Add tests * Added telemetry * Change line width * Fix formatting * Custom symbol provider * Fixed linters * Fix tests
1 parent 9187479 commit 7601808

34 files changed

Lines changed: 1067 additions & 32 deletions

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@
2727
"javascript.preferences.quoteStyle": "single",
2828
"typescriptHero.imports.stringQuoteStyle": "'",
2929
"prettier.tslintIntegration": false,
30-
"prettier.printWidth": 120,
30+
"prettier.printWidth": 180,
31+
"prettier.singleQuote": true
3132
}

pythonFiles/symbolProvider.py

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 ast
5+
import json
6+
import sys
7+
8+
9+
class Visitor(ast.NodeVisitor):
10+
def __init__(self):
11+
self.symbols = {"classes": [], "methods": [], "functions": []}
12+
13+
def visit_Module(self, node):
14+
self.visitChildren(node)
15+
16+
def visitChildren(self, node, namespace=""):
17+
for child in node.body:
18+
if isinstance(child, ast.FunctionDef):
19+
self.visitDef(child, namespace)
20+
if isinstance(child, ast.ClassDef):
21+
self.visitClassDef(child, namespace)
22+
try:
23+
if isinstance(child, ast.AsyncFunctionDef):
24+
self.visitDef(child, namespace)
25+
except Exception:
26+
pass
27+
28+
def visitDef(self, node, namespace=""):
29+
end_position = self.getEndPosition(node)
30+
symbol = "functions" if namespace == "" else "methods"
31+
self.symbols[symbol].append(self.getDataObject(node, namespace))
32+
33+
def visitClassDef(self, node, namespace=""):
34+
end_position = self.getEndPosition(node)
35+
self.symbols['classes'].append(self.getDataObject(node, namespace))
36+
37+
if len(namespace) > 0:
38+
namespace = "{0}::{1}".format(namespace, node.name)
39+
else:
40+
namespace = node.name
41+
self.visitChildren(node, namespace)
42+
43+
def getDataObject(self, node, namespace=""):
44+
end_position = self.getEndPosition(node)
45+
return {
46+
"namespace": namespace,
47+
"name": node.name,
48+
"range": {
49+
"start": {
50+
"line": node.lineno,
51+
"character": node.col_offset
52+
},
53+
"end": {
54+
"line": end_position[0],
55+
"character": end_position[1]
56+
}
57+
}
58+
}
59+
60+
def getEndPosition(self, node):
61+
if not hasattr(node, 'body') or len(node.body) == 0:
62+
return (node.lineno, node.col_offset)
63+
return self.getEndPosition(node.body[-1])
64+
65+
66+
def provide_symbols(source):
67+
"""Provides a list of all symbols in provided code.
68+
69+
The list comprises of 3-item tuples that contain the starting line number,
70+
ending line number and whether the statement is a single line.
71+
72+
"""
73+
tree = ast.parse(source)
74+
visitor = Visitor()
75+
visitor.visit(tree)
76+
sys.stdout.write(json.dumps(visitor.symbols))
77+
sys.stdout.flush()
78+
79+
80+
if __name__ == "__main__":
81+
if len(sys.argv) == 3:
82+
contents = sys.argv[2]
83+
else:
84+
with open(sys.argv[1], "r") as source:
85+
contents = source.read()
86+
87+
try:
88+
default_encoding = sys.getdefaultencoding()
89+
encoded_contents = contents.encode(default_encoding, 'surrogateescape')
90+
contents = encoded_contents.decode(default_encoding, 'replace')
91+
except (UnicodeError, LookupError):
92+
pass
93+
if isinstance(contents, bytes):
94+
contents = contents.decode('utf8')
95+
provide_symbols(contents)

src/client/activation/jedi.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@ export class JediExtensionActivator implements ILanguageServerActivator {
107107

108108
const testManagementService = this.serviceManager.get<IUnitTestManagementService>(IUnitTestManagementService);
109109
testManagementService
110-
.activate()
111-
.then(() => testManagementService.activateCodeLenses(symbolProvider))
110+
.activate(symbolProvider)
112111
.catch(ex => this.serviceManager.get<ILogger>(ILogger).logError('Failed to activate Unit Tests', ex));
113112
}
114113

src/client/activation/languageServer/languageServer.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ export class LanguageServer implements ILanguageServer {
9090
if (!this.languageClient) {
9191
throw new Error('languageClient not initialized');
9292
}
93-
await this.testManager.activate();
94-
await this.testManager.activateCodeLenses(new LanguageServerSymbolProvider(this.languageClient!));
93+
await this.testManager.activate(new LanguageServerSymbolProvider(this.languageClient!));
9594
}
9695
}

src/client/common/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export namespace Commands {
4141
export const Enable_Linter = 'python.enableLinting';
4242
export const Run_Linter = 'python.runLinting';
4343
export const Enable_SourceMap_Support = 'python.enableSourceMapSupport';
44+
export const navigateToTestFunction = 'navigateToTestFunction';
45+
export const navigateToTestSuite = 'navigateToTestSuite';
46+
export const navigateToTestFile = 'navigateToTestFile';
4447
}
4548
export namespace Octicons {
4649
export const Test_Pass = '$(check)';

src/client/common/types.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
'use strict';
44

55
import { Socket } from 'net';
6-
import { ConfigurationTarget, DiagnosticSeverity, Disposable, Event, Extension, ExtensionContext, OutputChannel, Uri, WorkspaceEdit } from 'vscode';
6+
import { ConfigurationTarget, DiagnosticSeverity, Disposable, DocumentSymbolProvider, Event, Extension, ExtensionContext, OutputChannel, Uri, WorkspaceEdit } from 'vscode';
77
import { EnvironmentVariables } from './variables/types';
88
export const IOutputChannel = Symbol('IOutputChannel');
9-
export interface IOutputChannel extends OutputChannel { }
9+
export interface IOutputChannel extends OutputChannel {}
1010
export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider');
11+
export interface IDocumentSymbolProvider extends DocumentSymbolProvider {}
1112
export const IsWindows = Symbol('IS_WINDOWS');
1213
export const IDisposableRegistry = Symbol('IDiposableRegistry');
1314
export type IDisposableRegistry = { push(disposable: Disposable): void };
@@ -293,7 +294,7 @@ export interface IDataScienceSettings {
293294
showCellInputCode: boolean;
294295
collapseCellInputCodeByDefault: boolean;
295296
maxOutputSize: number;
296-
sendSelectionToInteractiveWindow : boolean;
297+
sendSelectionToInteractiveWindow: boolean;
297298
markdownRegularExpression: string;
298299
codeRegularExpression: string;
299300
}
@@ -313,7 +314,7 @@ export interface ISocketServer extends Disposable {
313314
}
314315

315316
export const IExtensionContext = Symbol('ExtensionContext');
316-
export interface IExtensionContext extends ExtensionContext { }
317+
export interface IExtensionContext extends ExtensionContext {}
317318

318319
export const IExtensions = Symbol('IExtensions');
319320
export interface IExtensions {

src/client/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import { EditorLoadTelemetry } from './telemetry/types';
102102
import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry';
103103
import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types';
104104
import { TEST_OUTPUT_CHANNEL } from './unittests/common/constants';
105+
import { ITestCodeNavigatorCommandHandler } from './unittests/navigation/types';
105106
import { registerTypes as unitTestsRegisterTypes } from './unittests/serviceRegistry';
106107

107108
durations.codeLoadingTime = stopWatch.elapsedTime;
@@ -294,6 +295,7 @@ function initializeServices(context: ExtensionContext, serviceManager: ServiceMa
294295
serviceContainer.get<InterpreterLocatorProgressHandler>(InterpreterLocatorProgressHandler).register();
295296
serviceContainer.get<IInterpreterLocatorProgressService>(IInterpreterLocatorProgressService).register();
296297
serviceContainer.get<IApplicationDiagnostics>(IApplicationDiagnostics).register();
298+
serviceContainer.get<ITestCodeNavigatorCommandHandler>(ITestCodeNavigatorCommandHandler).register();
297299
}
298300

299301
// tslint:disable-next-line:no-any

src/client/telemetry/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export enum EventName {
4040
UNITTEST_RUN = 'UNITTEST.RUN',
4141
UNITTEST_DISCOVER = 'UNITTEST.DISCOVER',
4242
UNITTEST_VIEW_OUTPUT = 'UNITTEST.VIEW_OUTPUT',
43+
UNITTEST_NAVIGATE_TEST_FILE = 'UNITTEST.NAVIGATE.TEST_FILE',
44+
UNITTEST_NAVIGATE_TEST_FUNCTION = 'UNITTEST.NAVIGATE.TEST_FUNCTION',
45+
UNITTEST_NAVIGATE_TEST_SUITE = 'UNITTEST.NAVIGATE.TEST_SUITE',
4346
PYTHON_LANGUAGE_SERVER_ANALYSISTIME = 'PYTHON_LANGUAGE_SERVER.ANALYSIS_TIME',
4447
PYTHON_LANGUAGE_SERVER_ENABLED = 'PYTHON_LANGUAGE_SERVER.ENABLED',
4548
PYTHON_LANGUAGE_SERVER_EXTRACTED = 'PYTHON_LANGUAGE_SERVER.EXTRACTED',

src/client/telemetry/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,7 @@ interface IEventNamePropertyMapping {
324324
[Telemetry.StartJupyter]: never | undefined;
325325
[Telemetry.SubmitCellThroughInput]: never | undefined;
326326
[Telemetry.Undo]: never | undefined;
327+
[EventName.UNITTEST_NAVIGATE_TEST_FILE]: never | undefined;
328+
[EventName.UNITTEST_NAVIGATE_TEST_FUNCTION]: never | undefined;
329+
[EventName.UNITTEST_NAVIGATE_TEST_SUITE]: never | undefined;
327330
}

src/client/unittests/codeLenses/main.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import { PYTHON } from '../../common/constants';
33
import { ITestCollectionStorageService } from '../common/types';
44
import { TestFileCodeLensProvider } from './testFiles';
55

6-
export function activateCodeLenses(onDidChange: vscode.EventEmitter<void>,
7-
symboldProvider: vscode.DocumentSymbolProvider, testCollectionStorage: ITestCollectionStorageService): vscode.Disposable {
8-
6+
export function activateCodeLenses(
7+
onDidChange: vscode.EventEmitter<void>,
8+
symbolProvider: vscode.DocumentSymbolProvider,
9+
testCollectionStorage: ITestCollectionStorageService
10+
): vscode.Disposable {
911
const disposables: vscode.Disposable[] = [];
10-
const codeLensProvider = new TestFileCodeLensProvider(onDidChange, symboldProvider, testCollectionStorage);
12+
const codeLensProvider = new TestFileCodeLensProvider(onDidChange, symbolProvider, testCollectionStorage);
1113
disposables.push(vscode.languages.registerCodeLensProvider(PYTHON, codeLensProvider));
1214

1315
return {
14-
dispose: () => { disposables.forEach(d => d.dispose()); }
16+
dispose: () => {
17+
disposables.forEach(d => d.dispose());
18+
}
1519
};
1620
}

0 commit comments

Comments
 (0)