get children when node expands (#201909)

* get children when node expands

* switch to asynchronous tree for more natural lazy loading

* store variables by ID so we can look up the correct instance
This commit is contained in:
Aaron Munger
2024-01-05 15:44:43 -08:00
committed by GitHub
parent bff3ef7651
commit f88bce8fe6
13 changed files with 164 additions and 94 deletions
@@ -101,7 +101,7 @@ abstract class MainThreadKernel implements INotebookKernel {
abstract executeNotebookCellsRequest(uri: URI, cellHandles: number[]): Promise<void>;
abstract cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise<void>;
abstract provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult>;
abstract provideVariables(notebookUri: URI, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult>;
}
class MainThreadKernelDetectionTask implements INotebookKernelDetectionTask {
@@ -242,7 +242,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape
async cancelNotebookCellExecution(uri: URI, handles: number[]): Promise<void> {
await that._proxy.$cancelCells(handle, uri, handles);
}
provideVariables(notebookUri: URI, parentName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
provideVariables(notebookUri: URI, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
const requestId = `${handle}variables${that.variableRequestIndex++}`;
if (that.variableRequestMap.has(requestId)) {
return that.variableRequestMap.get(requestId)!.asyncIterable;
@@ -250,7 +250,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape
const source = new AsyncIterableSource<VariablesResult>();
that.variableRequestMap.set(requestId, source);
that._proxy.$provideVariables(handle, requestId, notebookUri, parentName, kind, start, token).then(() => {
that._proxy.$provideVariables(handle, requestId, notebookUri, parentId, kind, start, token).then(() => {
source.resolve();
that.variableRequestMap.delete(requestId);
}).catch((err) => {
@@ -80,7 +80,6 @@ import { CandidatePort } from 'vs/workbench/services/remote/common/tunnelModel';
import { ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder';
import * as search from 'vs/workbench/services/search/common/search';
import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { VariablesResult } from 'vscode';
export interface IWorkspaceData extends IStaticWorkspaceData {
folders: { uri: UriComponents; name: string; index: number }[];
@@ -1114,6 +1113,14 @@ export interface ICellExecutionCompleteDto extends ICellExecutionComplete {
export type ICellExecuteUpdateDto = ICellExecuteOutputEditDto | ICellExecuteOutputItemEditDto | ICellExecutionStateUpdateDto;
export interface VariablesResult {
id: number;
name: string;
value: string;
hasNamedChildren: boolean;
indexedChildrenCount: number;
}
export interface MainThreadNotebookKernelsShape extends IDisposable {
$postMessage(handle: number, editorId: string | undefined, message: any): Promise<boolean>;
$addKernel(handle: number, data: INotebookKernelDto2): Promise<void>;
@@ -2544,7 +2551,7 @@ export interface ExtHostNotebookKernelsShape {
$acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void;
$cellExecutionChanged(uri: UriComponents, cellHandle: number, state: notebookCommon.NotebookCellExecutionState | undefined): void;
$provideKernelSourceActions(handle: number, token: CancellationToken): Promise<notebookCommon.INotebookKernelSourceAction[]>;
$provideVariables(handle: number, requestId: string, notebookUri: UriComponents, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): Promise<void>;
$provideVariables(handle: number, requestId: string, notebookUri: UriComponents, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): Promise<void>;
}
export interface ExtHostInteractiveShape {
@@ -415,7 +415,10 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape {
}
}
async $provideVariables(handle: number, requestId: string, notebookUri: UriComponents, parentName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): Promise<void> {
private id = 0;
private variableStore: Record<string, vscode.Variable> = {};
async $provideVariables(handle: number, requestId: string, notebookUri: UriComponents, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): Promise<void> {
const obj = this._kernelData.get(handle);
if (!obj) {
return;
@@ -427,13 +430,34 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape {
return;
}
const parent = parentName ? { name: parentName, value: '' } : undefined;
let parent: vscode.Variable | undefined = undefined;
if (parentId) {
parent = this.variableStore[parentId];
if (!parent) {
// request for unknown parent
return;
}
} else {
// root request, clear store
this.variableStore = {};
}
const requestKind = kind === 'named' ? NotebookVariablesRequestKind.Named : NotebookVariablesRequestKind.Indexed;
const variables = variableProvider.provideVariables(document.apiNotebook, parent, requestKind, start, token);
for await (const variable of variables) {
const variableResults = variableProvider.provideVariables(document.apiNotebook, parent, requestKind, start, token);
for await (const result of variableResults) {
if (token.isCancellationRequested) {
return;
}
const variable = {
id: this.id++,
name: result.variable.name,
value: result.variable.value,
hasNamedChildren: result.hasNamedChildren,
indexedChildrenCount: result.indexedChildrenCount
};
this.variableStore[variable.id] = result.variable;
this._proxy.$receiveVariable(requestId, variable);
}
}
@@ -43,7 +43,8 @@ export class NotebookVariables extends Disposable implements IWorkbenchContribut
if (debugViewContainer) {
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
const viewDescriptor = {
id: 'NOTEBOOK_VARIABLES', name: nls.localize2('notebookVariables', "Notebook Variables"), containerIcon: variablesViewIcon, ctorDescriptor: new SyncDescriptor(NotebookVariablesView),
id: 'NOTEBOOK_VARIABLES', name: nls.localize2('notebookVariables', "Notebook Variables"),
containerIcon: variablesViewIcon, ctorDescriptor: new SyncDescriptor(NotebookVariablesView),
order: 50, weight: 5, canToggleVisibility: true, canMoveView: true, collapsed: true, when: ContextKeyExpr.notEquals(NOTEBOOK_KERNEL.key, ''),
};
@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
import { CancellationToken } from 'vs/base/common/cancellation';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
export interface INotebookScope {
type: 'root';
readonly notebook: NotebookTextModel | undefined;
}
export interface INotebookVariableElement {
type: 'variable';
readonly id: number;
readonly name: string;
readonly value: string;
readonly indexedChildrenCount: number;
readonly hasNamedChildren: boolean;
}
export class NotebookVariableDataSource implements IAsyncDataSource<INotebookScope, INotebookVariableElement> {
private notebook: NotebookTextModel | undefined = undefined;
constructor(private readonly notebookKernelService: INotebookKernelService) { }
hasChildren(element: INotebookScope | INotebookVariableElement): boolean {
return element.type === 'root' || element.hasNamedChildren || element.indexedChildrenCount > 0;
}
async getChildren(element: INotebookScope | INotebookVariableElement): Promise<Array<INotebookVariableElement>> {
if (element.type === 'root') {
this.notebook = element.notebook;
return this.getRootVariables();
} else {
return this.getVariables(element);
}
}
async getVariables(parent: INotebookVariableElement): Promise<INotebookVariableElement[]> {
if (!this.notebook) {
return [];
}
const selectedKernel = this.notebookKernelService.getMatchingKernel(this.notebook).selected;
if (selectedKernel && selectedKernel.hasVariableProvider) {
let children: INotebookVariableElement[] = [];
if (parent.hasNamedChildren) {
const variables = selectedKernel.provideVariables(this.notebook.uri, parent.id, 'named', 0, CancellationToken.None);
const childNodes = await variables
.map(variable => { return this.createVariableElement(variable); })
.toPromise();
children = children.concat(childNodes);
}
if (parent.indexedChildrenCount > 0) {
const variables = selectedKernel.provideVariables(this.notebook.uri, parent.id, 'indexed', 0, CancellationToken.None);
const childNodes = await variables
.map(variable => { return this.createVariableElement(variable); })
.toPromise();
children = children.concat(childNodes);
}
return children;
}
return [];
}
async getRootVariables(): Promise<INotebookVariableElement[]> {
if (!this.notebook) {
return [];
}
const selectedKernel = this.notebookKernelService.getMatchingKernel(this.notebook).selected;
if (selectedKernel && selectedKernel.hasVariableProvider) {
const variables = selectedKernel.provideVariables(this.notebook.uri, undefined, 'named', 0, CancellationToken.None);
return await variables
.map(variable => { return this.createVariableElement(variable); })
.toPromise();
}
return [];
}
private createVariableElement(variable: VariablesResult): INotebookVariableElement {
return {
type: 'variable',
...variable
};
}
}
@@ -10,12 +10,7 @@ import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
import { FuzzyScore } from 'vs/base/common/filters';
import { localize } from 'vs/nls';
import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
export interface INotebookVariableElement {
readonly id: string;
readonly label: string;
readonly value: string;
}
import { INotebookVariableElement } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource';
export class NotebookVariablesTree extends WorkbenchObjectTree<INotebookVariableElement> { }
@@ -43,8 +38,8 @@ export class NotebookVariableRenderer implements ITreeRenderer<INotebookVariable
return { wrapper };
}
renderElement(element: ITreeNode<INotebookVariableElement, FuzzyScore>, index: number, templateData: { wrapper: HTMLElement }, height: number | undefined): void {
templateData.wrapper.innerText = `${element.element.label} - ${element.element.value}`;
renderElement(element: ITreeNode<INotebookVariableElement, FuzzyScore>, _index: number, templateData: { wrapper: HTMLElement }): void {
templateData.wrapper.innerText = `${element.element.name}: ${element.element.value}`;
}
disposeTemplate(): void {
@@ -59,6 +54,6 @@ export class NotebookVariableAccessibilityProvider implements IListAccessibility
}
getAriaLabel(element: INotebookVariableElement): string {
return localize('notebookVariableAriaLabel', "Variable {0}, value {1}", element.label, element.value);
return localize('notebookVariableAriaLabel', "Variable {0}, value {1}", element.name, element.value);
}
}
@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IObjectTreeElement } from 'vs/base/browser/ui/tree/tree';
import { CancellationToken } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { ILocalizedString } from 'vs/platform/action/common/action';
@@ -14,18 +12,19 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { NotebookVariableAccessibilityProvider, NotebookVariableRenderer, INotebookVariableElement, NotebookVariablesDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree';
import { INotebookScope, INotebookVariableElement, NotebookVariableDataSource } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource';
import { NotebookVariableAccessibilityProvider, NotebookVariableRenderer, NotebookVariablesDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree';
import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
import { INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
export class NotebookVariablesView extends ViewPane {
@@ -33,7 +32,7 @@ export class NotebookVariablesView extends ViewPane {
static readonly ID = 'notebookVariablesView';
static readonly TITLE: ILocalizedString = nls.localize2('notebook.notebookVariables', "Notebook Variables");
private tree: WorkbenchObjectTree<INotebookVariableElement> | undefined;
private tree: WorkbenchAsyncDataTree<INotebookScope, INotebookVariableElement> | undefined;
private activeNotebook: NotebookTextModel | undefined;
constructor(
@@ -63,23 +62,20 @@ export class NotebookVariablesView extends ViewPane {
protected override renderBody(container: HTMLElement): void {
super.renderBody(container);
this.tree = <WorkbenchObjectTree<INotebookVariableElement>>this.instantiationService.createInstance(
WorkbenchObjectTree,
this.tree = <WorkbenchAsyncDataTree<INotebookScope, INotebookVariableElement>>this.instantiationService.createInstance(
WorkbenchAsyncDataTree,
'notebookVariablesTree',
container,
new NotebookVariablesDelegate(),
[new NotebookVariableRenderer()],
new NotebookVariableDataSource(this.notebookKernelService),
{
identityProvider: { getId: (e: INotebookVariableElement) => e.id },
horizontalScrolling: false,
supportDynamicHeights: true,
hideTwistiesOfChildlessElements: true,
accessibilityProvider: new NotebookVariableAccessibilityProvider(),
setRowLineHeight: false,
identityProvider: { getId: (e: INotebookVariableElement) => e.id },
});
this.tree.layout();
this.tree?.setChildren(null, []);
this.tree.setInput({ type: 'root', notebook: this.activeNotebook });
}
protected override layoutBody(height: number, width: number): void {
@@ -93,7 +89,8 @@ export class NotebookVariablesView extends ViewPane {
const notebookDocument = getNotebookEditorFromEditorPane(activeEditorPane)?.getViewModel()?.notebookDocument;
if (notebookDocument && notebookDocument !== this.activeNotebook) {
this.activeNotebook = notebookDocument;
this.updateVariables(this.activeNotebook);
this.tree?.setInput({ type: 'root', notebook: this.activeNotebook });
this.tree?.updateChildren();
}
}
}
@@ -102,60 +99,15 @@ export class NotebookVariablesView extends ViewPane {
if (this.activeNotebook) {
// changed === undefined -> excecution ended
if (event.changed === undefined && event.affectsNotebook(this.activeNotebook?.uri)) {
this.updateVariables(this.activeNotebook);
this.tree?.updateChildren();
}
}
}
private handleVariablesChanged(notebookUri: URI) {
if (this.activeNotebook && notebookUri.toString() === this.activeNotebook.uri.toString()) {
this.updateVariables(this.activeNotebook);
this.tree?.setInput({ type: 'root', notebook: this.activeNotebook });
this.tree?.updateChildren();
}
}
private async updateVariables(notebook: NotebookTextModel) {
const selectedKernel = this.notebookKernelService.getMatchingKernel(notebook).selected;
if (selectedKernel && selectedKernel.hasVariableProvider) {
const variables = selectedKernel.provideVariables(notebook.uri, undefined, 'named', 0, CancellationToken.None);
const treeData = await variables
.map(variable => { return this.createTreeItem(variable); })
.toPromise();
this.tree?.setChildren(null, treeData);
}
}
private index = 0;
private createTreeItem(variable: VariablesResult): IObjectTreeElement<INotebookVariableElement> {
let collapsed: boolean | undefined = undefined;
let children: IObjectTreeElement<INotebookVariableElement>[] | undefined = undefined;
if (variable.namedChildrenCount > 0 || variable.indexedChildrenCount > 0) {
collapsed = true;
children = [
{
element: {
id: `${this.index + 1}-placeholder`,
label: ' ',
value: 'loading...',
}
}
];
}
const element = {
element: {
id: `${this.index++}`,
label: variable.variable.name,
value: variable.variable.value,
},
children,
collapsed
};
return element;
}
}
@@ -38,14 +38,11 @@ export interface INotebookKernelChangeEvent {
hasVariableProvider?: true;
}
export interface Variable {
export interface VariablesResult {
id: number;
name: string;
value: string;
}
export interface VariablesResult {
variable: Variable;
namedChildrenCount: number;
hasNamedChildren: boolean;
indexedChildrenCount: number;
}
@@ -70,7 +67,7 @@ export interface INotebookKernel {
executeNotebookCellsRequest(uri: URI, cellHandles: number[]): Promise<void>;
cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise<void>;
provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult>;
provideVariables(notebookUri: URI, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult>;
}
export const enum ProxyKernelState {
@@ -174,7 +174,7 @@ class TestNotebookKernel implements INotebookKernel {
preloadUris: URI[] = [];
preloadProvides: string[] = [];
supportedLanguages: string[] = [];
provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
provideVariables(notebookUri: URI, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
return AsyncIterableObject.EMPTY;
}
executeNotebookCellsRequest(): Promise<void> {
@@ -362,7 +362,7 @@ class TestNotebookKernel implements INotebookKernel {
supportedLanguages: string[] = [];
async executeNotebookCellsRequest(): Promise<void> { }
async cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise<void> { }
provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
provideVariables(notebookUri: URI, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
return AsyncIterableObject.EMPTY;
}
@@ -186,7 +186,7 @@ class TestNotebookKernel implements INotebookKernel {
cancelNotebookCellExecution(): Promise<void> {
throw new Error('Method not implemented.');
}
provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
provideVariables(notebookUri: URI, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
return AsyncIterableObject.EMPTY;
}
@@ -199,7 +199,7 @@ class TestNotebookKernel implements INotebookKernel {
cancelNotebookCellExecution(): Promise<void> {
throw new Error('Method not implemented.');
}
provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
provideVariables(notebookUri: URI, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
return AsyncIterableObject.EMPTY;
}
@@ -16,7 +16,7 @@ declare module 'vscode' {
interface VariablesResult {
variable: Variable;
namedChildrenCount: number;
hasNamedChildren: boolean;
indexedChildrenCount: number;
}