Files
azuredatastudio/src/vs/editor/contrib/links/links.ts
Anthony Dresser ea0f9e6ce9 Merge from vscode 64980ea1f3f532c82bb6c28d27bba9ef2c5b4463 (#7206)
* Merge from vscode 64980ea1f3f532c82bb6c28d27bba9ef2c5b4463

* fix config changes

* fix strictnull checks
2019-09-15 22:38:26 -07:00

403 lines
13 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./links';
import * as nls from 'vs/nls';
import * as async 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 { DisposableStore } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { Position } from 'vs/editor/common/core/position';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { LinkProviderRegistry } from 'vs/editor/common/modes';
import { ClickLinkGesture, ClickLinkKeyboardEvent, ClickLinkMouseEvent } from 'vs/editor/contrib/goToDefinition/clickLinkGesture';
import { Link, getLinks, LinksList } from 'vs/editor/contrib/links/getLinks';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString {
const executeCmd = link.url && /^command:/i.test(link.url.toString());
const label = link.tooltip
? link.tooltip
: executeCmd
? nls.localize('links.navigate.executeCmd', 'Execute command')
: nls.localize('links.navigate.follow', 'Follow link');
const kb = useMetaKey
? platform.isMacintosh
? nls.localize('links.navigate.kb.meta.mac', "cmd + click")
: nls.localize('links.navigate.kb.meta', "ctrl + click")
: platform.isMacintosh
? nls.localize('links.navigate.kb.alt.mac', "option + click")
: nls.localize('links.navigate.kb.alt', "alt + click");
if (link.url) {
const hoverMessage = new MarkdownString().appendMarkdown(`[${label}](${link.url.toString()}) (${kb})`);
hoverMessage.isTrusted = true;
return hoverMessage;
} else {
return new MarkdownString().appendText(`${label} (${kb})`);
}
}
const decoration = {
general: ModelDecorationOptions.register({
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
collapseOnReplaceEdit: true,
inlineClassName: 'detected-link'
}),
active: ModelDecorationOptions.register({
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
collapseOnReplaceEdit: true,
inlineClassName: 'detected-link-active'
})
};
class LinkOccurrence {
public static decoration(link: Link, useMetaKey: boolean): IModelDeltaDecoration {
return {
range: link.range,
options: LinkOccurrence._getOptions(link, useMetaKey, false)
};
}
private static _getOptions(link: Link, useMetaKey: boolean, isActive: boolean): ModelDecorationOptions {
const options = { ... (isActive ? decoration.active : decoration.general) };
options.hoverMessage = getHoverMessage(link, useMetaKey);
return options;
}
public decorationId: string;
public link: Link;
constructor(link: Link, decorationId: string) {
this.link = link;
this.decorationId = decorationId;
}
public activate(changeAccessor: IModelDecorationsChangeAccessor, useMetaKey: boolean): void {
changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurrence._getOptions(this.link, useMetaKey, true));
}
public deactivate(changeAccessor: IModelDecorationsChangeAccessor, useMetaKey: boolean): void {
changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurrence._getOptions(this.link, useMetaKey, false));
}
}
class LinkDetector implements editorCommon.IEditorContribution {
private static readonly ID: string = 'editor.linkDetector';
public static get(editor: ICodeEditor): LinkDetector {
return editor.getContribution<LinkDetector>(LinkDetector.ID);
}
static RECOMPUTE_TIME = 1000; // ms
private readonly editor: ICodeEditor;
private enabled: boolean;
private readonly listenersToRemove = new DisposableStore();
private readonly timeout: async.TimeoutTimer;
private computePromise: async.CancelablePromise<LinksList> | null;
private activeLinksList: LinksList | null;
private activeLinkDecorationId: string | null;
private readonly openerService: IOpenerService;
private readonly notificationService: INotificationService;
private currentOccurrences: { [decorationId: string]: LinkOccurrence; };
constructor(
editor: ICodeEditor,
@IOpenerService openerService: IOpenerService,
@INotificationService notificationService: INotificationService
) {
this.editor = editor;
this.openerService = openerService;
this.notificationService = notificationService;
let clickLinkGesture = new ClickLinkGesture(editor);
this.listenersToRemove.add(clickLinkGesture);
this.listenersToRemove.add(clickLinkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
this._onEditorMouseMove(mouseEvent, keyboardEvent);
}));
this.listenersToRemove.add(clickLinkGesture.onExecute((e) => {
this.onEditorMouseUp(e);
}));
this.listenersToRemove.add(clickLinkGesture.onCancel((e) => {
this.cleanUpActiveLinkDecoration();
}));
this.enabled = editor.getOption(EditorOption.links);
this.listenersToRemove.add(editor.onDidChangeConfiguration((e) => {
const enabled = editor.getOption(EditorOption.links);
if (this.enabled === enabled) {
// No change in our configuration option
return;
}
this.enabled = enabled;
// Remove any links (for the getting disabled case)
this.updateDecorations([]);
// Stop any computation (for the getting disabled case)
this.stop();
// Start computing (for the getting enabled case)
this.beginCompute();
}));
this.listenersToRemove.add(editor.onDidChangeModelContent((e) => this.onChange()));
this.listenersToRemove.add(editor.onDidChangeModel((e) => this.onModelChanged()));
this.listenersToRemove.add(editor.onDidChangeModelLanguage((e) => this.onModelModeChanged()));
this.listenersToRemove.add(LinkProviderRegistry.onDidChange((e) => this.onModelModeChanged()));
this.timeout = new async.TimeoutTimer();
this.computePromise = null;
this.activeLinksList = null;
this.currentOccurrences = {};
this.activeLinkDecorationId = null;
this.beginCompute();
}
public getId(): string {
return LinkDetector.ID;
}
private onModelChanged(): void {
this.currentOccurrences = {};
this.activeLinkDecorationId = null;
this.stop();
this.beginCompute();
}
private onModelModeChanged(): void {
this.stop();
this.beginCompute();
}
private onChange(): void {
this.timeout.setIfNotSet(() => this.beginCompute(), LinkDetector.RECOMPUTE_TIME);
}
private async beginCompute(): Promise<void> {
if (!this.editor.hasModel() || !this.enabled) {
return;
}
const model = this.editor.getModel();
if (!LinkProviderRegistry.has(model)) {
return;
}
if (this.activeLinksList) {
this.activeLinksList.dispose();
this.activeLinksList = null;
}
this.computePromise = async.createCancelablePromise(token => getLinks(model, token));
try {
this.activeLinksList = await this.computePromise;
this.updateDecorations(this.activeLinksList.links);
} catch (err) {
onUnexpectedError(err);
} finally {
this.computePromise = null;
}
}
private updateDecorations(links: Link[]): void {
const useMetaKey = (this.editor.getOption(EditorOption.multiCursorModifier) === 'altKey');
let oldDecorations: string[] = [];
let keys = Object.keys(this.currentOccurrences);
for (let i = 0, len = keys.length; i < len; i++) {
let decorationId = keys[i];
let occurance = this.currentOccurrences[decorationId];
oldDecorations.push(occurance.decorationId);
}
let newDecorations: IModelDeltaDecoration[] = [];
if (links) {
// Not sure why this is sometimes null
for (const link of links) {
newDecorations.push(LinkOccurrence.decoration(link, useMetaKey));
}
}
let decorations = this.editor.deltaDecorations(oldDecorations, newDecorations);
this.currentOccurrences = {};
this.activeLinkDecorationId = null;
for (let i = 0, len = decorations.length; i < len; i++) {
let occurance = new LinkOccurrence(links[i], decorations[i]);
this.currentOccurrences[occurance.decorationId] = occurance;
}
}
private _onEditorMouseMove(mouseEvent: ClickLinkMouseEvent, withKey: ClickLinkKeyboardEvent | null): void {
const useMetaKey = (this.editor.getOption(EditorOption.multiCursorModifier) === 'altKey');
if (this.isEnabled(mouseEvent, withKey)) {
this.cleanUpActiveLinkDecoration(); // always remove previous link decoration as their can only be one
const occurrence = this.getLinkOccurrence(mouseEvent.target.position);
if (occurrence) {
this.editor.changeDecorations((changeAccessor) => {
occurrence.activate(changeAccessor, useMetaKey);
this.activeLinkDecorationId = occurrence.decorationId;
});
}
} else {
this.cleanUpActiveLinkDecoration();
}
}
private cleanUpActiveLinkDecoration(): void {
const useMetaKey = (this.editor.getOption(EditorOption.multiCursorModifier) === 'altKey');
if (this.activeLinkDecorationId) {
const occurrence = this.currentOccurrences[this.activeLinkDecorationId];
if (occurrence) {
this.editor.changeDecorations((changeAccessor) => {
occurrence.deactivate(changeAccessor, useMetaKey);
});
}
this.activeLinkDecorationId = null;
}
}
private onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void {
if (!this.isEnabled(mouseEvent)) {
return;
}
const occurrence = this.getLinkOccurrence(mouseEvent.target.position);
if (!occurrence) {
return;
}
this.openLinkOccurrence(occurrence, mouseEvent.hasSideBySideModifier);
}
public openLinkOccurrence(occurrence: LinkOccurrence, openToSide: boolean): void {
if (!this.openerService) {
return;
}
const { link } = occurrence;
link.resolve(CancellationToken.None).then(uri => {
// open the uri
return this.openerService.open(uri, { openToSide });
}, 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);
}
});
}
public getLinkOccurrence(position: Position | null): LinkOccurrence | null {
if (!this.editor.hasModel() || !position) {
return null;
}
const decorations = this.editor.getModel().getDecorationsInRange({
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: position.lineNumber,
endColumn: position.column
}, 0, true);
for (const decoration of decorations) {
const currentOccurrence = this.currentOccurrences[decoration.id];
if (currentOccurrence) {
return currentOccurrence;
}
}
return null;
}
private isEnabled(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent | null): boolean {
return Boolean(
(mouseEvent.target.type === MouseTargetType.CONTENT_TEXT)
&& (mouseEvent.hasTriggerModifier || (withKey && withKey.keyCodeIsTriggerKey))
);
}
private stop(): void {
this.timeout.cancel();
if (this.activeLinksList) {
this.activeLinksList.dispose();
}
if (this.computePromise) {
this.computePromise.cancel();
this.computePromise = null;
}
}
public dispose(): void {
this.listenersToRemove.dispose();
this.stop();
this.timeout.dispose();
}
}
class OpenLinkAction extends EditorAction {
constructor() {
super({
id: 'editor.action.openLink',
label: nls.localize('label', "Open Link"),
alias: 'Open Link',
precondition: undefined
});
}
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
let linkDetector = LinkDetector.get(editor);
if (!linkDetector) {
return;
}
if (!editor.hasModel()) {
return;
}
let selections = editor.getSelections();
for (let sel of selections) {
let link = linkDetector.getLinkOccurrence(sel.getEndPosition());
if (link) {
linkDetector.openLinkOccurrence(link, false);
}
}
}
}
registerEditorContribution(LinkDetector);
registerEditorAction(OpenLinkAction);
registerThemingParticipant((theme, collector) => {
const activeLinkForeground = theme.getColor(editorActiveLinkForeground);
if (activeLinkForeground) {
collector.addRule(`.monaco-editor .detected-link-active { color: ${activeLinkForeground} !important; }`);
}
});