Skip to content

Commit d3e73f9

Browse files
Merge pull request #2377 from DustinCampbell/use-code-structure
Use new `/codestructure` endpoint from OmniSharp
2 parents b989062 + 2d4907e commit d3e73f9

31 files changed

+749
-6972
lines changed

src/features/codeLensProvider.ts

+208-108
Original file line numberDiff line numberDiff line change
@@ -6,171 +6,271 @@
66
import * as protocol from '../omnisharp/protocol';
77
import * as serverUtils from '../omnisharp/utils';
88
import * as vscode from 'vscode';
9-
import { toLocation, toRange } from '../omnisharp/typeConvertion';
9+
import { toLocation } from '../omnisharp/typeConvertion';
1010
import AbstractProvider from './abstractProvider';
1111
import { OmniSharpServer } from '../omnisharp/server';
1212
import { Options } from '../omnisharp/options';
1313
import TestManager from './dotnetTest';
1414
import OptionProvider from '../observers/OptionProvider';
1515

16-
class OmniSharpCodeLens extends vscode.CodeLens {
16+
import Structure = protocol.V2.Structure;
17+
import SymbolKinds = protocol.V2.SymbolKinds;
18+
import SymbolPropertyNames = protocol.V2.SymbolPropertyNames;
19+
import SymbolRangeNames = protocol.V2.SymbolRangeNames;
1720

18-
fileName: string;
21+
abstract class OmniSharpCodeLens extends vscode.CodeLens {
22+
constructor(
23+
range: protocol.V2.Range,
24+
public fileName: string) {
1925

20-
constructor(fileName: string, range: vscode.Range) {
21-
super(range);
22-
this.fileName = fileName;
26+
super(new vscode.Range(
27+
range.Start.Line - 1, range.Start.Column - 1, range.End.Line - 1, range.End.Column - 1
28+
));
29+
}
30+
}
31+
32+
class ReferencesCodeLens extends OmniSharpCodeLens {
33+
constructor(
34+
range: protocol.V2.Range,
35+
fileName: string) {
36+
super(range, fileName);
37+
}
38+
}
39+
40+
abstract class TestCodeLens extends OmniSharpCodeLens {
41+
constructor(
42+
range: protocol.V2.Range,
43+
fileName: string,
44+
public isTestContainer: boolean,
45+
public testFramework: string,
46+
public testMethodNames: string[]) {
47+
48+
super(range, fileName);
49+
}
50+
}
51+
52+
class RunTestsCodeLens extends TestCodeLens {
53+
constructor(
54+
range: protocol.V2.Range,
55+
fileName: string,
56+
isTestContainer: boolean,
57+
testFramework: string,
58+
testMethodNames: string[]) {
59+
60+
super(range, fileName, isTestContainer, testFramework, testMethodNames);
61+
}
62+
}
63+
64+
class DebugTestsCodeLens extends TestCodeLens {
65+
constructor(
66+
range: protocol.V2.Range,
67+
fileName: string,
68+
isTestContainer: boolean,
69+
testFramework: string,
70+
testMethodNames: string[]) {
71+
72+
super(range, fileName, isTestContainer, testFramework, testMethodNames);
2373
}
2474
}
2575

2676
export default class OmniSharpCodeLensProvider extends AbstractProvider implements vscode.CodeLensProvider {
2777

2878
constructor(server: OmniSharpServer, testManager: TestManager, private optionProvider: OptionProvider) {
2979
super(server);
30-
3180
}
3281

33-
private static filteredSymbolNames: { [name: string]: boolean } = {
34-
'Equals': true,
35-
'Finalize': true,
36-
'GetHashCode': true,
37-
'ToString': true
38-
};
39-
40-
async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken) {
41-
let options = this.optionProvider.GetLatestOptions();
82+
async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.CodeLens[]> {
83+
const options = this.optionProvider.GetLatestOptions();
4284
if (!options.showReferencesCodeLens && !options.showTestsCodeLens) {
4385
return [];
4486
}
4587

46-
let tree = await serverUtils.currentFileMembersAsTree(this._server, { FileName: document.fileName }, token);
47-
let ret: vscode.CodeLens[] = [];
88+
const response = await serverUtils.codeStructure(this._server, { FileName: document.fileName }, token);
4889

49-
for (let node of tree.TopLevelTypeDefinitions) {
50-
await this._convertQuickFix(ret, document.fileName, node, options);
90+
if (response && response.Elements) {
91+
return createCodeLenses(response.Elements, document.fileName, options);
5192
}
5293

53-
return ret;
94+
return [];
5495
}
5596

97+
async resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken): Promise<vscode.CodeLens> {
98+
if (codeLens instanceof ReferencesCodeLens) {
99+
return this.resolveReferencesCodeLens(codeLens, token);
100+
}
101+
else if (codeLens instanceof RunTestsCodeLens) {
102+
return this.resolveTestCodeLens(codeLens, 'Run Test', 'dotnet.test.run', 'Run All Tests', 'dotnet.classTests.run');
103+
}
104+
else if (codeLens instanceof DebugTestsCodeLens) {
105+
return this.resolveTestCodeLens(codeLens, 'Debug Test', 'dotnet.test.debug', 'Debug All Tests', 'dotnet.classTests.debug');
106+
}
107+
}
108+
109+
private async resolveReferencesCodeLens(codeLens: ReferencesCodeLens, token: vscode.CancellationToken): Promise<vscode.CodeLens> {
110+
const request: protocol.FindUsagesRequest = {
111+
FileName: codeLens.fileName,
112+
Line: codeLens.range.start.line + 1, // OmniSharp is 1-based
113+
Column: codeLens.range.start.character + 1, // OmniSharp is 1-based
114+
OnlyThisFile: false,
115+
ExcludeDefinition: true
116+
};
56117

57-
private async _convertQuickFix(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node, options: Options): Promise<void> {
118+
const result = await serverUtils.findUsages(this._server, request, token);
58119

59-
if (node.Kind === 'MethodDeclaration' && OmniSharpCodeLensProvider.filteredSymbolNames[node.Location.Text]) {
120+
if (!result || !result.QuickFixes) {
60121
return;
61122
}
62123

63-
let lens = new OmniSharpCodeLens(fileName, toRange(node.Location));
64-
if (options.showReferencesCodeLens) {
65-
bucket.push(lens);
66-
}
124+
const quickFixes = result.QuickFixes;
125+
const count = quickFixes.length;
67126

68-
for (let child of node.ChildNodes) {
69-
this._convertQuickFix(bucket, fileName, child, options);
70-
}
127+
codeLens.command = {
128+
title: count === 1 ? '1 reference' : `${count} references`,
129+
command: 'editor.action.showReferences',
130+
arguments: [vscode.Uri.file(request.FileName), codeLens.range.start, quickFixes.map(toLocation)]
131+
};
71132

72-
if (options.showTestsCodeLens) {
73-
await this._updateCodeLensForTest(bucket, fileName, node);
74-
}
133+
return codeLens;
75134
}
76135

77-
resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken): Thenable<vscode.CodeLens> {
78-
if (codeLens instanceof OmniSharpCodeLens) {
136+
private async resolveTestCodeLens(codeLens: TestCodeLens, singularTitle: string, singularCommandName: string, pluralTitle: string, pluralCommandName: string): Promise<vscode.CodeLens> {
137+
if (!codeLens.isTestContainer) {
138+
// This is just a single test method, not a container.
139+
codeLens.command = {
140+
title: singularTitle,
141+
command: singularCommandName,
142+
arguments: [codeLens.testMethodNames[0], codeLens.fileName, codeLens.testFramework]
143+
};
79144

80-
let req = <protocol.FindUsagesRequest>{
81-
FileName: codeLens.fileName,
82-
Line: codeLens.range.start.line + 1,
83-
Column: codeLens.range.start.character + 1,
84-
OnlyThisFile: false,
85-
ExcludeDefinition: true
145+
return codeLens;
146+
}
147+
148+
const projectInfo = await serverUtils.requestProjectInformation(this._server, { FileName: codeLens.fileName });
149+
150+
// We do not support running all tests on legacy projects.
151+
if (projectInfo.MsBuildProject && !projectInfo.DotNetProject) {
152+
codeLens.command = {
153+
title: pluralTitle,
154+
command: pluralCommandName,
155+
arguments: [codeLens.testMethodNames, codeLens.fileName, codeLens.testFramework]
86156
};
157+
}
87158

88-
return serverUtils.findUsages(this._server, req, token).then(res => {
89-
if (!res || !Array.isArray(res.QuickFixes)) {
90-
return;
91-
}
159+
return codeLens;
160+
}
161+
}
162+
163+
function createCodeLenses(elements: Structure.CodeElement[], fileName: string, options: Options): vscode.CodeLens[] {
164+
let results: vscode.CodeLens[] = [];
165+
166+
Structure.walkCodeElements(elements, element => {
167+
let codeLenses = createCodeLensesForElement(element, fileName, options);
168+
169+
results.push(...codeLenses);
170+
});
92171

93-
let len = res.QuickFixes.length;
94-
codeLens.command = {
95-
title: len === 1 ? '1 reference' : `${len} references`,
96-
command: 'editor.action.showReferences',
97-
arguments: [vscode.Uri.file(req.FileName), codeLens.range.start, res.QuickFixes.map(toLocation)]
98-
};
172+
return results;
173+
}
174+
175+
function createCodeLensesForElement(element: Structure.CodeElement, fileName: string, options: Options): vscode.CodeLens[] {
176+
let results: vscode.CodeLens[] = [];
99177

100-
return codeLens;
101-
});
178+
if (options.showReferencesCodeLens && isValidElementForReferencesCodeLens(element)) {
179+
let range = element.Ranges[SymbolRangeNames.Name];
180+
if (range) {
181+
results.push(new ReferencesCodeLens(range, fileName));
102182
}
103183
}
104184

105-
private async _updateCodeLensForTest(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node): Promise<void> {
106-
// backward compatible check: Features property doesn't present on older version OmniSharp
107-
if (node.Features === undefined) {
108-
return;
109-
}
185+
if (options.showTestsCodeLens) {
186+
if (isValidMethodForTestCodeLens(element)) {
187+
let [testFramework, testMethodName] = getTestFrameworkAndMethodName(element);
188+
let range = element.Ranges[SymbolRangeNames.Name];
110189

111-
if (node.Kind === "ClassDeclaration" && node.ChildNodes.length > 0) {
112-
let projectInfo = await serverUtils.requestProjectInformation(this._server, { FileName: fileName });
113-
if (!projectInfo.DotNetProject && projectInfo.MsBuildProject) {
114-
this._updateCodeLensForTestClass(bucket, fileName, node);
190+
if (range && testFramework && testMethodName) {
191+
results.push(new RunTestsCodeLens(range, fileName, /*isTestContainer*/ false, testFramework, [testMethodName]));
192+
results.push(new DebugTestsCodeLens(range, fileName, /*isTestContainer*/ false, testFramework, [testMethodName]));
115193
}
116194
}
195+
else if (isValidClassForTestCodeLens(element)) {
196+
// Note: We don't handle multiple test frameworks in the same class. The first test framework wins.
197+
let testFramework: string = null;
198+
let testMethodNames: string[] = [];
199+
let range = element.Ranges[SymbolRangeNames.Name];
200+
201+
for (let childElement of element.Children) {
202+
let [childTestFramework, childTestMethodName] = getTestFrameworkAndMethodName(childElement);
203+
204+
if (!testFramework && childTestFramework) {
205+
testFramework = childTestFramework;
206+
testMethodNames.push(childTestMethodName);
207+
}
208+
else if (testFramework && childTestFramework === testFramework) {
209+
testMethodNames.push(childTestMethodName);
210+
}
211+
}
117212

118-
let [testFeature, testFrameworkName] = this._getTestFeatureAndFramework(node);
119-
if (testFeature) {
120-
bucket.push(new vscode.CodeLens(
121-
toRange(node.Location),
122-
{ title: "Run Test", command: 'dotnet.test.run', arguments: [testFeature.Data, fileName, testFrameworkName] }));
123-
124-
bucket.push(new vscode.CodeLens(
125-
toRange(node.Location),
126-
{ title: "Debug Test", command: 'dotnet.test.debug', arguments: [testFeature.Data, fileName, testFrameworkName] }));
213+
results.push(new RunTestsCodeLens(range, fileName, /*isTestContainer*/ true, testFramework, testMethodNames));
214+
results.push(new DebugTestsCodeLens(range, fileName, /*isTestContainer*/ true, testFramework, testMethodNames));
127215
}
128216
}
129217

130-
private _updateCodeLensForTestClass(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node) {
131-
// if the class doesnot contain any method then return
132-
if (!node.ChildNodes.find(value => (value.Kind === "MethodDeclaration"))) {
133-
return;
134-
}
218+
return results;
219+
}
135220

136-
let testMethods = new Array<string>();
137-
let testFrameworkName: string = null;
138-
for (let child of node.ChildNodes) {
139-
let [testFeature, frameworkName] = this._getTestFeatureAndFramework(child);
140-
if (testFeature) {
141-
// this test method has a test feature
142-
if (!testFrameworkName) {
143-
testFrameworkName = frameworkName;
144-
}
221+
const filteredSymbolNames: { [name: string]: boolean } = {
222+
'Equals': true,
223+
'Finalize': true,
224+
'GetHashCode': true,
225+
'ToString': true
226+
};
145227

146-
testMethods.push(testFeature.Data);
147-
}
148-
}
228+
function isValidElementForReferencesCodeLens(element: Structure.CodeElement): boolean {
229+
if (element.Kind === SymbolKinds.Namespace) {
230+
return false;
231+
}
149232

150-
if (testMethods.length > 0) {
151-
bucket.push(new vscode.CodeLens(
152-
toRange(node.Location),
153-
{ title: "Run All Tests", command: 'dotnet.classTests.run', arguments: [testMethods, fileName, testFrameworkName] }));
154-
bucket.push(new vscode.CodeLens(
155-
toRange(node.Location),
156-
{ title: "Debug All Tests", command: 'dotnet.classTests.debug', arguments: [testMethods, fileName, testFrameworkName] }));
157-
}
233+
if (element.Kind === SymbolKinds.Method && filteredSymbolNames[element.Name]) {
234+
return false;
158235
}
159236

160-
private _getTestFeatureAndFramework(node: protocol.Node): [protocol.SyntaxFeature, string] {
161-
let testFeature = node.Features.find(value => (value.Name == 'XunitTestMethod' || value.Name == 'NUnitTestMethod' || value.Name == 'MSTestMethod'));
162-
if (testFeature) {
163-
let testFrameworkName = 'xunit';
164-
if (testFeature.Name == 'NUnitTestMethod') {
165-
testFrameworkName = 'nunit';
166-
}
167-
else if (testFeature.Name == 'MSTestMethod') {
168-
testFrameworkName = 'mstest';
169-
}
237+
return true;
238+
}
170239

171-
return [testFeature, testFrameworkName];
172-
}
173240

241+
function isValidClassForTestCodeLens(element: Structure.CodeElement): boolean {
242+
if (element.Kind != SymbolKinds.Class) {
243+
return false;
244+
}
245+
246+
if (!element.Children) {
247+
return false;
248+
}
249+
250+
return element.Children.find(isValidMethodForTestCodeLens) !== undefined;
251+
}
252+
253+
function isValidMethodForTestCodeLens(element: Structure.CodeElement): boolean {
254+
if (element.Kind != SymbolKinds.Method) {
255+
return false;
256+
}
257+
258+
if (!element.Properties ||
259+
!element.Properties[SymbolPropertyNames.TestFramework] ||
260+
!element.Properties[SymbolPropertyNames.TestMethodName]) {
261+
return false;
262+
}
263+
264+
return true;
265+
}
266+
267+
function getTestFrameworkAndMethodName(element: Structure.CodeElement): [string, string] {
268+
if (!element.Properties) {
174269
return [null, null];
175270
}
271+
272+
const testFramework = element.Properties[SymbolPropertyNames.TestFramework];
273+
const testMethodName = element.Properties[SymbolPropertyNames.TestMethodName];
274+
275+
return [testFramework, testMethodName];
176276
}

src/features/commands.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export default function registerCommands(server: OmniSharpServer, platformInfo:
2525
disposable.add(vscode.commands.registerCommand('o.restart', () => restartOmniSharp(server)));
2626
disposable.add(vscode.commands.registerCommand('o.pickProjectAndStart', async () => pickProjectAndStart(server, optionProvider)));
2727
disposable.add(vscode.commands.registerCommand('o.showOutput', () => eventStream.post(new ShowOmniSharpChannel())));
28-
disposable.add(vscode.commands.registerCommand('dotnet.restore.project', () => pickProjectAndDotnetRestore(server, eventStream)));
29-
disposable.add(vscode.commands.registerCommand('dotnet.restore.all', () => dotnetRestoreAllProjects(server, eventStream)));
28+
disposable.add(vscode.commands.registerCommand('dotnet.restore.project', async () => pickProjectAndDotnetRestore(server, eventStream)));
29+
disposable.add(vscode.commands.registerCommand('dotnet.restore.all', async () => dotnetRestoreAllProjects(server, eventStream)));
3030

3131
// register empty handler for csharp.installDebugger
3232
// running the command activates the extension, which is all we need for installation to kickoff
@@ -147,7 +147,7 @@ async function getProjectDescriptors(server: OmniSharpServer): Promise<protocol.
147147
return descriptors;
148148
}
149149

150-
async function dotnetRestore(cwd: string, eventStream: EventStream, filePath?: string): Promise<void> {
150+
export async function dotnetRestore(cwd: string, eventStream: EventStream, filePath?: string): Promise<void> {
151151
return new Promise<void>((resolve, reject) => {
152152
let cmd = 'dotnet';
153153
let args = ['restore'];

0 commit comments

Comments
 (0)