Skip to content

Commit 59b694e

Browse files
datho7561rgrunber
authored andcommitted
Translate jdt:// links in Hover to a command link
Translate all links to `jdt://` in hover documentation to a `command://` link that opens the corresponding document. This gets around a limitation of hover Markdown in VS Code, where custom schemes aren't supported in links. Closes #2810 Signed-off-by: David Thompson <[email protected]>
1 parent 75a859c commit 59b694e

File tree

8 files changed

+89
-46
lines changed

8 files changed

+89
-46
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ bin/
1515
.project
1616
test/resources/projects/**/.vscode
1717
test/resources/projects/maven/salut/testGradle
18+
test-temp

src/commands.ts

+6
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ export namespace Commands {
118118
* Open Java formatter settings
119119
*/
120120
export const OPEN_FORMATTER = 'java.open.formatter.settings';
121+
122+
/**
123+
* Open a file given the URI
124+
*/
125+
export const OPEN_FILE = 'java.open.file';
126+
121127
/**
122128
* Clean the Java language server workspace
123129
*/

src/extension.ts

+25-21
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
'use strict';
22

3-
import * as path from 'path';
4-
import * as os from 'os';
3+
import * as chokidar from 'chokidar';
54
import * as fs from 'fs';
65
import * as fse from 'fs-extra';
7-
import { workspace, extensions, ExtensionContext, window, commands, ViewColumn, Uri, languages, IndentAction, InputBoxOptions, EventEmitter, OutputChannel, TextDocument, RelativePattern, ConfigurationTarget, WorkspaceConfiguration, env, UIKind, CodeActionContext, Diagnostic, CodeActionTriggerKind, version } from 'vscode';
8-
import { ExecuteCommandParams, ExecuteCommandRequest, LanguageClientOptions, RevealOutputChannelOn, ErrorHandler, Message, ErrorAction, CloseAction, DidChangeConfigurationNotification, CancellationToken, CodeActionRequest, CodeActionParams, Command } from 'vscode-languageclient';
6+
import * as os from 'os';
7+
import * as path from 'path';
8+
import { CodeActionContext, CodeActionTriggerKind, commands, ConfigurationTarget, Diagnostic, env, EventEmitter, ExtensionContext, extensions, IndentAction, InputBoxOptions, languages, OutputChannel, RelativePattern, TextDocument, UIKind, Uri, version, ViewColumn, window, workspace, WorkspaceConfiguration } from 'vscode';
9+
import { CancellationToken, CloseAction, CodeActionParams, CodeActionRequest, Command, DidChangeConfigurationNotification, ErrorAction, ErrorHandler, ExecuteCommandParams, ExecuteCommandRequest, LanguageClientOptions, Message, RevealOutputChannelOn } from 'vscode-languageclient';
910
import { LanguageClient } from 'vscode-languageclient/node';
10-
import { collectJavaExtensions, getBundlesToReload, isContributedPartUpdated } from './plugin';
11-
import { HEAP_DUMP_LOCATION, prepareExecutable } from './javaServerStarter';
12-
import * as requirements from './requirements';
13-
import { initialize as initializeRecommendation } from './recommendation';
11+
import { apiManager } from './apiManager';
1412
import { Commands } from './commands';
15-
import { ExtensionAPI, ClientStatus } from './extension.api';
16-
import { getJavaConfiguration, deleteDirectory, getBuildFilePatterns, getInclusionPatternsFromNegatedExclusion, convertToGlob, getExclusionBlob, ensureExists } from './utils';
17-
import { onConfigurationChange, getJavaServerMode, ServerMode, ACTIVE_BUILD_TOOL_STATE, handleTextBlockClosing } from './settings';
18-
import { logger, initializeLogFile } from './log';
19-
import glob = require('glob');
20-
import { SyntaxLanguageClient } from './syntaxLanguageClient';
21-
import { registerClientProviders } from './providerDispatcher';
13+
import { ClientStatus, ExtensionAPI } from './extension.api';
2214
import * as fileEventHandler from './fileEventHandler';
23-
import { StandardLanguageClient } from './standardLanguageClient';
24-
import { apiManager } from './apiManager';
25-
import { snippetCompletionProvider } from './snippetCompletionProvider';
15+
import { HEAP_DUMP_LOCATION, prepareExecutable } from './javaServerStarter';
16+
import { initializeLogFile, logger } from './log';
17+
import { cleanupLombokCache } from "./lombokSupport";
18+
import { markdownPreviewProvider } from "./markdownPreviewProvider";
19+
import { collectJavaExtensions, getBundlesToReload, isContributedPartUpdated } from './plugin';
20+
import { registerClientProviders } from './providerDispatcher';
21+
import { initialize as initializeRecommendation } from './recommendation';
22+
import * as requirements from './requirements';
2623
import { runtimeStatusBarProvider } from './runtimeStatusBarProvider';
2724
import { serverStatusBarProvider } from './serverStatusBarProvider';
28-
import { markdownPreviewProvider } from "./markdownPreviewProvider";
29-
import * as chokidar from 'chokidar';
30-
import { cleanupLombokCache } from "./lombokSupport";
25+
import { ACTIVE_BUILD_TOOL_STATE, getJavaServerMode, handleTextBlockClosing, onConfigurationChange, ServerMode } from './settings';
26+
import { snippetCompletionProvider } from './snippetCompletionProvider';
27+
import { StandardLanguageClient } from './standardLanguageClient';
28+
import { SyntaxLanguageClient } from './syntaxLanguageClient';
29+
import { convertToGlob, deleteDirectory, ensureExists, getBuildFilePatterns, getExclusionBlob, getInclusionPatternsFromNegatedExclusion, getJavaConfiguration } from './utils';
30+
import glob = require('glob');
3131

3232
const syntaxClient: SyntaxLanguageClient = new SyntaxLanguageClient();
3333
const standardClient: StandardLanguageClient = new StandardLanguageClient();
@@ -381,6 +381,10 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
381381
context.subscriptions.push(commands.registerCommand(Commands.OPEN_LOGS, () => openLogs()));
382382

383383
context.subscriptions.push(commands.registerCommand(Commands.OPEN_FORMATTER, async () => openFormatter(context.extensionPath)));
384+
context.subscriptions.push(commands.registerCommand(Commands.OPEN_FILE, async (uri: string) => {
385+
const parsedUri = Uri.parse(uri);
386+
await window.showTextDocument(parsedUri);
387+
}));
384388

385389
context.subscriptions.push(commands.registerCommand(Commands.CLEAN_WORKSPACE, (force?: boolean) => cleanWorkspace(workspacePath, force)));
386390

src/goToDefinition.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
'use strict';
22

33
import {
4-
CancellationToken,
5-
Location,
6-
LocationLink,
7-
DefinitionParams,
8-
DefinitionRequest
4+
CancellationToken, DefinitionParams,
5+
DefinitionRequest, Location,
6+
LocationLink
97
} from 'vscode-languageclient';
108
import { LanguageClient } from 'vscode-languageclient/node';
119
import { getActiveLanguageClient } from './extension';

src/providerDispatcher.ts

+41-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
'use strict';
22

3-
import { DocumentSymbolRequest, SymbolInformation as clientSymbolInformation, DocumentSymbol as clientDocumentSymbol, HoverRequest, WorkspaceSymbolRequest } from "vscode-languageclient";
3+
import { CancellationToken, commands, DocumentSymbol, DocumentSymbolProvider, Event, ExtensionContext, Hover, HoverProvider, languages, MarkdownString, MarkedString, Position, Range, SymbolInformation, SymbolKind, TextDocument, TextDocumentContentProvider, Uri, workspace, WorkspaceSymbolProvider } from "vscode";
4+
import { DocumentSymbol as clientDocumentSymbol, DocumentSymbolRequest, HoverRequest, SymbolInformation as clientSymbolInformation, WorkspaceSymbolRequest } from "vscode-languageclient";
45
import { LanguageClient } from "vscode-languageclient/node";
5-
import { ExtensionContext, languages, DocumentSymbolProvider, TextDocument, CancellationToken, SymbolInformation, DocumentSymbol, TextDocumentContentProvider, workspace, Uri, Event, HoverProvider, Position, Hover, WorkspaceSymbolProvider, Range, commands, SymbolKind } from "vscode";
6-
import { ClassFileContentsRequest, StatusNotification } from "./protocol";
7-
import { createClientHoverProvider } from "./hoverAction";
8-
import { getActiveLanguageClient } from "./extension";
96
import { apiManager } from "./apiManager";
10-
import { ServerMode } from "./settings";
11-
import { serverStatus, ServerStatusKind } from "./serverStatus";
127
import { Commands } from "./commands";
8+
import { getActiveLanguageClient } from "./extension";
9+
import { createClientHoverProvider } from "./hoverAction";
10+
import { ClassFileContentsRequest } from "./protocol";
11+
import { ServerMode } from "./settings";
1312

1413
export interface ProviderOptions {
1514
contentProviderEvent: Event<Uri>;
@@ -51,14 +50,16 @@ export class ClientHoverProvider implements HoverProvider {
5150
if (!this.delegateProvider) {
5251
this.delegateProvider = createClientHoverProvider(languageClient);
5352
}
54-
return this.delegateProvider.provideHover(document, position, token);
53+
const hover = await this.delegateProvider.provideHover(document, position, token);
54+
return fixJdtSchemeHoverLinks(hover);
5555
} else {
5656
const params = {
5757
textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document),
5858
position: languageClient.code2ProtocolConverter.asPosition(position)
5959
};
6060
const hoverResponse = await languageClient.sendRequest(HoverRequest.type, params, token);
61-
return languageClient.protocol2CodeConverter.asHover(hoverResponse);
61+
const hover = languageClient.protocol2CodeConverter.asHover(hoverResponse);
62+
return fixJdtSchemeHoverLinks(hover);
6263
}
6364
}
6465
}
@@ -170,4 +171,35 @@ function overwriteWorkspaceSymbolProvider(context: ExtensionContext): void {
170171
}
171172
}
172173
});
174+
}
175+
176+
const REPLACE_JDT_LINKS_PATTERN: RegExp = /(\[(?:[^\]])+\]\()(jdt:\/\/(?:(?:(?:\\\))|([^)]))+))\)/g;
177+
178+
/**
179+
* Returns the hover with all jdt:// links replaced with a command:// link that opens the jdt URI.
180+
*
181+
* VS Code doesn't render links with the `jdt` scheme in hover popups.
182+
* To get around this, you can create a command:// link that invokes a command that opens the corresponding URI.
183+
* VS Code will render command:// links in hover pop ups if they are marked as trusted.
184+
*
185+
* @param hover The hover to fix the jdt:// links for
186+
* @returns the hover with all jdt:// links replaced with a command:// link that opens the jdt URI
187+
*/
188+
function fixJdtSchemeHoverLinks(hover: Hover): Hover {
189+
const newContents: (MarkedString|MarkdownString)[] = [];
190+
for (const content of hover.contents) {
191+
if (content instanceof MarkdownString) {
192+
const newContent: string = (<MarkdownString>content).value.replace(REPLACE_JDT_LINKS_PATTERN, (_substring, group1, group2) => {
193+
const uri = `command:${Commands.OPEN_FILE}?${encodeURI(JSON.stringify([encodeURIComponent(group2)]))}`;
194+
return `${group1}${uri})`;
195+
});
196+
const mdString = new MarkdownString(newContent);
197+
mdString.isTrusted = true;
198+
newContents.push(mdString);
199+
} else {
200+
newContents.push(content);
201+
}
202+
}
203+
hover.contents = newContents;
204+
return hover;
173205
}

src/syntaxLanguageClient.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
'use strict';
22

33
import * as net from "net";
4-
import { LanguageClientOptions, DidChangeConfigurationNotification } from "vscode-languageclient";
5-
import { LanguageClient, StreamInfo, ServerOptions } from "vscode-languageclient/node";
6-
import { OutputInfoCollector, ClientErrorHandler, getJavaConfig } from "./extension";
4+
import { DidChangeConfigurationNotification, LanguageClientOptions } from "vscode-languageclient";
5+
import { LanguageClient, ServerOptions, StreamInfo } from "vscode-languageclient/node";
6+
import { apiManager } from "./apiManager";
7+
import { ClientErrorHandler, getJavaConfig, OutputInfoCollector } from "./extension";
8+
import { ClientStatus, ExtensionAPI } from "./extension.api";
79
import { logger } from "./log";
8-
import { ServerMode } from "./settings";
910
import { StatusNotification } from "./protocol";
10-
import { apiManager } from "./apiManager";
11-
import { ExtensionAPI, ClientStatus } from "./extension.api";
11+
import { ServerMode } from "./settings";
1212
import { snippetCompletionProvider } from "./snippetCompletionProvider";
1313

1414
const extensionName = "Language Support for Java (Syntax Server)";

test/lightweight-mode-suite/extension.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as assert from 'assert';
22
import * as vscode from 'vscode';
3-
import { Commands } from '../../src/commands';
43
import { extensions } from 'vscode';
4+
import { Commands } from '../../src/commands';
55

66
suite('Java Language Extension - LightWeight', () => {
77

@@ -22,6 +22,7 @@ suite('Java Language Extension - LightWeight', () => {
2222
Commands.OPEN_FORMATTER,
2323
Commands.CLEAN_WORKSPACE,
2424
Commands.SWITCH_SERVER_MODE,
25+
Commands.OPEN_FILE,
2526
].sort();
2627
const foundJavaCommands = commands.filter((value) => {
2728
return JAVA_COMMANDS.indexOf(value)>=0 || value.startsWith('java.');

test/standard-mode-suite/extension.test.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as assert from 'assert';
2-
import * as vscode from 'vscode';
32
import * as fs from 'fs';
43
import * as path from 'path';
5-
import * as plugin from '../../src/plugin';
4+
import { env } from 'process';
5+
import * as vscode from 'vscode';
6+
import { Commands } from '../../src/commands';
67
import * as java from '../../src/javaServerStarter';
8+
import * as plugin from '../../src/plugin';
79
import * as requirements from '../../src/requirements';
8-
import { Commands } from '../../src/commands';
9-
import { env } from 'process';
1010

1111
suite('Java Language Extension - Standard', () => {
1212

@@ -87,6 +87,7 @@ suite('Java Language Extension - Standard', () => {
8787
Commands.SHOW_SUPERTYPE_HIERARCHY,
8888
Commands.SHOW_CLASS_HIERARCHY,
8989
Commands.LEARN_MORE_ABOUT_CLEAN_UPS,
90+
Commands.OPEN_FILE,
9091
].sort();
9192
const foundJavaCommands = commands.filter((value) => {
9293
return JAVA_COMMANDS.indexOf(value)>=0 || value.startsWith('java.');

0 commit comments

Comments
 (0)