diff --git a/src/features/definitionProvider.ts b/src/features/definitionProvider.ts index 604bf1169..1d8e84256 100644 --- a/src/features/definitionProvider.ts +++ b/src/features/definitionProvider.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ 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'; @@ -13,7 +13,7 @@ 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, @@ -23,76 +23,89 @@ export default class CSharpDefinitionProvider extends AbstractSupport implements } public async provideDefinition(document: TextDocument, position: Position, token: CancellationToken): Promise { - let req = 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 { + let req = 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, { - Timeout: 5000, - AssemblyName: metadataSource.AssemblyName, - VersionNumber: metadataSource.VersionNumber, - ProjectName: metadataSource.ProjectName, - Language: metadataSource.Language, - TypeName: metadataSource.TypeName - }); + private async GetLocationsFromResponse (response: GoToTypeDefinitionResponse | V2.GoToDefinitionResponse, token: CancellationToken): Promise + { + 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, { + 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); } } diff --git a/src/omnisharp/extension.ts b/src/omnisharp/extension.ts index d5c5894e8..d7add1c56 100644 --- a/src/omnisharp/extension.ts +++ b/src/omnisharp/extension.ts @@ -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))); diff --git a/src/omnisharp/prioritization.ts b/src/omnisharp/prioritization.ts index 1484947bf..775e17336 100644 --- a/src/omnisharp/prioritization.ts +++ b/src/omnisharp/prioritization.ts @@ -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(priorityCommands); diff --git a/src/omnisharp/protocol.ts b/src/omnisharp/protocol.ts index 3252ffa5a..f028cf427 100644 --- a/src/omnisharp/protocol.ts +++ b/src/omnisharp/protocol.ts @@ -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'; @@ -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 { diff --git a/src/omnisharp/utils.ts b/src/omnisharp/utils.ts index 3b07e55be..0bd5d5589 100644 --- a/src/omnisharp/utils.ts +++ b/src/omnisharp/utils.ts @@ -67,6 +67,10 @@ export async function goToDefinition(server: OmniSharpServer, request: protocol. return server.makeRequest(protocol.V2.Requests.GoToDefinition, request, token); } +export async function goToTypeDefinition(server: OmniSharpServer, request: protocol.GoToTypeDefinitionRequest, token: vscode.CancellationToken) { + return server.makeRequest(protocol.Requests.GoToTypeDefinition, request, token); +} + export async function getSourceGeneratedFile(server: OmniSharpServer, request: protocol.SourceGeneratedFileRequest, token: vscode.CancellationToken) { return server.makeRequest(protocol.Requests.SourceGeneratedFile, request, token); } diff --git a/test/integrationTests/testAssets/singleCsproj/typeDefinition.cs b/test/integrationTests/testAssets/singleCsproj/typeDefinition.cs new file mode 100644 index 000000000..addee4fde --- /dev/null +++ b/test/integrationTests/testAssets/singleCsproj/typeDefinition.cs @@ -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;} + } +} diff --git a/test/integrationTests/testAssets/slnFilterWithCsproj/src/app/typeDefinition.cs b/test/integrationTests/testAssets/slnFilterWithCsproj/src/app/typeDefinition.cs new file mode 100644 index 000000000..addee4fde --- /dev/null +++ b/test/integrationTests/testAssets/slnFilterWithCsproj/src/app/typeDefinition.cs @@ -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;} + } +} diff --git a/test/integrationTests/testAssets/slnWithCsproj/src/app/typeDefinition.cs b/test/integrationTests/testAssets/slnWithCsproj/src/app/typeDefinition.cs new file mode 100644 index 000000000..addee4fde --- /dev/null +++ b/test/integrationTests/testAssets/slnWithCsproj/src/app/typeDefinition.cs @@ -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;} + } +} diff --git a/test/integrationTests/typeDefinitionProvider.test.ts b/test/integrationTests/typeDefinitionProvider.test.ts new file mode 100644 index 000000000..582274c70 --- /dev/null +++ b/test/integrationTests/typeDefinitionProvider.test.ts @@ -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 = (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 = (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 = (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(); + }); +});