monaco-editor-core
Version:
A browser based code editor
376 lines (375 loc) • 16.7 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var LinkDetector_1;
import { createCancelablePromise, RunOnceScheduler } from '../../../../base/common/async.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { onUnexpectedError } from '../../../../base/common/errors.js';
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../base/common/network.js';
import * as platform from '../../../../base/common/platform.js';
import * as resources from '../../../../base/common/resources.js';
import { StopWatch } from '../../../../base/common/stopwatch.js';
import { URI } from '../../../../base/common/uri.js';
import './links.css';
import { EditorAction, registerEditorAction, registerEditorContribution } from '../../../browser/editorExtensions.js';
import { ModelDecorationOptions } from '../../../common/model/textModel.js';
import { ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
import { ClickLinkGesture } from '../../gotoSymbol/browser/link/clickLinkGesture.js';
import { getLinks } from './getLinks.js';
import * as nls from '../../../../nls.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
let LinkDetector = class LinkDetector extends Disposable {
static { LinkDetector_1 = this; }
static { this.ID = 'editor.linkDetector'; }
static get(editor) {
return editor.getContribution(LinkDetector_1.ID);
}
constructor(editor, openerService, notificationService, languageFeaturesService, languageFeatureDebounceService) {
super();
this.editor = editor;
this.openerService = openerService;
this.notificationService = notificationService;
this.languageFeaturesService = languageFeaturesService;
this.providers = this.languageFeaturesService.linkProvider;
this.debounceInformation = languageFeatureDebounceService.for(this.providers, 'Links', { min: 1000, max: 4000 });
this.computeLinks = this._register(new RunOnceScheduler(() => this.computeLinksNow(), 1000));
this.computePromise = null;
this.activeLinksList = null;
this.currentOccurrences = {};
this.activeLinkDecorationId = null;
const clickLinkGesture = this._register(new ClickLinkGesture(editor));
this._register(clickLinkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
this._onEditorMouseMove(mouseEvent, keyboardEvent);
}));
this._register(clickLinkGesture.onExecute((e) => {
this.onEditorMouseUp(e);
}));
this._register(clickLinkGesture.onCancel((e) => {
this.cleanUpActiveLinkDecoration();
}));
this._register(editor.onDidChangeConfiguration((e) => {
if (!e.hasChanged(71 /* EditorOption.links */)) {
return;
}
// 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.computeLinks.schedule(0);
}));
this._register(editor.onDidChangeModelContent((e) => {
if (!this.editor.hasModel()) {
return;
}
this.computeLinks.schedule(this.debounceInformation.get(this.editor.getModel()));
}));
this._register(editor.onDidChangeModel((e) => {
this.currentOccurrences = {};
this.activeLinkDecorationId = null;
this.stop();
this.computeLinks.schedule(0);
}));
this._register(editor.onDidChangeModelLanguage((e) => {
this.stop();
this.computeLinks.schedule(0);
}));
this._register(this.providers.onDidChange((e) => {
this.stop();
this.computeLinks.schedule(0);
}));
this.computeLinks.schedule(0);
}
async computeLinksNow() {
if (!this.editor.hasModel() || !this.editor.getOption(71 /* EditorOption.links */)) {
return;
}
const model = this.editor.getModel();
if (model.isTooLargeForSyncing()) {
return;
}
if (!this.providers.has(model)) {
return;
}
if (this.activeLinksList) {
this.activeLinksList.dispose();
this.activeLinksList = null;
}
this.computePromise = createCancelablePromise(token => getLinks(this.providers, model, token));
try {
const sw = new StopWatch(false);
this.activeLinksList = await this.computePromise;
this.debounceInformation.update(model, sw.elapsed());
if (model.isDisposed()) {
return;
}
this.updateDecorations(this.activeLinksList.links);
}
catch (err) {
onUnexpectedError(err);
}
finally {
this.computePromise = null;
}
}
updateDecorations(links) {
const useMetaKey = (this.editor.getOption(78 /* EditorOption.multiCursorModifier */) === 'altKey');
const oldDecorations = [];
const keys = Object.keys(this.currentOccurrences);
for (const decorationId of keys) {
const occurence = this.currentOccurrences[decorationId];
oldDecorations.push(occurence.decorationId);
}
const newDecorations = [];
if (links) {
// Not sure why this is sometimes null
for (const link of links) {
newDecorations.push(LinkOccurrence.decoration(link, useMetaKey));
}
}
this.editor.changeDecorations((changeAccessor) => {
const decorations = changeAccessor.deltaDecorations(oldDecorations, newDecorations);
this.currentOccurrences = {};
this.activeLinkDecorationId = null;
for (let i = 0, len = decorations.length; i < len; i++) {
const occurence = new LinkOccurrence(links[i], decorations[i]);
this.currentOccurrences[occurence.decorationId] = occurence;
}
});
}
_onEditorMouseMove(mouseEvent, withKey) {
const useMetaKey = (this.editor.getOption(78 /* 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();
}
}
cleanUpActiveLinkDecoration() {
const useMetaKey = (this.editor.getOption(78 /* 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;
}
}
onEditorMouseUp(mouseEvent) {
if (!this.isEnabled(mouseEvent)) {
return;
}
const occurrence = this.getLinkOccurrence(mouseEvent.target.position);
if (!occurrence) {
return;
}
this.openLinkOccurrence(occurrence, mouseEvent.hasSideBySideModifier, true /* from user gesture */);
}
openLinkOccurrence(occurrence, openToSide, fromUserGesture = false) {
if (!this.openerService) {
return;
}
const { link } = occurrence;
link.resolve(CancellationToken.None).then(uri => {
// 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);
let relativePath = null;
if (fsPath.startsWith('/./') || fsPath.startsWith('\\.\\')) {
relativePath = `.${fsPath.substr(1)}`;
}
else if (fsPath.startsWith('//./') || 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 ? 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);
}
});
}
getLinkOccurrence(position) {
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;
}
isEnabled(mouseEvent, withKey) {
return Boolean((mouseEvent.target.type === 6 /* MouseTargetType.CONTENT_TEXT */)
&& (mouseEvent.hasTriggerModifier || (withKey && withKey.keyCodeIsTriggerKey)));
}
stop() {
this.computeLinks.cancel();
if (this.activeLinksList) {
this.activeLinksList?.dispose();
this.activeLinksList = null;
}
if (this.computePromise) {
this.computePromise.cancel();
this.computePromise = null;
}
}
dispose() {
super.dispose();
this.stop();
}
};
LinkDetector = LinkDetector_1 = __decorate([
__param(1, IOpenerService),
__param(2, INotificationService),
__param(3, ILanguageFeaturesService),
__param(4, ILanguageFeatureDebounceService)
], LinkDetector);
export { LinkDetector };
const decoration = {
general: ModelDecorationOptions.register({
description: 'detected-link',
stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */,
collapseOnReplaceEdit: true,
inlineClassName: 'detected-link'
}),
active: ModelDecorationOptions.register({
description: 'detected-link-active',
stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */,
collapseOnReplaceEdit: true,
inlineClassName: 'detected-link-active'
})
};
class LinkOccurrence {
static decoration(link, useMetaKey) {
return {
range: link.range,
options: LinkOccurrence._getOptions(link, useMetaKey, false)
};
}
static _getOptions(link, useMetaKey, isActive) {
const options = { ...(isActive ? decoration.active : decoration.general) };
options.hoverMessage = getHoverMessage(link, useMetaKey);
return options;
}
constructor(link, decorationId) {
this.link = link;
this.decorationId = decorationId;
}
activate(changeAccessor, useMetaKey) {
changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurrence._getOptions(this.link, useMetaKey, true));
}
deactivate(changeAccessor, useMetaKey) {
changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurrence._getOptions(this.link, useMetaKey, false));
}
}
function getHoverMessage(link, useMetaKey) {
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) {
let nativeLabel = '';
if (/^command:/i.test(link.url.toString())) {
// Don't show complete command arguments in the native tooltip
const match = link.url.toString().match(/^command:([^?#]+)/);
if (match) {
const commandId = match[1];
nativeLabel = nls.localize('tooltip.explanation', "Execute command {0}", commandId);
}
}
const hoverMessage = new MarkdownString('', true)
.appendLink(link.url.toString(true).replace(/ /g, '%20'), label, nativeLabel)
.appendMarkdown(` (${kb})`);
return hoverMessage;
}
else {
return new MarkdownString().appendText(`${label} (${kb})`);
}
}
class OpenLinkAction extends EditorAction {
constructor() {
super({
id: 'editor.action.openLink',
label: nls.localize('label', "Open Link"),
alias: 'Open Link',
precondition: undefined
});
}
run(accessor, editor) {
const linkDetector = LinkDetector.get(editor);
if (!linkDetector) {
return;
}
if (!editor.hasModel()) {
return;
}
const selections = editor.getSelections();
for (const sel of selections) {
const link = linkDetector.getLinkOccurrence(sel.getEndPosition());
if (link) {
linkDetector.openLinkOccurrence(link, false);
}
}
}
}
registerEditorContribution(LinkDetector.ID, LinkDetector, 1 /* EditorContributionInstantiation.AfterFirstRender */);
registerEditorAction(OpenLinkAction);