Skip to content

Commit

Permalink
vscode: use ILanguageFeatureDebounceService for outline model, micr…
Browse files Browse the repository at this point in the history
…osoft/vscode#140557

Commit: 4135ba294b4027440523ed3383ba6f56049e776e
  • Loading branch information
Johannes Rieken authored and sourcegraph-bot committed Jan 17, 2022
1 parent d2725aa commit af76107
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ namespace IdentityHash {
}
}

export class FeatureDebounceInformation implements IFeatureDebounceInformation {
class FeatureDebounceInformation implements IFeatureDebounceInformation {

private readonly _cache = new LRUCache<string, SlidingWindowAverage>(50, 0.7);

Expand Down
22 changes: 5 additions & 17 deletions vscode/src/vs/editor/contrib/documentSymbols/documentSymbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,20 @@
import { CancellationToken } from 'vs/base/common/cancellation';
import { assertType } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
import { DocumentSymbol } from 'vs/editor/common/languages';
import { IModelService } from 'vs/editor/common/services/model';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';

export async function getDocumentSymbols(document: ITextModel, flat: boolean, token: CancellationToken): Promise<DocumentSymbol[]> {
const model = await OutlineModel.create(document, token);
return flat
? model.asListOfDocumentSymbols()
: model.getTopLevelSymbols();
}

CommandsRegistry.registerCommand('_executeDocumentSymbolProvider', async function (accessor, ...args) {
const [resource] = args;
assertType(URI.isUri(resource));

const model = accessor.get(IModelService).getModel(resource);
if (model) {
return getDocumentSymbols(model, false, CancellationToken.None);
}
const outlineService = accessor.get(IOutlineModelService);
const modelService = accessor.get(ITextModelService);

const reference = await accessor.get(ITextModelService).createModelReference(resource);
const reference = await modelService.createModelReference(resource);
try {
return await getDocumentSymbols(reference.object.textEditorModel, false, CancellationToken.None);
return (await outlineService.getOrCreate(reference.object.textEditorModel, CancellationToken.None)).getTopLevelSymbols();
} finally {
reference.dispose();
}
Expand Down
173 changes: 92 additions & 81 deletions vscode/src/vs/editor/contrib/documentSymbols/outlineModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import { IRange, Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { DocumentSymbol, DocumentSymbolProvider, DocumentSymbolProviderRegistry } from 'vs/editor/common/languages';
import { MarkerSeverity } from 'vs/platform/markers/common/markers';
import { FeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce';
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';

export abstract class TreeElement {

Expand Down Expand Up @@ -206,87 +208,8 @@ export class OutlineGroup extends TreeElement {

export class OutlineModel extends TreeElement {

private static readonly _requestDurations = new FeatureDebounceInformation(DocumentSymbolProviderRegistry, 350, 350); // todo@jrieken ADOPT debounce service
private static readonly _requests = new LRUCache<string, { promiseCnt: number, source: CancellationTokenSource, promise: Promise<any>, model: OutlineModel | undefined }>(9, 0.75);
private static readonly _keys = new class {

private _counter = 1;
private _data = new WeakMap<DocumentSymbolProvider, number>();

for(textModel: ITextModel, version: boolean): string {
return `${textModel.id}/${version ? textModel.getVersionId() : ''}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`;
}

private _hash(providers: DocumentSymbolProvider[]): string {
let result = '';
for (const provider of providers) {
let n = this._data.get(provider);
if (typeof n === 'undefined') {
n = this._counter++;
this._data.set(provider, n);
}
result += n;
}
return result;
}
};


static create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {

let key = this._keys.for(textModel, true);
let data = OutlineModel._requests.get(key);

if (!data) {
let source = new CancellationTokenSource();
data = {
promiseCnt: 0,
source,
promise: OutlineModel._create(textModel, source.token),
model: undefined,
};
OutlineModel._requests.set(key, data);

// keep moving average of request durations
const now = Date.now();
data.promise.then(() => {
this._requestDurations.update(textModel, Date.now() - now);
});
}

if (data!.model) {
// resolved -> return data
return Promise.resolve(data.model!);
}

// increase usage counter
data!.promiseCnt += 1;

token.onCancellationRequested(() => {
// last -> cancel provider request, remove cached promise
if (--data!.promiseCnt === 0) {
data!.source.cancel();
OutlineModel._requests.delete(key);
}
});

return new Promise((resolve, reject) => {
data!.promise.then(model => {
data!.model = model;
resolve(model);
}, err => {
OutlineModel._requests.delete(key);
reject(err);
});
});
}

static getRequestDelay(textModel: ITextModel): number {
return this._requestDurations.get(textModel);
}

private static _create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {

const cts = new CancellationTokenSource(token);
const result = new OutlineModel(textModel.uri);
const provider = DocumentSymbolProviderRegistry.ordered(textModel);
Expand Down Expand Up @@ -321,7 +244,7 @@ export class OutlineModel extends TreeElement {

return Promise.all(promises).then(() => {
if (cts.token.isCancellationRequested && !token.isCancellationRequested) {
return OutlineModel._create(textModel, token);
return OutlineModel.create(textModel, token);
} else {
return result._compact();
}
Expand Down Expand Up @@ -485,3 +408,91 @@ export class OutlineModel extends TreeElement {
}
}
}


export const IOutlineModelService = createDecorator<IOutlineModelService>('IOutlineModelService');

export interface IOutlineModelService {
_serviceBrand: undefined;
getOrCreate(model: ITextModel, token: CancellationToken): Promise<OutlineModel>;
getDebounceValue(textModel: ITextModel): number;
}

interface CacheEntry {
versionId: number;
provider: DocumentSymbolProvider[];

promiseCnt: number;
source: CancellationTokenSource;
promise: Promise<OutlineModel>;
model: OutlineModel | undefined;
}

export class OutlineModelService {

declare _serviceBrand: undefined;

private readonly _debounceInformation: IFeatureDebounceInformation;
private readonly _cache = new LRUCache<string, CacheEntry>(10, 0.7);

constructor(
@ILanguageFeatureDebounceService debounces: ILanguageFeatureDebounceService
) {
this._debounceInformation = debounces.for(DocumentSymbolProviderRegistry, { min: 350 });
}

async getOrCreate(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {

const provider = DocumentSymbolProviderRegistry.ordered(textModel);

let data = this._cache.get(textModel.id);
if (!data || data.versionId !== textModel.getVersionId() || !equals(data.provider, provider)) {
let source = new CancellationTokenSource();
data = {
versionId: textModel.getVersionId(),
provider,
promiseCnt: 0,
source,
promise: OutlineModel.create(textModel, source.token),
model: undefined,
};
this._cache.set(textModel.id, data);

const now = Date.now();
data.promise.then(outlineModel => {
data!.model = outlineModel;
this._debounceInformation.update(textModel, Date.now() - now);
}).catch(_err => {
this._cache.delete(textModel.id);
});
}

if (data.model) {
// resolved -> return data
return data.model;
}

// increase usage counter
data.promiseCnt += 1;

const listener = token.onCancellationRequested(() => {
// last -> cancel provider request, remove cached promise
if (--data!.promiseCnt === 0) {
data!.source.cancel();
this._cache.delete(textModel.id);
}
});

try {
return await data.promise;
} finally {
listener.dispose();
}
}

getDebounceValue(textModel: ITextModel): number {
return this._debounceInformation.get(textModel);
}
}

registerSingleton(IOutlineModelService, OutlineModelService, true);
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance
import { URI } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
import { DocumentSymbol, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/languages';
import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
import { createTextModel } from 'vs/editor/test/common/testTextModel';
import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers';
import { OutlineElement, OutlineGroup, OutlineModel } from '../outlineModel';
import { OutlineElement, OutlineGroup, OutlineModel, OutlineModelService } from '../outlineModel';

suite('OutlineModel', function () {

test('OutlineModel#create, cached', async function () {
const service = new OutlineModelService(new LanguageFeatureDebounceService());

let model = createTextModel('foo', undefined, undefined, URI.file('/fome/path.foo'));
let count = 0;
Expand All @@ -25,16 +27,16 @@ suite('OutlineModel', function () {
}
});

await OutlineModel.create(model, CancellationToken.None);
await service.getOrCreate(model, CancellationToken.None);
assert.strictEqual(count, 1);

// cached
await OutlineModel.create(model, CancellationToken.None);
await service.getOrCreate(model, CancellationToken.None);
assert.strictEqual(count, 1);

// new version
model.applyEdits([{ text: 'XXX', range: new Range(1, 1, 1, 1) }]);
await OutlineModel.create(model, CancellationToken.None);
await service.getOrCreate(model, CancellationToken.None);
assert.strictEqual(count, 2);

reg.dispose();
Expand All @@ -43,6 +45,7 @@ suite('OutlineModel', function () {

test('OutlineModel#create, cached/cancel', async function () {

const service = new OutlineModelService(new LanguageFeatureDebounceService());
let model = createTextModel('foo', undefined, undefined, URI.file('/fome/path.foo'));
let isCancelled = false;

Expand All @@ -59,9 +62,9 @@ suite('OutlineModel', function () {

assert.strictEqual(isCancelled, false);
let s1 = new CancellationTokenSource();
OutlineModel.create(model, s1.token);
service.getOrCreate(model, s1.token);
let s2 = new CancellationTokenSource();
OutlineModel.create(model, s2.token);
service.getOrCreate(model, s2.token);

s1.cancel();
assert.strictEqual(isCancelled, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { IRange, Range } from 'vs/editor/common/core/range';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { DocumentSymbol, DocumentSymbolProviderRegistry, SymbolKind, SymbolKinds, SymbolTag } from 'vs/editor/common/languages';
import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess';
import { localize } from 'vs/nls';
import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
Expand All @@ -38,7 +38,10 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit

protected override readonly options: IGotoSymbolQuickAccessProviderOptions;

constructor(options: IGotoSymbolQuickAccessProviderOptions = Object.create(null)) {
constructor(
@IOutlineModelService private readonly _outlineModelService: IOutlineModelService,
options: IGotoSymbolQuickAccessProviderOptions = Object.create(null)
) {
super(options);

this.options = options;
Expand Down Expand Up @@ -421,7 +424,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
}

protected async getDocumentSymbols(document: ITextModel, token: CancellationToken): Promise<DocumentSymbol[]> {
const model = await OutlineModel.create(document, token);
const model = await this._outlineModelService.getOrCreate(document, token);
return token.isCancellationRequested ? [] : model.asListOfDocumentSymbols();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/outlineModel';

export class StandaloneGotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider {

protected readonly onDidActiveTextEditorControlChange = Event.None;

constructor(@ICodeEditorService private readonly editorService: ICodeEditorService) {
super();
constructor(
@ICodeEditorService private readonly editorService: ICodeEditorService,
@IOutlineModelService outlineModelService: IOutlineModelService,
) {
super(outlineModelService);
}

protected get activeTextEditorControl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import { IEditorPane } from 'vs/workbench/common/editor';
import { DocumentSymbolComparator, DocumentSymbolAccessibilityProvider, DocumentSymbolRenderer, DocumentSymbolFilter, DocumentSymbolGroupRenderer, DocumentSymbolIdentityProvider, DocumentSymbolNavigationLabelProvider, DocumentSymbolVirtualDelegate } from 'vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree';
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { OutlineGroup, OutlineElement, OutlineModel, TreeElement, IOutlineMarker } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { OutlineGroup, OutlineElement, OutlineModel, TreeElement, IOutlineMarker, IOutlineModelService } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { DocumentSymbolProviderRegistry } from 'vs/editor/common/languages';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { raceCancellation, TimeoutTimer, timeout, Barrier } from 'vs/base/common/async';
Expand Down Expand Up @@ -133,6 +133,7 @@ class DocumentSymbolsOutline implements IOutline<DocumentSymbolItem> {
target: OutlineTarget,
firstLoadBarrier: Barrier,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IOutlineModelService private readonly _outlineModelService: IOutlineModelService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService,
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
Expand Down Expand Up @@ -190,7 +191,7 @@ class DocumentSymbolsOutline implements IOutline<DocumentSymbolItem> {
this._disposables.add(this._editor.onDidChangeModelContent(event => {
const model = this._editor.getModel();
if (model) {
const timeout = OutlineModel.getRequestDelay(model);
const timeout = _outlineModelService.getDebounceValue(model);
updateSoon.cancelAndSet(() => this._createOutline(event), timeout);
}
}));
Expand Down Expand Up @@ -276,7 +277,7 @@ class DocumentSymbolsOutline implements IOutline<DocumentSymbolItem> {
this._outlineDisposables.add(toDisposable(() => cts.dispose(true)));

try {
let model = await OutlineModel.create(buffer, cts.token);
let model = await this._outlineModelService.getOrCreate(buffer, cts.token);
if (cts.token.isCancellationRequested) {
// cancelled -> do nothing
return;
Expand Down
Loading

0 comments on commit af76107

Please sign in to comment.