Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GoToTypeDefinition provider #5094

Merged
merged 9 commits into from
Mar 24, 2022
121 changes: 67 additions & 54 deletions src/features/definitionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
*--------------------------------------------------------------------------------------------*/

import * as serverUtils from '../omnisharp/utils';
import { CancellationToken, DefinitionProvider, Location, Position, TextDocument, Uri } from 'vscode';
import { MetadataRequest, MetadataSource, V2 } from '../omnisharp/protocol';
import { CancellationToken, TypeDefinitionProvider, DefinitionProvider, Location, Position, TextDocument, Uri } from 'vscode';
import { GoToTypeDefinitionRequest, GoToTypeDefinitionResponse, MetadataRequest, MetadataSource, V2 } from '../omnisharp/protocol';
import { createRequest, toRange3, toVscodeLocation } from '../omnisharp/typeConversion';
import AbstractSupport from './abstractProvider';
import DefinitionMetadataOrSourceGeneratedDocumentProvider from './definitionMetadataDocumentProvider';
import { OmniSharpServer } from '../omnisharp/server';
import { LanguageMiddlewareFeature } from '../omnisharp/LanguageMiddlewareFeature';
import SourceGeneratedDocumentProvider from './sourceGeneratedDocumentProvider';

export default class CSharpDefinitionProvider extends AbstractSupport implements DefinitionProvider {
export default class CSharpDefinitionProvider extends AbstractSupport implements DefinitionProvider, TypeDefinitionProvider {
constructor(
server: OmniSharpServer,
private definitionMetadataDocumentProvider: DefinitionMetadataOrSourceGeneratedDocumentProvider,
Expand All @@ -23,76 +23,89 @@ export default class CSharpDefinitionProvider extends AbstractSupport implements
}

public async provideDefinition(document: TextDocument, position: Position, token: CancellationToken): Promise<Location[]> {

let req = <V2.GoToDefinitionRequest>createRequest(document, position);
req.WantMetadata = true;

const locations: Location[] = [];
try {
const gotoDefinitionResponse = await serverUtils.goToDefinition(this._server, req, token);
// the defintion is in source
if (gotoDefinitionResponse && gotoDefinitionResponse.Definitions) {

for (const definition of gotoDefinitionResponse.Definitions) {
if (definition.MetadataSource) {
// the definition is in metadata
const metadataSource: MetadataSource = definition.MetadataSource;
return await this.GetLocationsFromResponse(gotoDefinitionResponse, token);
}
catch (error) {
return [];
}
}

// Do we already have a document for this metadata reference?
if (definition.Location.FileName.startsWith("$metadata$") &&
this.definitionMetadataDocumentProvider.hasMetadataDocument(definition.Location.FileName)) {
public async provideTypeDefinition(document: TextDocument, position: Position, token: CancellationToken): Promise<Location[]> {
let req = <GoToTypeDefinitionRequest>createRequest(document, position);
req.WantMetadata = true;

// if it is part of an already used metadata file, retrieve its uri instead of going to the physical file
const uri = this.definitionMetadataDocumentProvider.getExistingMetadataResponseUri(definition.Location.FileName);
const vscodeRange = toRange3(definition.Location.Range);
locations.push(new Location(uri, vscodeRange));
continue;
}
try {
const goToTypeDefinitionResponse = await serverUtils.goToTypeDefinition(this._server, req, token);
return await this.GetLocationsFromResponse(goToTypeDefinitionResponse, token);
}
catch (error) {
return [];
}
}

// We need to go to the metadata endpoint for more information
const metadataResponse = await serverUtils.getMetadata(this._server, <MetadataRequest>{
Timeout: 5000,
AssemblyName: metadataSource.AssemblyName,
VersionNumber: metadataSource.VersionNumber,
ProjectName: metadataSource.ProjectName,
Language: metadataSource.Language,
TypeName: metadataSource.TypeName
});
private async GetLocationsFromResponse<TReponse> (response: GoToTypeDefinitionResponse | V2.GoToDefinitionResponse, token: CancellationToken): Promise<Location[]>
{
let locations: Location[] = [];
if (response && response.Definitions) {
for (const definition of response.Definitions) {
if (definition.MetadataSource) {
// the definition is in metadata
const metadataSource: MetadataSource = definition.MetadataSource;

if (!metadataResponse || !metadataResponse.Source || !metadataResponse.SourceName) {
continue;
}
// Do we already have a document for this metadata reference?
if (definition.Location.FileName.startsWith("$metadata$") &&
this.definitionMetadataDocumentProvider.hasMetadataDocument(definition.Location.FileName)) {

const uri: Uri = this.definitionMetadataDocumentProvider.addMetadataResponse(metadataResponse);
// if it is part of an already used metadata file, retrieve its uri instead of going to the physical file
const uri = this.definitionMetadataDocumentProvider.getExistingMetadataResponseUri(definition.Location.FileName);
const vscodeRange = toRange3(definition.Location.Range);
locations.push(new Location(uri, vscodeRange));
} else if (definition.SourceGeneratedFileInfo) {
// File is source generated
let uri = this.sourceGeneratedDocumentProvider.tryGetExistingSourceGeneratedFile(definition.SourceGeneratedFileInfo);
if (!uri) {
const sourceGeneratedFileResponse = await serverUtils.getSourceGeneratedFile(this._server, definition.SourceGeneratedFileInfo, token);
continue;
}

// We need to go to the metadata endpoint for more information
const metadataResponse = await serverUtils.getMetadata(this._server, <MetadataRequest>{
Timeout: 5000,
AssemblyName: metadataSource.AssemblyName,
VersionNumber: metadataSource.VersionNumber,
ProjectName: metadataSource.ProjectName,
Language: metadataSource.Language,
TypeName: metadataSource.TypeName
});

if (!metadataResponse || !metadataResponse.Source || !metadataResponse.SourceName) {
continue;
}

if (!sourceGeneratedFileResponse || !sourceGeneratedFileResponse.Source || !sourceGeneratedFileResponse.SourceName) {
continue;
}
const uri: Uri = this.definitionMetadataDocumentProvider.addMetadataResponse(metadataResponse);
const vscodeRange = toRange3(definition.Location.Range);
locations.push(new Location(uri, vscodeRange));
} else if (definition.SourceGeneratedFileInfo) {
// File is source generated
let uri = this.sourceGeneratedDocumentProvider.tryGetExistingSourceGeneratedFile(definition.SourceGeneratedFileInfo);
if (!uri) {
const sourceGeneratedFileResponse = await serverUtils.getSourceGeneratedFile(this._server, definition.SourceGeneratedFileInfo, token);

uri = this.sourceGeneratedDocumentProvider.addSourceGeneratedFile(definition.SourceGeneratedFileInfo, sourceGeneratedFileResponse);
if (!sourceGeneratedFileResponse || !sourceGeneratedFileResponse.Source || !sourceGeneratedFileResponse.SourceName) {
continue;
}

locations.push(new Location(uri, toRange3(definition.Location.Range)));
} else {
// if it is a normal source definition, convert the response to a location
locations.push(toVscodeLocation(definition.Location));
uri = this.sourceGeneratedDocumentProvider.addSourceGeneratedFile(definition.SourceGeneratedFileInfo, sourceGeneratedFileResponse);
}

locations.push(new Location(uri, toRange3(definition.Location.Range)));
} else {
// if it is a normal source definition, convert the response to a location
locations.push(toVscodeLocation(definition.Location));
}
}

// Allow language middlewares to re-map its edits if necessary.
const result = await this._languageMiddlewareFeature.remap("remapLocations", locations, token);
return result;
}
catch (error) {
return [];
}
// Allow language middlewares to re-map its edits if necessary.
return await this._languageMiddlewareFeature.remap("remapLocations", locations, token);
}
}
2 changes: 2 additions & 0 deletions src/omnisharp/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export async function activate(context: vscode.ExtensionContext, packageJSON: an
const definitionProvider = new DefinitionProvider(server, definitionMetadataDocumentProvider, sourceGeneratedDocumentProvider, languageMiddlewareFeature);
localDisposables.add(vscode.languages.registerDefinitionProvider(documentSelector, definitionProvider));
localDisposables.add(vscode.languages.registerDefinitionProvider({ scheme: definitionMetadataDocumentProvider.scheme }, definitionProvider));
localDisposables.add(vscode.languages.registerTypeDefinitionProvider(documentSelector, definitionProvider));
localDisposables.add(vscode.languages.registerTypeDefinitionProvider({ scheme: definitionMetadataDocumentProvider.scheme }, definitionProvider));
localDisposables.add(vscode.languages.registerImplementationProvider(documentSelector, new ImplementationProvider(server, languageMiddlewareFeature)));
localDisposables.add(vscode.languages.registerCodeLensProvider(documentSelector, new CodeLensProvider(server, testManager, optionProvider, languageMiddlewareFeature)));
localDisposables.add(vscode.languages.registerDocumentHighlightProvider(documentSelector, new DocumentHighlightProvider(server, languageMiddlewareFeature)));
Expand Down
3 changes: 2 additions & 1 deletion src/omnisharp/prioritization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const normalCommands = [
protocol.V2.Requests.GoToDefinition,
protocol.Requests.RunCodeAction,
protocol.Requests.SignatureHelp,
protocol.Requests.TypeLookup
protocol.Requests.TypeLookup,
protocol.Requests.GoToTypeDefinition
];

const prioritySet = new Set<string>(priorityCommands);
Expand Down
16 changes: 16 additions & 0 deletions src/omnisharp/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export module Requests {
export const FormatAfterKeystroke = '/formatAfterKeystroke';
export const FormatRange = '/formatRange';
export const GetCodeActions = '/getcodeactions';
export const GoToTypeDefinition = '/gototypedefinition';
export const FindImplementations = '/findimplementations';
export const Project = '/project';
export const Projects = '/projects';
Expand Down Expand Up @@ -580,6 +581,21 @@ export enum UpdateType {
export interface SourceGeneratedFileClosedRequest extends SourceGeneratedFileInfo {
}

export interface Definition {
Location: V2.Location;
MetadataSource?: MetadataSource;
SourceGeneratedFileInfo?: SourceGeneratedFileInfo;
}

export interface GoToTypeDefinitionRequest extends Request {
WantMetadata?: boolean;
}

export interface GoToTypeDefinitionResponse {
Definitions?: Definition[];
}


export namespace V2 {

export module Requests {
Expand Down
4 changes: 4 additions & 0 deletions src/omnisharp/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export async function goToDefinition(server: OmniSharpServer, request: protocol.
return server.makeRequest<protocol.V2.GoToDefinitionResponse>(protocol.V2.Requests.GoToDefinition, request, token);
}

export async function goToTypeDefinition(server: OmniSharpServer, request: protocol.GoToTypeDefinitionRequest, token: vscode.CancellationToken) {
return server.makeRequest<protocol.GoToTypeDefinitionResponse>(protocol.Requests.GoToTypeDefinition, request, token);
}

export async function getSourceGeneratedFile(server: OmniSharpServer, request: protocol.SourceGeneratedFileRequest, token: vscode.CancellationToken) {
return server.makeRequest<protocol.SourceGeneratedFileResponse>(protocol.Requests.SourceGeneratedFile, request, token);
}
Expand Down
24 changes: 24 additions & 0 deletions test/integrationTests/testAssets/singleCsproj/typeDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

using System;

namespace Test
{
public class LinkedList
{
public void MyMethod()
{
var linked = new LinkedList();
var str = "test string";
var part = new PartialClass();
Console.WriteLine(str);
}
}

public partial class PartialClass {
public string Foo {get; set;};
}

public partial class PartialClass {
public int Bar {get; set;}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

using System;

namespace Test
{
public class LinkedList
{
public void MyMethod()
{
var linked = new LinkedList();
var str = "test string";
var part = new PartialClass();
Console.WriteLine(str);
}
}

public partial class PartialClass {
public string Foo {get; set;};
}

public partial class PartialClass {
public int Bar {get; set;}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

using System;

namespace Test
{
public class LinkedList
{
public void MyMethod()
{
var linked = new LinkedList();
var str = "test string";
var part = new PartialClass();
Console.WriteLine(str);
}
}

public partial class PartialClass {
public string Foo {get; set;};
}

public partial class PartialClass {
public int Bar {get; set;}
}
}
68 changes: 68 additions & 0 deletions test/integrationTests/typeDefinitionProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from "vscode";
import CSharpDefinitionProvider from "../../src/features/definitionProvider";
import * as path from "path";
import testAssetWorkspace from "./testAssets/testAssetWorkspace";
import { expect, should } from "chai";
import { activateCSharpExtension, isRazorWorkspace, isSlnWithGenerator, restartOmniSharpServer } from './integrationHelpers';

suite(`${CSharpDefinitionProvider.name}: ${testAssetWorkspace.description}`, () => {
let fileUri: vscode.Uri;

suiteSetup(async function () {
should();

if (isRazorWorkspace(vscode.workspace) || isSlnWithGenerator(vscode.workspace)) {
this.skip();
}

const activation = await activateCSharpExtension();
await testAssetWorkspace.restore();

const fileName = 'typeDefinition.cs';
const projectDirectory = testAssetWorkspace.projects[0].projectDirectoryPath;
fileUri = vscode.Uri.file(path.join(projectDirectory, fileName));
await vscode.commands.executeCommand("vscode.open", fileUri);

await testAssetWorkspace.waitForIdle(activation.eventStream);
});

suiteTeardown(async () => {
await testAssetWorkspace.cleanupWorkspace();
});

test("Returns the type definition", async () => {
const definitionList = <vscode.Location[]>(await vscode.commands.executeCommand("vscode.executeTypeDefinitionProvider", fileUri, new vscode.Position(9, 18)));
expect(definitionList.length).to.be.equal(1);
expect(definitionList[0]).to.exist;
expect(definitionList[0].uri.path).to.contain("typeDefinition.cs");
});

test("Returns the definition from Metadata", async () => {
const omnisharpConfig = vscode.workspace.getConfiguration('omnisharp');
await omnisharpConfig.update('enableDecompilationSupport', false, vscode.ConfigurationTarget.Global);
await restartOmniSharpServer();

const definitionList = <vscode.Location[]>(await vscode.commands.executeCommand("vscode.executeTypeDefinitionProvider", fileUri, new vscode.Position(10, 18)));
expect(definitionList.length).to.be.equal(1);
expect(definitionList[0]).to.exist;
expect(definitionList[0].uri.path).to.contain("[metadata] String.cs");
});

test("Returns multiple definitions for partial types", async () => {
const definitionList = <vscode.Location[]>(await vscode.commands.executeCommand("vscode.executeTypeDefinitionProvider", fileUri, new vscode.Position(11, 18)));
expect(definitionList.length).eq(2);
expect(definitionList[0]).to.exist;
expect(definitionList[0].uri.path).to.contain("typeDefinition.cs");
expect(definitionList[1]).to.exist;
expect(definitionList[1].uri.path).to.contain("typeDefinition.cs");
});

suiteTeardown(async () => {
await testAssetWorkspace.cleanupWorkspace();
});
});