links.ts 11.4 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

import 'vs/css!./links';
A
Alex Dima 已提交
9
import * as nls from 'vs/nls';
A
Alex Dima 已提交
10 11 12 13 14 15
import {onUnexpectedError} from 'vs/base/common/errors';
import {KeyCode} from 'vs/base/common/keyCodes';
import * as platform from 'vs/base/common/platform';
import Severity from 'vs/base/common/severity';
import {TPromise} from 'vs/base/common/winjs.base';
import {IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
E
Erich Gamma 已提交
16
import {IMessageService} from 'vs/platform/message/common/message';
17
import {IOpenerService} from 'vs/platform/opener/common/opener';
A
Alex Dima 已提交
18
import * as editorCommon from 'vs/editor/common/editorCommon';
A
Alex Dima 已提交
19
import {ServicesAccessor, EditorAction2, CommonEditorRegistry} from 'vs/editor/common/editorCommonExtensions';
20
import {LinkProviderRegistry} from 'vs/editor/common/modes';
21
import {IEditorWorkerService} from 'vs/editor/common/services/editorWorkerService';
A
Alex Dima 已提交
22
import {IEditorMouseEvent, ICodeEditor} from 'vs/editor/browser/editorBrowser';
23
import {getLinks, Link} from 'vs/editor/contrib/links/common/links';
A
Alex Dima 已提交
24
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
25
import {EditorBrowserRegistry} from 'vs/editor/browser/editorBrowserExtensions';
E
Erich Gamma 已提交
26 27 28

class LinkOccurence {

29
	public static decoration(link: Link): editorCommon.IModelDeltaDecoration {
E
Erich Gamma 已提交
30 31 32 33 34 35 36 37
		return {
			range: {
				startLineNumber: link.range.startLineNumber,
				startColumn: link.range.startColumn,
				endLineNumber: link.range.startLineNumber,
				endColumn: link.range.endColumn
			},
			options: LinkOccurence._getOptions(link, false)
A
tslint  
Alex Dima 已提交
38
		};
E
Erich Gamma 已提交
39 40
	}

41
	private static _getOptions(link: Link, isActive: boolean): editorCommon.IModelDecorationOptions {
E
Erich Gamma 已提交
42 43 44 45 46 47 48 49 50
		var result = '';

		if (isActive) {
			result += LinkDetector.CLASS_NAME_ACTIVE;
		} else {
			result += LinkDetector.CLASS_NAME;
		}

		return {
A
Alex Dima 已提交
51
			stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
E
Erich Gamma 已提交
52 53 54 55 56
			inlineClassName: result,
			hoverMessage: LinkDetector.HOVER_MESSAGE_GENERAL
		};
	}

57 58
	public decorationId: string;
	public link: Link;
E
Erich Gamma 已提交
59

60
	constructor(link: Link, decorationId: string/*, changeAccessor:editorCommon.IModelDecorationsChangeAccessor*/) {
E
Erich Gamma 已提交
61 62 63 64
		this.link = link;
		this.decorationId = decorationId;
	}

65
	public activate(changeAccessor: editorCommon.IModelDecorationsChangeAccessor): void {
E
Erich Gamma 已提交
66 67 68
		changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurence._getOptions(this.link, true));
	}

69
	public deactivate(changeAccessor: editorCommon.IModelDecorationsChangeAccessor): void {
E
Erich Gamma 已提交
70 71 72 73
		changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurence._getOptions(this.link, false));
	}
}

A
Alex Dima 已提交
74 75 76
class LinkDetector implements editorCommon.IEditorContribution {

	public static ID: string = 'editor.linkDetector';
77
	public static get(editor: editorCommon.ICommonCodeEditor): LinkDetector {
A
Alex Dima 已提交
78 79 80
		return <LinkDetector>editor.getContribution(LinkDetector.ID);
	}

E
Erich Gamma 已提交
81
	static RECOMPUTE_TIME = 1000; // ms
A
Alex Dima 已提交
82 83 84
	static TRIGGER_KEY_VALUE = platform.isMacintosh ? KeyCode.Meta : KeyCode.Ctrl;
	static TRIGGER_MODIFIER = platform.isMacintosh ? 'metaKey' : 'ctrlKey';
	static HOVER_MESSAGE_GENERAL = platform.isMacintosh ? nls.localize('links.navigate.mac', "Cmd + click to follow link") : nls.localize('links.navigate', "Ctrl + click to follow link");
E
Erich Gamma 已提交
85 86 87
	static CLASS_NAME = 'detected-link';
	static CLASS_NAME_ACTIVE = 'detected-link-active';

88 89 90 91 92 93 94 95
	private editor: ICodeEditor;
	private listenersToRemove: IDisposable[];
	private timeoutPromise: TPromise<void>;
	private computePromise: TPromise<void>;
	private activeLinkDecorationId: string;
	private lastMouseEvent: IEditorMouseEvent;
	private openerService: IOpenerService;
	private messageService: IMessageService;
96
	private editorWorkerService: IEditorWorkerService;
97
	private currentOccurences: { [decorationId: string]: LinkOccurence; };
E
Erich Gamma 已提交
98

99
	constructor(
100 101 102
		editor: ICodeEditor,
		@IOpenerService openerService: IOpenerService,
		@IMessageService messageService: IMessageService,
A
Alex Dima 已提交
103
		@IEditorWorkerService editorWorkerService: IEditorWorkerService
104
	) {
E
Erich Gamma 已提交
105
		this.editor = editor;
106
		this.openerService = openerService;
E
Erich Gamma 已提交
107
		this.messageService = messageService;
108
		this.editorWorkerService = editorWorkerService;
E
Erich Gamma 已提交
109
		this.listenersToRemove = [];
A
Alex Dima 已提交
110
		this.listenersToRemove.push(editor.onDidChangeModelContent((e) => this.onChange()));
A
Alex Dima 已提交
111 112
		this.listenersToRemove.push(editor.onDidChangeModel((e) => this.onModelChanged()));
		this.listenersToRemove.push(editor.onDidChangeModelMode((e) => this.onModelModeChanged()));
113
		this.listenersToRemove.push(LinkProviderRegistry.onDidChange((e) => this.onModelModeChanged()));
114 115 116 117
		this.listenersToRemove.push(this.editor.onMouseUp((e: IEditorMouseEvent) => this.onEditorMouseUp(e)));
		this.listenersToRemove.push(this.editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(e)));
		this.listenersToRemove.push(this.editor.onKeyDown((e: IKeyboardEvent) => this.onEditorKeyDown(e)));
		this.listenersToRemove.push(this.editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(e)));
E
Erich Gamma 已提交
118 119 120 121 122 123 124
		this.timeoutPromise = null;
		this.computePromise = null;
		this.currentOccurences = {};
		this.activeLinkDecorationId = null;
		this.beginCompute();
	}

A
Alex Dima 已提交
125 126 127 128
	public getId(): string {
		return LinkDetector.ID;
	}

E
Erich Gamma 已提交
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
	public isComputing(): boolean {
		return TPromise.is(this.computePromise);
	}

	private onModelChanged(): void {
		this.lastMouseEvent = null;
		this.currentOccurences = {};
		this.activeLinkDecorationId = null;
		this.stop();
		this.beginCompute();
	}

	private onModelModeChanged(): void {
		this.stop();
		this.beginCompute();
	}

146
	private onChange(): void {
E
Erich Gamma 已提交
147 148 149 150 151 152 153 154 155
		if (!this.timeoutPromise) {
			this.timeoutPromise = TPromise.timeout(LinkDetector.RECOMPUTE_TIME);
			this.timeoutPromise.then(() => {
				this.timeoutPromise = null;
				this.beginCompute();
			});
		}
	}

156
	private beginCompute(): void {
E
Erich Gamma 已提交
157 158 159
		if (!this.editor.getModel()) {
			return;
		}
160

161 162
		if (!LinkProviderRegistry.has(this.editor.getModel())) {
			return;
E
Erich Gamma 已提交
163
		}
164

165
		this.computePromise = getLinks(this.editor.getModel()).then(links => {
166 167 168 169 170
			this.updateDecorations(links);
			this.computePromise = null;
		});
	}

171 172 173
	private updateDecorations(links: Link[]): void {
		this.editor.changeDecorations((changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => {
			var oldDecorations: string[] = [];
A
Alex Dima 已提交
174 175 176 177 178
			let keys = Object.keys(this.currentOccurences);
			for (let i = 0, len = keys.length; i < len; i++) {
				let decorationId = keys[i];
				let occurance = this.currentOccurences[decorationId];
				oldDecorations.push(occurance.decorationId);
E
Erich Gamma 已提交
179 180
			}

181
			var newDecorations: editorCommon.IModelDeltaDecoration[] = [];
E
Erich Gamma 已提交
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
			if (links) {
				// Not sure why this is sometimes null
				for (var i = 0; i < links.length; i++) {
					newDecorations.push(LinkOccurence.decoration(links[i]));
				}
			}

			var decorations = changeAccessor.deltaDecorations(oldDecorations, newDecorations);

			this.currentOccurences = {};
			this.activeLinkDecorationId = null;
			for (let i = 0, len = decorations.length; i < len; i++) {
				var occurance = new LinkOccurence(links[i], decorations[i]);
				this.currentOccurences[occurance.decorationId] = occurance;
			}
		});
	}

200
	private onEditorKeyDown(e: IKeyboardEvent): void {
E
Erich Gamma 已提交
201 202 203 204 205
		if (e.keyCode === LinkDetector.TRIGGER_KEY_VALUE && this.lastMouseEvent) {
			this.onEditorMouseMove(this.lastMouseEvent, e);
		}
	}

206
	private onEditorKeyUp(e: IKeyboardEvent): void {
E
Erich Gamma 已提交
207 208 209 210 211
		if (e.keyCode === LinkDetector.TRIGGER_KEY_VALUE) {
			this.cleanUpActiveLinkDecoration();
		}
	}

212
	private onEditorMouseMove(mouseEvent: IEditorMouseEvent, withKey?: IKeyboardEvent): void {
E
Erich Gamma 已提交
213 214 215 216 217 218
		this.lastMouseEvent = mouseEvent;

		if (this.isEnabled(mouseEvent, withKey)) {
			this.cleanUpActiveLinkDecoration(); // always remove previous link decoration as their can only be one
			var occurence = this.getLinkOccurence(mouseEvent.target.position);
			if (occurence) {
219
				this.editor.changeDecorations((changeAccessor) => {
E
Erich Gamma 已提交
220 221 222 223 224 225 226 227 228
					occurence.activate(changeAccessor);
					this.activeLinkDecorationId = occurence.decorationId;
				});
			}
		} else {
			this.cleanUpActiveLinkDecoration();
		}
	}

229
	private cleanUpActiveLinkDecoration(): void {
E
Erich Gamma 已提交
230 231 232
		if (this.activeLinkDecorationId) {
			var occurence = this.currentOccurences[this.activeLinkDecorationId];
			if (occurence) {
233
				this.editor.changeDecorations((changeAccessor) => {
E
Erich Gamma 已提交
234 235 236 237 238 239 240 241
					occurence.deactivate(changeAccessor);
				});
			}

			this.activeLinkDecorationId = null;
		}
	}

242
	private onEditorMouseUp(mouseEvent: IEditorMouseEvent): void {
E
Erich Gamma 已提交
243 244 245 246 247 248 249 250 251 252
		if (!this.isEnabled(mouseEvent)) {
			return;
		}
		var occurence = this.getLinkOccurence(mouseEvent.target.position);
		if (!occurence) {
			return;
		}
		this.openLinkOccurence(occurence, mouseEvent.event.altKey);
	}

253
	public openLinkOccurence(occurence: LinkOccurence, openToSide: boolean): void {
E
Erich Gamma 已提交
254

255
		if (!this.openerService) {
E
Erich Gamma 已提交
256 257 258
			return;
		}

259 260 261 262 263
		const {link} = occurence;

		link.resolve().then(uri => {
			// open the uri
			return this.openerService.open(uri, { openToSide });
E
Erich Gamma 已提交
264

265 266 267 268 269 270 271 272 273 274
		}, err => {
			// different error cases
			if (err === 'invalid') {
				this.messageService.show(Severity.Warning, nls.localize('invalid.url', 'Sorry, failed to open this link because it is not well-formed: {0}', link.url));
			} else if (err === 'missing') {
				this.messageService.show(Severity.Warning, nls.localize('missing.url', 'Sorry, failed to open this link because its target is missing.'));
			} else {
				onUnexpectedError(err);
			}
		});
E
Erich Gamma 已提交
275 276
	}

A
Alex Dima 已提交
277
	public getLinkOccurence(position: editorCommon.IPosition): LinkOccurence {
E
Erich Gamma 已提交
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
		var decorations = this.editor.getModel().getDecorationsInRange({
			startLineNumber: position.lineNumber,
			startColumn: position.column,
			endLineNumber: position.lineNumber,
			endColumn: position.column
		}, null, true);

		for (var i = 0; i < decorations.length; i++) {
			var decoration = decorations[i];
			var currentOccurence = this.currentOccurences[decoration.id];
			if (currentOccurence) {
				return currentOccurence;
			}
		}

		return null;
	}

296 297 298
	private isEnabled(mouseEvent: IEditorMouseEvent, withKey?: IKeyboardEvent): boolean {
		return mouseEvent.target.type === editorCommon.MouseTargetType.CONTENT_TEXT &&
			(mouseEvent.event[LinkDetector.TRIGGER_MODIFIER] || (withKey && withKey.keyCode === LinkDetector.TRIGGER_KEY_VALUE));
E
Erich Gamma 已提交
299 300
	}

301
	private stop(): void {
E
Erich Gamma 已提交
302 303 304 305 306 307 308 309 310 311
		if (this.timeoutPromise) {
			this.timeoutPromise.cancel();
			this.timeoutPromise = null;
		}
		if (this.computePromise) {
			this.computePromise.cancel();
			this.computePromise = null;
		}
	}

312
	public dispose(): void {
A
Alex Dima 已提交
313
		this.listenersToRemove = dispose(this.listenersToRemove);
E
Erich Gamma 已提交
314 315 316 317
		this.stop();
	}
}

A
Alex Dima 已提交
318
class OpenLinkAction extends EditorAction2 {
E
Erich Gamma 已提交
319

A
Alex Dima 已提交
320 321 322 323 324 325 326
	constructor() {
		super(
			'editor.action.openLink',
			nls.localize('label', "Open Link"),
			'Open Link',
			false
		);
E
Erich Gamma 已提交
327 328
	}

A
Alex Dima 已提交
329 330 331
	public enabled(accessor:ServicesAccessor, editor:editorCommon.ICommonCodeEditor): boolean {
		let linkDetector = LinkDetector.get(editor);
		if (linkDetector.isComputing()) {
E
Erich Gamma 已提交
332 333 334
			// optimistic enablement while state is being computed
			return true;
		}
A
Alex Dima 已提交
335
		return !!linkDetector.getLinkOccurence(editor.getPosition());
E
Erich Gamma 已提交
336 337
	}

A
Alex Dima 已提交
338 339 340
	public run(accessor:ServicesAccessor, editor:editorCommon.ICommonCodeEditor): void {
		let linkDetector = LinkDetector.get(editor);
		let link = linkDetector.getLinkOccurence(editor.getPosition());
341
		if (link) {
A
Alex Dima 已提交
342
			linkDetector.openLinkOccurence(link, false);
E
Erich Gamma 已提交
343 344 345 346
		}
	}
}

A
Alex Dima 已提交
347
CommonEditorRegistry.registerEditorAction2(new OpenLinkAction());
A
Alex Dima 已提交
348
EditorBrowserRegistry.registerEditorContribution(LinkDetector);