add 'vscode.executeLinkProvider' command

This commit is contained in:
Johannes Rieken 2016-07-25 14:57:03 +02:00
parent c1b999fdb0
commit d9067043bc
6 changed files with 130 additions and 100 deletions

View file

@ -8,7 +8,7 @@ import URI from 'vs/base/common/uri';
import {TPromise} from 'vs/base/common/winjs.base';
import {createDecorator} from 'vs/platform/instantiation/common/instantiation';
import {IChange, ILineChange, IPosition, IRange} from 'vs/editor/common/editorCommon';
import {IInplaceReplaceSupportResult, ILink, ISuggestResult} from 'vs/editor/common/modes';
import {IInplaceReplaceSupportResult, ISuggestResult} from 'vs/editor/common/modes';
export var ID_EDITOR_WORKER_SERVICE = 'editorWorkerService';
export var IEditorWorkerService = createDecorator<IEditorWorkerService>(ID_EDITOR_WORKER_SERVICE);
@ -18,7 +18,6 @@ export interface IEditorWorkerService {
computeDiff(original:URI, modified:URI, ignoreTrimWhitespace:boolean):TPromise<ILineChange[]>;
computeDirtyDiff(original:URI, modified:URI, ignoreTrimWhitespace:boolean):TPromise<IChange[]>;
computeLinks(resource:URI):TPromise<ILink[]>;
textualSuggest(resource: URI, position: IPosition): TPromise<ISuggestResult[]>;
navigateValueSet(resource: URI, range:IRange, up:boolean): TPromise<IInplaceReplaceSupportResult>;
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import {IntervalTimer, ShallowCancelThenPromise} from 'vs/base/common/async';
import {IntervalTimer, ShallowCancelThenPromise, wireCancellationToken} from 'vs/base/common/async';
import {Disposable, IDisposable, dispose} from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import {TPromise} from 'vs/base/common/winjs.base';
@ -12,7 +12,7 @@ import {SimpleWorkerClient} from 'vs/base/common/worker/simpleWorker';
import {DefaultWorkerFactory} from 'vs/base/worker/defaultWorkerFactory';
import * as editorCommon from 'vs/editor/common/editorCommon';
import {WordHelper} from 'vs/editor/common/model/textModelWithTokensHelpers';
import {IInplaceReplaceSupportResult, ILink, ISuggestResult} from 'vs/editor/common/modes';
import {IInplaceReplaceSupportResult, ILink, ISuggestResult, LinkProviderRegistry, LinkProvider} from 'vs/editor/common/modes';
import {IEditorWorkerService} from 'vs/editor/common/services/editorWorkerService';
import {IModelService} from 'vs/editor/common/services/modelService';
import {EditorSimpleWorkerImpl} from 'vs/editor/common/services/editorSimpleWorker';
@ -31,10 +31,23 @@ const STOP_WORKER_DELTA_TIME_MS = 5 * 60 * 1000;
export class EditorWorkerServiceImpl implements IEditorWorkerService {
public _serviceBrand: any;
private _workerManager:WorkerManager;
private _workerManager: WorkerManager;
private _registration: IDisposable;
constructor(@IModelService modelService:IModelService) {
this._workerManager = new WorkerManager(modelService);
// todo@joh make sure this happens only once
this._registration = LinkProviderRegistry.register('*', <LinkProvider>{
provideLinks: (model, token) => {
return wireCancellationToken(token, this._workerManager.withWorker().then(client => client.computeLinks(model.uri)));
}
});
}
public dispose(): void {
this._workerManager.dispose();
this._registration.dispose();
}
public computeDiff(original:URI, modified:URI, ignoreTrimWhitespace:boolean):TPromise<editorCommon.ILineChange[]> {
@ -45,10 +58,6 @@ export class EditorWorkerServiceImpl implements IEditorWorkerService {
return this._workerManager.withWorker().then(client => client.computeDirtyDiff(original, modified, ignoreTrimWhitespace));
}
public computeLinks(resource:URI):TPromise<ILink[]> {
return this._workerManager.withWorker().then(client => client.computeLinks(resource));
}
public textualSuggest(resource: URI, position: editorCommon.IPosition): TPromise<ISuggestResult[]> {
return this._workerManager.withWorker().then(client => client.textualSuggest(resource, position));
}

View file

@ -16,7 +16,6 @@ import {TPromise} from 'vs/base/common/winjs.base';
import {IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import {IEditorService, IResourceInput} from 'vs/platform/editor/common/editor';
import {IMessageService} from 'vs/platform/message/common/message';
import {Range} from 'vs/editor/common/core/range';
import {EditorAction} from 'vs/editor/common/editorAction';
import {Behaviour} from 'vs/editor/common/editorActionEnablement';
import * as editorCommon from 'vs/editor/common/editorCommon';
@ -75,16 +74,6 @@ class LinkOccurence {
}
}
class Link {
range: Range;
url: string;
constructor(source:ILink) {
this.range = new Range(source.range.startLineNumber, source.range.startColumn, source.range.endLineNumber, source.range.endColumn);
this.url = source.url;
}
}
class LinkDetector implements editorCommon.IEditorContribution {
public static ID: string = 'editor.linkDetector';
@ -102,7 +91,7 @@ class LinkDetector implements editorCommon.IEditorContribution {
private editor:ICodeEditor;
private listenersToRemove:IDisposable[];
private timeoutPromise:TPromise<void>;
private computePromise:TPromise<ILink[]>;
private computePromise:TPromise<void>;
private activeLinkDecorationId:string;
private lastMouseEvent:IEditorMouseEvent;
private editorService:IEditorService;
@ -172,75 +161,16 @@ class LinkDetector implements editorCommon.IEditorContribution {
return;
}
let modePromise:TPromise<ILink[]> = TPromise.as(null);
if (LinkProviderRegistry.has(this.editor.getModel())) {
modePromise = getLinks(this.editor.getModel());
if (!LinkProviderRegistry.has(this.editor.getModel())) {
return;
}
let standardPromise:TPromise<ILink[]> = this.editorWorkerService.computeLinks(this.editor.getModel().uri);
this.computePromise = TPromise.join([modePromise, standardPromise]).then((r) => {
let a = r[0];
let b = r[1];
if (!a || a.length === 0) {
return b || [];
}
if (!b || b.length === 0) {
return a || [];
}
return LinkDetector._linksUnion(a.map(el => new Link(el)), b.map(el => new Link(el)));
});
this.computePromise.then((links:ILink[]) => {
this.computePromise = getLinks(this.editor.getModel()).then(links => {
this.updateDecorations(links);
this.computePromise = null;
});
}
private static _linksUnion(oldLinks: Link[], newLinks: Link[]): Link[] {
// reunite oldLinks with newLinks and remove duplicates
var result: Link[] = [],
oldIndex: number,
oldLen: number,
newIndex: number,
newLen: number,
oldLink: Link,
newLink: Link,
comparisonResult: number;
for (oldIndex = 0, newIndex = 0, oldLen = oldLinks.length, newLen = newLinks.length; oldIndex < oldLen && newIndex < newLen;) {
oldLink = oldLinks[oldIndex];
newLink = newLinks[newIndex];
if (Range.areIntersectingOrTouching(oldLink.range, newLink.range)) {
// Remove the oldLink
oldIndex++;
continue;
}
comparisonResult = Range.compareRangesUsingStarts(oldLink.range, newLink.range);
if (comparisonResult < 0) {
// oldLink is before
result.push(oldLink);
oldIndex++;
} else {
// newLink is before
result.push(newLink);
newIndex++;
}
}
for (; oldIndex < oldLen; oldIndex++) {
result.push(oldLinks[oldIndex]);
}
for (; newIndex < newLen; newIndex++) {
result.push(newLinks[newIndex]);
}
return result;
}
private updateDecorations(links:ILink[]):void {
this.editor.changeDecorations((changeAccessor:editorCommon.IModelDecorationsChangeAccessor) => {
var oldDecorations:string[] = [];

View file

@ -6,32 +6,88 @@
'use strict';
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 {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[]> {
const promises = LinkProviderRegistry.ordered(model).map((support) => {
return asWinJsPromise((token) => {
return support.provideLinks(model, token);
}).then((result) => {
let links: ILink[] = [];
// ask all providers for links in parallel
const promises = LinkProviderRegistry.ordered(model).reverse().map(support => {
return asWinJsPromise(token => support.provideLinks(model, token)).then(result => {
if (Array.isArray(result)) {
return <ILink[]> result;
links = union(links, result);
}
}, err => {
onUnexpectedError(err);
});
}, onUnexpectedError);
});
return TPromise.join(promises).then(manyLinks => {
let result: ILink[] = [];
for (let links of manyLinks) {
if (links) {
result = result.concat(links);
}
}
return result;
return TPromise.join(promises).then(() => {
return links;
});
}
function union(oldLinks: ILink[], newLinks: ILink[]): ILink[] {
// reunite oldLinks with newLinks and remove duplicates
var result: ILink[] = [],
oldIndex: number,
oldLen: number,
newIndex: number,
newLen: number,
oldLink: ILink,
newLink: ILink,
comparisonResult: number;
for (oldIndex = 0, newIndex = 0, oldLen = oldLinks.length, newLen = newLinks.length; oldIndex < oldLen && newIndex < newLen;) {
oldLink = oldLinks[oldIndex];
newLink = newLinks[newIndex];
if (Range.areIntersectingOrTouching(oldLink.range, newLink.range)) {
// Remove the oldLink
oldIndex++;
continue;
}
comparisonResult = Range.compareRangesUsingStarts(oldLink.range, newLink.range);
if (comparisonResult < 0) {
// oldLink is before
result.push(oldLink);
oldIndex++;
} else {
// newLink is before
result.push(newLink);
newIndex++;
}
}
for (; oldIndex < oldLen; oldIndex++) {
result.push(oldLinks[oldIndex]);
}
for (; newIndex < newLen; newIndex++) {
result.push(newLinks[newIndex]);
}
return result;
}
CommandsRegistry.registerCommand('_executeLinkProvider', (accessor, ...args) => {
const [uri] = args;
if (!(uri instanceof URI)) {
return;
}
const model = accessor.get(IModelService).getModel(uri);
if (!model) {
return;
}
return getLinks(model);
});

View file

@ -147,7 +147,13 @@ class ExtHostApiCommands {
],
returns: 'A promise that resolves to an array of TextEdits.'
});
this._register('vscode.executeLinkProvider', this._executeDocumentLinkProvider, {
description: 'Execute document link provider.',
args: [
{ name: 'uri', description: 'Uri of a text document', constraint: URI }
],
returns: 'A promise that resolves to an array of DocumentLink-instances.'
});
this._register('vscode.previewHtml', (uri: URI, position?: vscode.ViewColumn, label?: string) => {
return this._commands.executeCommand('_workbench.previewHtml',
@ -417,4 +423,12 @@ class ExtHostApiCommands {
}
});
}
private _executeDocumentLinkProvider(resource: URI): Thenable<vscode.DocumentLink[]> {
return this._commands.executeCommand<modes.ILink[]>('_executeLinkProvider', resource).then(value => {
if (Array.isArray(value)) {
return value.map(typeConverters.DocumentLink.to);
}
});
}
}

View file

@ -401,4 +401,26 @@ suite('ExtHostLanguageFeatureCommands', function() {
});
});
});
test('Links, back and forth', function() {
disposables.push(extHost.registerDocumentLinkProvider(defaultSelector, <vscode.DocumentLinkProvider>{
provideDocumentLinks(): any {
return [new types.DocumentLink(new types.Range(0, 0, 0, 20), URI.parse('foo:bar'))];
}
}));
return threadService.sync().then(() => {
return commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', model.uri).then(value => {
assert.equal(value.length, 1);
let [first] = value;
assert.equal(first.target.toString(), 'foo:bar');
assert.equal(first.range.start.line, 0);
assert.equal(first.range.start.character, 0);
assert.equal(first.range.end.line, 0);
assert.equal(first.range.end.character, 20);
});
});
});
});