mirror of
https://github.com/Microsoft/vscode
synced 2024-10-12 06:17:18 +00:00
Merge pull request #169382 from microsoft/joh/link-opener
joh/link opener
This commit is contained in:
commit
9e8d78012d
|
@ -32,6 +32,7 @@ import { getDefinitionsAtPosition } from '../goToSymbol';
|
|||
import { IWordAtPosition } from 'vs/editor/common/core/wordHelper';
|
||||
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
|
||||
import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel';
|
||||
import { LinkOpener } from 'vs/editor/contrib/links/browser/links';
|
||||
|
||||
export class GotoDefinitionAtPositionEditorContribution implements IEditorContribution {
|
||||
|
||||
|
@ -62,14 +63,21 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri
|
|||
}));
|
||||
|
||||
this.toUnhook.add(linkGesture.onExecute((mouseEvent: ClickLinkMouseEvent) => {
|
||||
if (this.isEnabled(mouseEvent)) {
|
||||
this.gotoDefinition(mouseEvent.target.position!, mouseEvent.hasSideBySideModifier).then(() => {
|
||||
this.removeLinkDecorations();
|
||||
}, (error: Error) => {
|
||||
this.removeLinkDecorations();
|
||||
onUnexpectedError(error);
|
||||
});
|
||||
if (!this.isEnabled(mouseEvent)) {
|
||||
return;
|
||||
}
|
||||
LinkOpener.get(this.editor)?.openLink(
|
||||
mouseEvent.target.range!,
|
||||
nls.localize('link.label', "Go to Definition"),
|
||||
() => {
|
||||
this.gotoDefinition(mouseEvent.target.position!, mouseEvent.hasSideBySideModifier)
|
||||
.catch((error: Error) => {
|
||||
onUnexpectedError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.removeLinkDecorations();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
this.toUnhook.add(linkGesture.onCancel(() => {
|
||||
|
@ -82,33 +90,30 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri
|
|||
return editor.getContribution<GotoDefinitionAtPositionEditorContribution>(GotoDefinitionAtPositionEditorContribution.ID);
|
||||
}
|
||||
|
||||
startFindDefinitionFromCursor(position: Position) {
|
||||
async startFindDefinitionFromCursor(position: Position) {
|
||||
// For issue: https://github.com/microsoft/vscode/issues/46257
|
||||
// equivalent to mouse move with meta/ctrl key
|
||||
|
||||
// First find the definition and add decorations
|
||||
// to the editor to be shown with the content hover widget
|
||||
return this.startFindDefinition(position).then(() => {
|
||||
|
||||
// Add listeners for editor cursor move and key down events
|
||||
// Dismiss the "extended" editor decorations when the user hides
|
||||
// the hover widget. There is no event for the widget itself so these
|
||||
// serve as a best effort. After removing the link decorations, the hover
|
||||
// widget is clean and will only show declarations per next request.
|
||||
this.toUnhookForKeyboard.add(this.editor.onDidChangeCursorPosition(() => {
|
||||
await this.startFindDefinition(position);
|
||||
// Add listeners for editor cursor move and key down events
|
||||
// Dismiss the "extended" editor decorations when the user hides
|
||||
// the hover widget. There is no event for the widget itself so these
|
||||
// serve as a best effort. After removing the link decorations, the hover
|
||||
// widget is clean and will only show declarations per next request.
|
||||
this.toUnhookForKeyboard.add(this.editor.onDidChangeCursorPosition(() => {
|
||||
this.currentWordAtPosition = null;
|
||||
this.removeLinkDecorations();
|
||||
this.toUnhookForKeyboard.clear();
|
||||
}));
|
||||
this.toUnhookForKeyboard.add(this.editor.onKeyDown((e: IKeyboardEvent) => {
|
||||
if (e) {
|
||||
this.currentWordAtPosition = null;
|
||||
this.removeLinkDecorations();
|
||||
this.toUnhookForKeyboard.clear();
|
||||
}));
|
||||
|
||||
this.toUnhookForKeyboard.add(this.editor.onKeyDown((e: IKeyboardEvent) => {
|
||||
if (e) {
|
||||
this.currentWordAtPosition = null;
|
||||
this.removeLinkDecorations();
|
||||
this.toUnhookForKeyboard.clear();
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private startFindDefinitionFromMouse(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): void {
|
||||
|
@ -129,7 +134,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri
|
|||
this.startFindDefinition(position);
|
||||
}
|
||||
|
||||
private startFindDefinition(position: Position): Promise<number | undefined> {
|
||||
private async startFindDefinition(position: Position): Promise<void> {
|
||||
|
||||
// Dispose listeners for updating decorations when using keyboard to show definition hover
|
||||
this.toUnhookForKeyboard.clear();
|
||||
|
@ -139,12 +144,12 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri
|
|||
if (!word) {
|
||||
this.currentWordAtPosition = null;
|
||||
this.removeLinkDecorations();
|
||||
return Promise.resolve(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Return early if word at position is still the same
|
||||
if (this.currentWordAtPosition && this.currentWordAtPosition.startColumn === word.startColumn && this.currentWordAtPosition.endColumn === word.endColumn && this.currentWordAtPosition.word === word.word) {
|
||||
return Promise.resolve(0);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentWordAtPosition = word;
|
||||
|
@ -159,66 +164,71 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri
|
|||
|
||||
this.previousPromise = createCancelablePromise(token => this.findDefinition(position, token));
|
||||
|
||||
return this.previousPromise.then(results => {
|
||||
if (!results || !results.length || !state.validate(this.editor)) {
|
||||
this.removeLinkDecorations();
|
||||
let results: LocationLink[] | null;
|
||||
try {
|
||||
results = await this.previousPromise;
|
||||
|
||||
} catch (error) {
|
||||
onUnexpectedError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results || !results.length || !state.validate(this.editor)) {
|
||||
this.removeLinkDecorations();
|
||||
return;
|
||||
}
|
||||
|
||||
const linkRange = results[0].originSelectionRange
|
||||
? Range.lift(results[0].originSelectionRange)
|
||||
: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);
|
||||
|
||||
// Multiple results
|
||||
if (results.length > 1) {
|
||||
|
||||
let combinedRange = linkRange;
|
||||
for (const { originSelectionRange } of results) {
|
||||
if (originSelectionRange) {
|
||||
combinedRange = Range.plusRange(combinedRange, originSelectionRange);
|
||||
}
|
||||
}
|
||||
|
||||
this.addDecoration(
|
||||
combinedRange,
|
||||
new MarkdownString().appendText(nls.localize('multipleResults', "Click to show {0} definitions.", results.length))
|
||||
);
|
||||
} else {
|
||||
// Single result
|
||||
const result = results[0];
|
||||
|
||||
if (!result.uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkRange = results[0].originSelectionRange
|
||||
? Range.lift(results[0].originSelectionRange)
|
||||
: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);
|
||||
this.textModelResolverService.createModelReference(result.uri).then(ref => {
|
||||
|
||||
// Multiple results
|
||||
if (results.length > 1) {
|
||||
|
||||
let combinedRange = linkRange;
|
||||
for (const { originSelectionRange } of results) {
|
||||
if (originSelectionRange) {
|
||||
combinedRange = Range.plusRange(combinedRange, originSelectionRange);
|
||||
}
|
||||
}
|
||||
|
||||
this.addDecoration(
|
||||
combinedRange,
|
||||
new MarkdownString().appendText(nls.localize('multipleResults', "Click to show {0} definitions.", results.length))
|
||||
);
|
||||
}
|
||||
|
||||
// Single result
|
||||
else {
|
||||
const result = results[0];
|
||||
|
||||
if (!result.uri) {
|
||||
if (!ref.object || !ref.object.textEditorModel) {
|
||||
ref.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
this.textModelResolverService.createModelReference(result.uri).then(ref => {
|
||||
const { object: { textEditorModel } } = ref;
|
||||
const { startLineNumber } = result.range;
|
||||
|
||||
if (!ref.object || !ref.object.textEditorModel) {
|
||||
ref.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
const { object: { textEditorModel } } = ref;
|
||||
const { startLineNumber } = result.range;
|
||||
|
||||
if (startLineNumber < 1 || startLineNumber > textEditorModel.getLineCount()) {
|
||||
// invalid range
|
||||
ref.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
const previewValue = this.getPreviewValue(textEditorModel, startLineNumber, result);
|
||||
const languageId = this.languageService.guessLanguageIdByFilepathOrFirstLine(textEditorModel.uri);
|
||||
this.addDecoration(
|
||||
linkRange,
|
||||
previewValue ? new MarkdownString().appendCodeblock(languageId ? languageId : '', previewValue) : undefined
|
||||
);
|
||||
if (startLineNumber < 1 || startLineNumber > textEditorModel.getLineCount()) {
|
||||
// invalid range
|
||||
ref.dispose();
|
||||
});
|
||||
}
|
||||
}).then(undefined, onUnexpectedError);
|
||||
return;
|
||||
}
|
||||
|
||||
const previewValue = this.getPreviewValue(textEditorModel, startLineNumber, result);
|
||||
const languageId = this.languageService.guessLanguageIdByFilepathOrFirstLine(textEditorModel.uri);
|
||||
this.addDecoration(
|
||||
linkRange,
|
||||
previewValue ? new MarkdownString().appendCodeblock(languageId ? languageId : '', previewValue) : undefined
|
||||
);
|
||||
ref.dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getPreviewValue(textEditorModel: ITextModel, startLineNumber: number, result: LocationLink) {
|
||||
|
@ -314,6 +324,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri
|
|||
|
||||
public dispose(): void {
|
||||
this.toUnhook.dispose();
|
||||
this.toUnhookForKeyboard.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,21 +3,27 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { toAction } from 'vs/base/common/actions';
|
||||
import { coalesceInPlace } from 'vs/base/common/arrays';
|
||||
import { createCancelablePromise, CancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { LinkedList } from 'vs/base/common/linkedList';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
import { assertType } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import 'vs/css!./links';
|
||||
import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry';
|
||||
import { LinkProvider } from 'vs/editor/common/languages';
|
||||
|
@ -28,6 +34,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat
|
|||
import { ClickLinkGesture, ClickLinkKeyboardEvent, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture';
|
||||
import { getLinks, Link, LinksList } from 'vs/editor/contrib/links/browser/getLinks';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
|
||||
|
@ -215,50 +222,50 @@ export class LinkDetector extends Disposable implements IEditorContribution {
|
|||
}
|
||||
|
||||
public openLinkOccurrence(occurrence: LinkOccurrence, openToSide: boolean, fromUserGesture = false): void {
|
||||
|
||||
if (!this.openerService) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { link } = occurrence;
|
||||
|
||||
link.resolve(CancellationToken.None).then(uri => {
|
||||
LinkOpener.get(this.editor)?.openLink(link.range, nls.localize('open', "Open Link"), () => {
|
||||
|
||||
// Support for relative file URIs of the shape file://./relativeFile.txt or file:///./relativeFile.txt
|
||||
if (typeof uri === 'string' && this.editor.hasModel()) {
|
||||
const modelUri = this.editor.getModel().uri;
|
||||
if (modelUri.scheme === Schemas.file && uri.startsWith(`${Schemas.file}:`)) {
|
||||
const parsedUri = URI.parse(uri);
|
||||
if (parsedUri.scheme === Schemas.file) {
|
||||
const fsPath = resources.originalFSPath(parsedUri);
|
||||
link.resolve(CancellationToken.None).then(uri => {
|
||||
|
||||
let relativePath: string | null = null;
|
||||
if (fsPath.startsWith('/./')) {
|
||||
relativePath = `.${fsPath.substr(1)}`;
|
||||
} else if (fsPath.startsWith('//./')) {
|
||||
relativePath = `.${fsPath.substr(2)}`;
|
||||
}
|
||||
// Support for relative file URIs of the shape file://./relativeFile.txt or file:///./relativeFile.txt
|
||||
if (typeof uri === 'string' && this.editor.hasModel()) {
|
||||
const modelUri = this.editor.getModel().uri;
|
||||
if (modelUri.scheme === Schemas.file && uri.startsWith(`${Schemas.file}:`)) {
|
||||
const parsedUri = URI.parse(uri);
|
||||
if (parsedUri.scheme === Schemas.file) {
|
||||
const fsPath = resources.originalFSPath(parsedUri);
|
||||
|
||||
if (relativePath) {
|
||||
uri = resources.joinPath(modelUri, relativePath);
|
||||
let relativePath: string | null = null;
|
||||
if (fsPath.startsWith('/./')) {
|
||||
relativePath = `.${fsPath.substr(1)}`;
|
||||
} else if (fsPath.startsWith('//./')) {
|
||||
relativePath = `.${fsPath.substr(2)}`;
|
||||
}
|
||||
|
||||
if (relativePath) {
|
||||
uri = resources.joinPath(modelUri, relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true, allowCommands: true, fromWorkspace: true });
|
||||
|
||||
}, err => {
|
||||
const messageOrError =
|
||||
err instanceof Error ? (<Error>err).message : err;
|
||||
// different error cases
|
||||
if (messageOrError === 'invalid') {
|
||||
this.notificationService.warn(nls.localize('invalid.url', 'Failed to open this link because it is not well-formed: {0}', link.url!.toString()));
|
||||
} else if (messageOrError === 'missing') {
|
||||
this.notificationService.warn(nls.localize('missing.url', 'Failed to open this link because its target is missing.'));
|
||||
} else {
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
|
||||
return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true, allowCommands: true, fromWorkspace: true });
|
||||
|
||||
}, err => {
|
||||
const messageOrError =
|
||||
err instanceof Error ? (<Error>err).message : err;
|
||||
// different error cases
|
||||
if (messageOrError === 'invalid') {
|
||||
this.notificationService.warn(nls.localize('invalid.url', 'Failed to open this link because it is not well-formed: {0}', link.url!.toString()));
|
||||
} else if (messageOrError === 'missing') {
|
||||
this.notificationService.warn(nls.localize('missing.url', 'Failed to open this link because its target is missing.'));
|
||||
} else {
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -391,6 +398,78 @@ function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString {
|
|||
}
|
||||
}
|
||||
|
||||
export class LinkOpener extends Disposable implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.linkOpener';
|
||||
|
||||
public static get(editor: ICodeEditor): LinkOpener | null {
|
||||
return editor.getContribution<LinkOpener>(LinkOpener.ID);
|
||||
}
|
||||
|
||||
private readonly links = new LinkedList<{ range: IRange; label: string; callback: () => void }>();
|
||||
|
||||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
openLink(range: IRange, label: string, callback: () => void): void {
|
||||
this.links.push({ range, label, callback });
|
||||
if (this.links.size === 1) {
|
||||
queueMicrotask(() => this.openLinks());
|
||||
}
|
||||
}
|
||||
|
||||
private openLinks(): void {
|
||||
if (!this.editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = [...this.links];
|
||||
this.links.clear();
|
||||
|
||||
// remove links that don't overlap, late links win over early links
|
||||
for (let i = 1; i < links.length; i++) {
|
||||
const link = links[i];
|
||||
const prevLink = links[i - 1];
|
||||
if (!Range.areIntersecting(link.range, prevLink.range)) {
|
||||
links[i - 1] = undefined!;
|
||||
}
|
||||
}
|
||||
coalesceInPlace(links);
|
||||
|
||||
if (links.length === 1) {
|
||||
// single: just invoke callback
|
||||
links[0].callback();
|
||||
|
||||
} else if (links.length > 1) {
|
||||
// multiple: show context menu
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => {
|
||||
assertType(this.editor.hasModel());
|
||||
const position = Range.getStartPosition(links[0].range);
|
||||
const cursorCoords = this.editor.getScrolledVisiblePosition(position);
|
||||
// Translate to absolute editor position
|
||||
const editorCoords = dom.getDomNodePagePosition(this.editor.getDomNode());
|
||||
const posx = editorCoords.left + cursorCoords.left;
|
||||
const posy = editorCoords.top + cursorCoords.top + cursorCoords.height;
|
||||
return { x: posx, y: posy };
|
||||
},
|
||||
getActions: () => {
|
||||
return links.map(item => toAction({
|
||||
id: '',
|
||||
label: item.label,
|
||||
run: item.callback
|
||||
}));
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OpenLinkAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
|
@ -402,7 +481,7 @@ class OpenLinkAction extends EditorAction {
|
|||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
const linkDetector = LinkDetector.get(editor);
|
||||
if (!linkDetector) {
|
||||
return;
|
||||
|
@ -422,4 +501,5 @@ class OpenLinkAction extends EditorAction {
|
|||
}
|
||||
|
||||
registerEditorContribution(LinkDetector.ID, LinkDetector, EditorContributionInstantiation.AfterFirstRender);
|
||||
registerEditorContribution(LinkOpener.ID, LinkOpener, EditorContributionInstantiation.Lazy);
|
||||
registerEditorAction(OpenLinkAction);
|
||||
|
|
Loading…
Reference in a new issue