Skip to content

Commit 6671c28

Browse files
testforstephenfbricon
authored andcommitted
Use hover to support 'Go To Super Implementation' action (#1053)
Signed-off-by: Jinbo Wang <jinbwan@microsoft.com>
1 parent a24c5e4 commit 6671c28

6 files changed

Lines changed: 159 additions & 4 deletions

File tree

src/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,8 @@ export namespace Commands {
156156
* Rename Command.
157157
*/
158158
export const RENAME_COMMAND = 'java.action.rename';
159+
/**
160+
* Navigate To Super Method Command.
161+
*/
162+
export const NAVIGATE_TO_SUPER_IMPLEMENTATION_COMMAND = 'java.action.navigateToSuperImplementation';
159163
}

src/extension.api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { RequirementsData } from './requirements';
2+
import { TextDocumentPositionParams } from 'vscode-languageclient';
3+
import { CancellationToken, Command, ProviderResult } from 'vscode';
4+
5+
export type provideHoverCommandFn = (params: TextDocumentPositionParams, token: CancellationToken) => ProviderResult<Command[] | undefined>;
6+
export type registerHoverCommand = (callback: provideHoverCommandFn) => void;
27

38
export interface ExtensionAPI {
49
readonly apiVersion: string;
510
readonly javaRequirement: RequirementsData;
611
readonly status: "Started" | "Error";
12+
readonly registerHoverCommand: registerHoverCommand;
713
}

src/extension.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from './protocol';
1616
import { ExtensionAPI } from './extension.api';
1717
import * as buildpath from './buildpath';
18+
import * as hoverAction from './hoverAction';
1819
import * as sourceAction from './sourceAction';
1920
import * as refactorAction from './refactorAction';
2021
import * as net from 'net';
@@ -161,6 +162,7 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
161162
generateDelegateMethodsPromptSupport: true,
162163
advancedExtractRefactoringSupport: true,
163164
moveRefactoringSupport: true,
165+
clientHoverProvider: true,
164166
},
165167
triggerFiles: getTriggerFiles()
166168
},
@@ -203,6 +205,7 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
203205
// Create the language client and start the client.
204206
languageClient = new LanguageClient('java', extensionName, serverOptions, clientOptions);
205207
languageClient.registerProposedFeatures();
208+
const registerHoverCommand = hoverAction.registerClientHoverProvider(languageClient, context);
206209

207210
languageClient.onReady().then(() => {
208211
languageClient.onNotification(StatusNotification.type, (report) => {
@@ -215,7 +218,8 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
215218
resolve({
216219
apiVersion: '0.2',
217220
javaRequirement: requirements,
218-
status: report.type
221+
status: report.type,
222+
registerHoverCommand,
219223
});
220224
break;
221225
case 'Error':
@@ -226,7 +230,8 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
226230
resolve({
227231
apiVersion: '0.2',
228232
javaRequirement: requirements,
229-
status: report.type
233+
status: report.type,
234+
registerHoverCommand,
230235
});
231236
break;
232237
case 'Starting':

src/hoverAction.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
'use strict';
2+
3+
import { commands, ExtensionContext, HoverProvider, languages, CancellationToken, Hover, Position, TextDocument, MarkdownString, window, Uri, MarkedString, Command } from "vscode";
4+
import { LanguageClient, TextDocumentPositionParams, HoverRequest } from "vscode-languageclient";
5+
import { Commands as javaCommands } from "./commands";
6+
import { FindLinks } from "./protocol";
7+
import { registerHoverCommand, provideHoverCommandFn } from "./extension.api";
8+
import { logger } from "./log";
9+
10+
export function registerClientHoverProvider(languageClient: LanguageClient, context: ExtensionContext): registerHoverCommand {
11+
const hoverProvider: JavaHoverProvider = new JavaHoverProvider(languageClient);
12+
hoverProvider.registerHoverCommand(async (params: TextDocumentPositionParams, token: CancellationToken) => {
13+
return await provideHoverCommand(languageClient, params, token);
14+
});
15+
context.subscriptions.push(languages.registerHoverProvider('java', hoverProvider));
16+
context.subscriptions.push(commands.registerCommand(javaCommands.NAVIGATE_TO_SUPER_IMPLEMENTATION_COMMAND, (location: any) => {
17+
navigateToSuperImplementation(languageClient, location);
18+
}));
19+
20+
return hoverProvider.registerHoverCommand;
21+
}
22+
23+
async function provideHoverCommand(languageClient: LanguageClient, params: TextDocumentPositionParams, token: CancellationToken): Promise<Command[] | undefined> {
24+
const response = await languageClient.sendRequest(FindLinks.type, {
25+
type: 'superImplementation',
26+
position: params,
27+
}, token);
28+
if (response && response.length) {
29+
const location = response[0];
30+
let tooltip;
31+
if (location.kind === 'method') {
32+
tooltip = `Go to super method '${location.displayName}'`;
33+
} else {
34+
tooltip = `Go to super implementation '${location.displayName}'`;
35+
}
36+
37+
return [{
38+
title: 'Go to Super Implementation',
39+
command: javaCommands.NAVIGATE_TO_SUPER_IMPLEMENTATION_COMMAND,
40+
tooltip,
41+
arguments: [{
42+
uri: encodeBase64(location.uri),
43+
range: location.range,
44+
}],
45+
}];
46+
}
47+
}
48+
49+
function navigateToSuperImplementation(languageClient: LanguageClient, location: any) {
50+
const range = languageClient.protocol2CodeConverter.asRange(location.range);
51+
window.showTextDocument(Uri.parse(decodeBase64(location.uri)), {
52+
preserveFocus: true,
53+
selection: range,
54+
});
55+
}
56+
57+
function encodeBase64(text: string): string {
58+
return Buffer.from(text).toString('base64');
59+
}
60+
61+
function decodeBase64(text: string): string {
62+
return Buffer.from(text, 'base64').toString('ascii');
63+
}
64+
65+
class JavaHoverProvider implements HoverProvider {
66+
private _hoverRegistry: provideHoverCommandFn[] = [];
67+
68+
constructor(readonly languageClient: LanguageClient) {
69+
}
70+
71+
async provideHover(document: TextDocument, position: Position, token: CancellationToken): Promise<Hover> {
72+
const params = {
73+
textDocument: this.languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document),
74+
position: this.languageClient.code2ProtocolConverter.asPosition(position),
75+
};
76+
77+
// Fetch the javadoc from Java language server.
78+
const hoverResponse = await this.languageClient.sendRequest(HoverRequest.type, params, token);
79+
const serverHover = this.languageClient.protocol2CodeConverter.asHover(hoverResponse);
80+
81+
// Fetch the contributed hover commands from third party extensions.
82+
const contributedCommands: Command[] = await this.getContributedHoverCommands(params, token);
83+
if (!contributedCommands.length) {
84+
return serverHover;
85+
}
86+
87+
const contributed = new MarkdownString(contributedCommands.map((command) => this.convertCommandToMarkdown(command)).join(' | '));
88+
contributed.isTrusted = true;
89+
let contents: MarkedString[] = [ contributed ];
90+
let range;
91+
if (serverHover && serverHover.contents) {
92+
contents = contents.concat(serverHover.contents);
93+
range = serverHover.range;
94+
}
95+
return new Hover(contents, range);
96+
}
97+
98+
registerHoverCommand(callback: provideHoverCommandFn) {
99+
this._hoverRegistry.push(callback);
100+
}
101+
102+
private async getContributedHoverCommands(params: TextDocumentPositionParams, token: CancellationToken): Promise<Command[]> {
103+
const contributedCommands: Command[] = [];
104+
for (const provideFn of this._hoverRegistry) {
105+
try {
106+
if (token.isCancellationRequested) {
107+
break;
108+
}
109+
110+
const commands = (await provideFn(params, token)) || [];
111+
commands.forEach((command) => {
112+
contributedCommands.push(command);
113+
});
114+
} catch (error) {
115+
logger.error(`Failed to provide hover command ${String(error)}`);
116+
}
117+
}
118+
119+
return contributedCommands;
120+
}
121+
122+
private convertCommandToMarkdown(command: Command): string {
123+
return `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify(command.arguments || []))} "${command.tooltip || command.command}")`;
124+
}
125+
}

src/protocol.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
import { RequestType, NotificationType, TextDocumentIdentifier, ExecuteCommandParams, CodeActionParams, WorkspaceEdit, FormattingOptions, WorkspaceSymbolParams, SymbolInformation } from 'vscode-languageclient';
3+
import { RequestType, NotificationType, TextDocumentIdentifier, ExecuteCommandParams, CodeActionParams, WorkspaceEdit, FormattingOptions, WorkspaceSymbolParams, SymbolInformation, TextDocumentPositionParams, Location } from 'vscode-languageclient';
44
import { Command, Range } from 'vscode';
55

66
/**
@@ -342,3 +342,17 @@ export interface SearchSymbolParams extends WorkspaceSymbolParams {
342342
export namespace SearchSymbols {
343343
export const type = new RequestType<SearchSymbolParams, SymbolInformation[], void, void>('java/searchSymbols');
344344
}
345+
346+
export interface FindLinksParams {
347+
type: string;
348+
position: TextDocumentPositionParams;
349+
}
350+
351+
export interface LinkLocation extends Location {
352+
displayName: string;
353+
kind: string;
354+
}
355+
356+
export namespace FindLinks {
357+
export const type = new RequestType<FindLinksParams, LinkLocation[], void, void>('java/findLinks');
358+
}

test/extension.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ suite('Java Language Extension', () => {
5353
Commands.GENERATE_CONSTRUCTORS_PROMPT,
5454
Commands.GENERATE_DELEGATE_METHODS_PROMPT,
5555
Commands.APPLY_REFACTORING_COMMAND,
56-
Commands.RENAME_COMMAND
56+
Commands.RENAME_COMMAND,
57+
Commands.NAVIGATE_TO_SUPER_IMPLEMENTATION_COMMAND
5758
];
5859
const foundJavaCommands = commands.filter((value) => {
5960
return JAVA_COMMANDS.indexOf(value)>=0 || value.startsWith('java.');

0 commit comments

Comments
 (0)