add resolveLink and allow incomplete DocumentLink

This commit is contained in:
Johannes Rieken 2016-08-03 17:14:06 +02:00
parent 21fb268883
commit 07a53e91d0
6 changed files with 123 additions and 72 deletions

View file

@ -830,6 +830,7 @@ export interface ILink {
*/
export interface LinkProvider {
provideLinks(model: editorCommon.IReadOnlyModel, token: CancellationToken): ILink[] | Thenable<ILink[]>;
resolveLink?: (link: ILink, token: CancellationToken) => ILink | Thenable<ILink>;
}

View file

@ -11,7 +11,6 @@ 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 URI from 'vs/base/common/uri';
import {TPromise} from 'vs/base/common/winjs.base';
import {IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import {IMessageService} from 'vs/platform/message/common/message';
@ -20,16 +19,16 @@ import {EditorAction} from 'vs/editor/common/editorAction';
import {Behaviour} from 'vs/editor/common/editorActionEnablement';
import * as editorCommon from 'vs/editor/common/editorCommon';
import {CommonEditorRegistry, EditorActionDescriptor} from 'vs/editor/common/editorCommonExtensions';
import {ILink, LinkProviderRegistry} from 'vs/editor/common/modes';
import {LinkProviderRegistry} from 'vs/editor/common/modes';
import {IEditorWorkerService} from 'vs/editor/common/services/editorWorkerService';
import {IEditorMouseEvent, ICodeEditor} from 'vs/editor/browser/editorBrowser';
import {getLinks} from 'vs/editor/contrib/links/common/links';
import {getLinks, Link} from 'vs/editor/contrib/links/common/links';
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
import {EditorBrowserRegistry} from 'vs/editor/browser/editorBrowserExtensions';
class LinkOccurence {
public static decoration(link:ILink): editorCommon.IModelDeltaDecoration {
public static decoration(link: Link): editorCommon.IModelDeltaDecoration {
return {
range: {
startLineNumber: link.range.startLineNumber,
@ -41,7 +40,7 @@ class LinkOccurence {
};
}
private static _getOptions(link:ILink, isActive:boolean):editorCommon.IModelDecorationOptions {
private static _getOptions(link: Link, isActive: boolean): editorCommon.IModelDecorationOptions {
var result = '';
if (isActive) {
@ -57,19 +56,19 @@ class LinkOccurence {
};
}
public decorationId:string;
public link:ILink;
public decorationId: string;
public link: Link;
constructor(link:ILink, decorationId:string/*, changeAccessor:editorCommon.IModelDecorationsChangeAccessor*/) {
constructor(link: Link, decorationId: string/*, changeAccessor:editorCommon.IModelDecorationsChangeAccessor*/) {
this.link = link;
this.decorationId = decorationId;
}
public activate(changeAccessor: editorCommon.IModelDecorationsChangeAccessor):void {
public activate(changeAccessor: editorCommon.IModelDecorationsChangeAccessor): void {
changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurence._getOptions(this.link, true));
}
public deactivate(changeAccessor: editorCommon.IModelDecorationsChangeAccessor):void {
public deactivate(changeAccessor: editorCommon.IModelDecorationsChangeAccessor): void {
changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurence._getOptions(this.link, false));
}
}
@ -77,7 +76,7 @@ class LinkOccurence {
class LinkDetector implements editorCommon.IEditorContribution {
public static ID: string = 'editor.linkDetector';
public static get(editor:editorCommon.ICommonCodeEditor): LinkDetector {
public static get(editor: editorCommon.ICommonCodeEditor): LinkDetector {
return <LinkDetector>editor.getContribution(LinkDetector.ID);
}
@ -88,21 +87,21 @@ class LinkDetector implements editorCommon.IEditorContribution {
static CLASS_NAME = 'detected-link';
static CLASS_NAME_ACTIVE = 'detected-link-active';
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;
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;
private editorWorkerService: IEditorWorkerService;
private currentOccurences:{ [decorationId:string]:LinkOccurence; };
private currentOccurences: { [decorationId: string]: LinkOccurence; };
constructor(
editor:ICodeEditor,
@IOpenerService openerService:IOpenerService,
@IMessageService messageService:IMessageService,
editor: ICodeEditor,
@IOpenerService openerService: IOpenerService,
@IMessageService messageService: IMessageService,
@IEditorWorkerService editorWorkerService: IEditorWorkerService
) {
this.editor = editor;
@ -114,10 +113,10 @@ class LinkDetector implements editorCommon.IEditorContribution {
this.listenersToRemove.push(editor.onDidChangeModel((e) => this.onModelChanged()));
this.listenersToRemove.push(editor.onDidChangeModelMode((e) => this.onModelModeChanged()));
this.listenersToRemove.push(LinkProviderRegistry.onDidChange((e) => this.onModelModeChanged()));
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)));
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)));
this.timeoutPromise = null;
this.computePromise = null;
this.currentOccurences = {};
@ -146,7 +145,7 @@ class LinkDetector implements editorCommon.IEditorContribution {
this.beginCompute();
}
private onChange():void {
private onChange(): void {
if (!this.timeoutPromise) {
this.timeoutPromise = TPromise.timeout(LinkDetector.RECOMPUTE_TIME);
this.timeoutPromise.then(() => {
@ -156,7 +155,7 @@ class LinkDetector implements editorCommon.IEditorContribution {
}
}
private beginCompute():void {
private beginCompute(): void {
if (!this.editor.getModel()) {
return;
}
@ -171,9 +170,9 @@ class LinkDetector implements editorCommon.IEditorContribution {
});
}
private updateDecorations(links:ILink[]):void {
this.editor.changeDecorations((changeAccessor:editorCommon.IModelDecorationsChangeAccessor) => {
var oldDecorations:string[] = [];
private updateDecorations(links: Link[]): void {
this.editor.changeDecorations((changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => {
var oldDecorations: string[] = [];
let keys = Object.keys(this.currentOccurences);
for (let i = 0, len = keys.length; i < len; i++) {
let decorationId = keys[i];
@ -181,7 +180,7 @@ class LinkDetector implements editorCommon.IEditorContribution {
oldDecorations.push(occurance.decorationId);
}
var newDecorations:editorCommon.IModelDeltaDecoration[] = [];
var newDecorations: editorCommon.IModelDeltaDecoration[] = [];
if (links) {
// Not sure why this is sometimes null
for (var i = 0; i < links.length; i++) {
@ -200,26 +199,26 @@ class LinkDetector implements editorCommon.IEditorContribution {
});
}
private onEditorKeyDown(e:IKeyboardEvent):void {
private onEditorKeyDown(e: IKeyboardEvent): void {
if (e.keyCode === LinkDetector.TRIGGER_KEY_VALUE && this.lastMouseEvent) {
this.onEditorMouseMove(this.lastMouseEvent, e);
}
}
private onEditorKeyUp(e:IKeyboardEvent):void {
private onEditorKeyUp(e: IKeyboardEvent): void {
if (e.keyCode === LinkDetector.TRIGGER_KEY_VALUE) {
this.cleanUpActiveLinkDecoration();
}
}
private onEditorMouseMove(mouseEvent: IEditorMouseEvent, withKey?:IKeyboardEvent):void {
private onEditorMouseMove(mouseEvent: IEditorMouseEvent, withKey?: IKeyboardEvent): void {
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) {
this.editor.changeDecorations((changeAccessor)=>{
this.editor.changeDecorations((changeAccessor) => {
occurence.activate(changeAccessor);
this.activeLinkDecorationId = occurence.decorationId;
});
@ -229,11 +228,11 @@ class LinkDetector implements editorCommon.IEditorContribution {
}
}
private cleanUpActiveLinkDecoration():void {
private cleanUpActiveLinkDecoration(): void {
if (this.activeLinkDecorationId) {
var occurence = this.currentOccurences[this.activeLinkDecorationId];
if (occurence) {
this.editor.changeDecorations((changeAccessor)=>{
this.editor.changeDecorations((changeAccessor) => {
occurence.deactivate(changeAccessor);
});
}
@ -242,7 +241,7 @@ class LinkDetector implements editorCommon.IEditorContribution {
}
}
private onEditorMouseUp(mouseEvent: IEditorMouseEvent):void {
private onEditorMouseUp(mouseEvent: IEditorMouseEvent): void {
if (!this.isEnabled(mouseEvent)) {
return;
}
@ -259,16 +258,22 @@ class LinkDetector implements editorCommon.IEditorContribution {
return;
}
let url: URI;
try {
url = URI.parse(occurence.link.url);
} catch (err) {
// invalid url
this.messageService.show(Severity.Warning, nls.localize('invalid.url', 'Invalid URI: cannot open {0}', occurence.link.url));
return;
}
const {link} = occurence;
this.openerService.open(url, { openToSide }).done(null, onUnexpectedError);
link.resolve().then(uri => {
// open the uri
return this.openerService.open(uri, { openToSide });
}, 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);
}
});
}
public getLinkOccurence(position: editorCommon.IPosition): LinkOccurence {
@ -290,12 +295,12 @@ class LinkDetector implements editorCommon.IEditorContribution {
return null;
}
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));
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));
}
private stop():void {
private stop(): void {
if (this.timeoutPromise) {
this.timeoutPromise.cancel();
this.timeoutPromise = null;
@ -306,7 +311,7 @@ class LinkDetector implements editorCommon.IEditorContribution {
}
}
public dispose():void {
public dispose(): void {
this.listenersToRemove = dispose(this.listenersToRemove);
this.stop();
}
@ -317,8 +322,8 @@ class OpenLinkAction extends EditorAction {
static ID = 'editor.action.openLink';
constructor(
descriptor:editorCommon.IEditorActionDescriptorData,
editor:editorCommon.ICommonCodeEditor
descriptor: editorCommon.IEditorActionDescriptorData,
editor: editorCommon.ICommonCodeEditor
) {
super(descriptor, editor, Behaviour.WidgetFocus | Behaviour.UpdateOnCursorPositionChange);
}
@ -335,9 +340,9 @@ class OpenLinkAction extends EditorAction {
return !!LinkDetector.get(this.editor).getLinkOccurence(this.editor.getPosition());
}
public run():TPromise<any> {
public run(): TPromise<any> {
var link = LinkDetector.get(this.editor).getLinkOccurence(this.editor.getPosition());
if(link) {
if (link) {
LinkDetector.get(this.editor).openLinkOccurence(link, false);
}
return TPromise.as(null);

View file

@ -9,21 +9,65 @@ import {onUnexpectedError} from 'vs/base/common/errors';
import URI from 'vs/base/common/uri';
import {TPromise} from 'vs/base/common/winjs.base';
import {Range} from 'vs/editor/common/core/range';
import {IReadOnlyModel} from 'vs/editor/common/editorCommon';
import {ILink, LinkProviderRegistry} from 'vs/editor/common/modes';
import {IReadOnlyModel, IRange} from 'vs/editor/common/editorCommon';
import {ILink, LinkProvider, LinkProviderRegistry} from 'vs/editor/common/modes';
import {asWinJsPromise} from 'vs/base/common/async';
import {CommandsRegistry} from 'vs/platform/commands/common/commands';
import {IModelService} from 'vs/editor/common/services/modelService';
export function getLinks(model: IReadOnlyModel): TPromise<ILink[]> {
export class Link implements ILink {
let links: ILink[] = [];
private _link: ILink;
private _provider: LinkProvider;
constructor(link: ILink, provider: LinkProvider) {
this._link = link;
this._provider = provider;
}
get range(): IRange {
return this._link.range;
}
get url(): string {
return this._link.url;
}
resolve(): TPromise<URI> {
if (this._link.url) {
try {
return TPromise.as(URI.parse(this._link.url));
} catch (e) {
return TPromise.wrapError('invalid');
}
}
if (typeof this._provider.resolveLink === 'function') {
return asWinJsPromise(token => this._provider.resolveLink(this._link, token)).then(value => {
this._link = value;
if (this._link.url) {
// recurse
return this.resolve();
}
return TPromise.wrapError('missing');
});
}
return TPromise.wrapError('missing');
}
}
export function getLinks(model: IReadOnlyModel): TPromise<Link[]> {
let links: Link[] = [];
// ask all providers for links in parallel
const promises = LinkProviderRegistry.ordered(model).reverse().map(support => {
return asWinJsPromise(token => support.provideLinks(model, token)).then(result => {
const promises = LinkProviderRegistry.ordered(model).reverse().map(provider => {
return asWinJsPromise(token => provider.provideLinks(model, token)).then(result => {
if (Array.isArray(result)) {
links = union(links, result);
const newLinks = result.map(link => new Link(link, provider));
links = union(links, newLinks);
}
}, onUnexpectedError);
});
@ -33,15 +77,15 @@ export function getLinks(model: IReadOnlyModel): TPromise<ILink[]> {
});
}
function union(oldLinks: ILink[], newLinks: ILink[]): ILink[] {
function union(oldLinks: Link[], newLinks: Link[]): Link[] {
// reunite oldLinks with newLinks and remove duplicates
var result: ILink[] = [],
var result: Link[] = [],
oldIndex: number,
oldLen: number,
newIndex: number,
newLen: number,
oldLink: ILink,
newLink: ILink,
oldLink: Link,
newLink: Link,
comparisonResult: number;
for (oldIndex = 0, newIndex = 0, oldLen = oldLinks.length, newLen = newLinks.length; oldIndex < oldLen && newIndex < newLen;) {

1
src/vs/monaco.d.ts vendored
View file

@ -4484,6 +4484,7 @@ declare module monaco.languages {
*/
export interface LinkProvider {
provideLinks(model: editor.IReadOnlyModel, token: CancellationToken): ILink[] | Thenable<ILink[]>;
resolveLink?: (link: ILink, token: CancellationToken) => ILink | Thenable<ILink>;
}
export interface IResourceEdit {

View file

@ -322,12 +322,12 @@ export namespace DocumentLink {
export function from(link: types.DocumentLink): modes.ILink {
return {
range: fromRange(link.range),
url: link.target.toString()
url: link.target && link.target.toString()
};
}
export function to(link: modes.ILink):types.DocumentLink {
return new types.DocumentLink(toRange(link.range), URI.parse(link.url));
return new types.DocumentLink(toRange(link.range), link.url && URI.parse(link.url));
}
}

View file

@ -836,7 +836,7 @@ export class DocumentLink {
target: URI;
constructor(range: Range, target: URI) {
if (!(target instanceof URI)) {
if (target && !(target instanceof URI)) {
throw illegalArgument('target');
}
if (!Range.isRange(range) || range.isEmpty) {