Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/vs/platform/actions/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export class MenuId {
static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle');
static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle');
static readonly NotebookOutputToolbar = new MenuId('NotebookOutputToolbar');
static readonly NotebookOutlineActionMenu = new MenuId('NotebookOutlineActionMenu');
static readonly NotebookEditorLayoutConfigure = new MenuId('NotebookEditorLayoutConfigure');
static readonly NotebookKernelSource = new MenuId('NotebookKernelSource');
static readonly BulkEditTitle = new MenuId('BulkEditTitle');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/

import { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IIconLabelValueOptions, IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
Expand All @@ -25,7 +27,7 @@ import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/co
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { IEditorPane } from 'vs/workbench/common/editor';
import { CellRevealType, ICellModelDecorations, ICellModelDeltaDecorations, INotebookEditorOptions, INotebookEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellFoldingState, CellRevealType, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, INotebookEditor, INotebookEditorOptions, INotebookEditorPane, INotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor';
import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider';
import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
Expand All @@ -37,7 +39,14 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
import { Range } from 'vs/editor/common/core/range';
import { mainWindow } from 'vs/base/browser/window';
import { WindowIdleValue } from 'vs/base/browser/dom';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { MenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IAction } from 'vs/base/common/actions';
import { NotebookSectionArgs } from 'vs/workbench/contrib/notebook/browser/controller/sectionActions';
import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';
import { disposableTimeout } from 'vs/base/common/async';

class NotebookOutlineTemplate {

Expand All @@ -47,7 +56,9 @@ class NotebookOutlineTemplate {
readonly container: HTMLElement,
readonly iconClass: HTMLElement,
readonly iconLabel: IconLabel,
readonly decoration: HTMLElement
readonly decoration: HTMLElement,
readonly actionMenu: HTMLElement,
readonly elementDisposables: DisposableStore,
) { }
}

Expand All @@ -56,19 +67,31 @@ class NotebookOutlineRenderer implements ITreeRenderer<OutlineEntry, FuzzyScore,
templateId: string = NotebookOutlineTemplate.templateId;

constructor(
private readonly _editor: INotebookEditor | undefined,
private readonly _target: OutlineTarget,
@IThemeService private readonly _themeService: IThemeService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IMenuService private readonly _menuService: IMenuService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) { }

renderTemplate(container: HTMLElement): NotebookOutlineTemplate {
const elementDisposables = new DisposableStore();

container.classList.add('notebook-outline-element', 'show-file-icons');
const iconClass = document.createElement('div');
container.append(iconClass);
const iconLabel = new IconLabel(container, { supportHighlights: true });
const decoration = document.createElement('div');
decoration.className = 'element-decoration';
container.append(decoration);
return new NotebookOutlineTemplate(container, iconClass, iconLabel, decoration);
const actionMenu = document.createElement('div');
actionMenu.className = 'action-menu';
container.append(actionMenu);

return new NotebookOutlineTemplate(container, iconClass, iconLabel, decoration, actionMenu, elementDisposables);
}

renderElement(node: ITreeNode<OutlineEntry, FuzzyScore>, _index: number, template: NotebookOutlineTemplate, _height: number | undefined): void {
Expand All @@ -79,7 +102,8 @@ class NotebookOutlineRenderer implements ITreeRenderer<OutlineEntry, FuzzyScore,
extraClasses,
};

if (node.element.cell.cellKind === CellKind.Code && this._themeService.getFileIconTheme().hasFileIcons && !node.element.isExecuting) {
const isCodeCell = node.element.cell.cellKind === CellKind.Code;
if (isCodeCell && this._themeService.getFileIconTheme().hasFileIcons && !node.element.isExecuting) {
template.iconClass.className = '';
extraClasses.push(...getIconClassesForLanguageId(node.element.cell.language ?? ''));
} else {
Expand Down Expand Up @@ -118,13 +142,114 @@ class NotebookOutlineRenderer implements ITreeRenderer<OutlineEntry, FuzzyScore,
template.container.style.setProperty('--outline-element-color', color?.toString() ?? 'inherit');
}
}

if (this._target === OutlineTarget.OutlinePane) {
const nbCell = node.element.cell;
const nbViewModel = this._editor?.getViewModel();
if (!nbViewModel) {
return;
}
const idx = nbViewModel.getCellIndex(nbCell);
const length = isCodeCell ? 0 : nbViewModel.getFoldedLength(idx);

const scopedContextKeyService = template.elementDisposables.add(this._contextKeyService.createScoped(template.container));
NotebookOutlineContext.CellKind.bindTo(scopedContextKeyService).set(isCodeCell ? CellKind.Code : CellKind.Markup);
NotebookOutlineContext.CellHasChildren.bindTo(scopedContextKeyService).set(length > 0);
NotebookOutlineContext.CellHasHeader.bindTo(scopedContextKeyService).set(node.element.level !== 7);
NotebookOutlineContext.OutlineElementTarget.bindTo(scopedContextKeyService).set(this._target);
this.setupFolding(isCodeCell, nbViewModel, scopedContextKeyService, template, nbCell);

const outlineEntryToolbar = template.elementDisposables.add(new ToolBar(template.actionMenu, this._contextMenuService, {
actionViewItemProvider: action => {
if (action instanceof MenuItemAction) {
return this._instantiationService.createInstance(MenuEntryActionViewItem, action, undefined);
}
return undefined;
},
}));

const menu = template.elementDisposables.add(this._menuService.createMenu(MenuId.NotebookOutlineActionMenu, scopedContextKeyService));
const actions = getOutlineToolbarActions(menu, { notebookEditor: this._editor, outlineEntry: node.element });
outlineEntryToolbar.setActions(actions.primary, actions.secondary);

this.setupToolbarListeners(outlineEntryToolbar, menu, actions, node.element, template);
}
}

disposeTemplate(templateData: NotebookOutlineTemplate): void {
templateData.iconLabel.dispose();
templateData.elementDisposables.clear();
}

disposeElement(element: ITreeNode<OutlineEntry, FuzzyScore>, index: number, templateData: NotebookOutlineTemplate, height: number | undefined): void {
templateData.elementDisposables.clear();
DOM.clearNode(templateData.actionMenu);
}

private setupFolding(isCodeCell: boolean, nbViewModel: INotebookViewModel, scopedContextKeyService: IContextKeyService, template: NotebookOutlineTemplate, nbCell: ICellViewModel) {
const foldingState = isCodeCell ? CellFoldingState.None : ((nbCell as MarkupCellViewModel).foldingState);
const foldingStateCtx = NotebookOutlineContext.CellFoldingState.bindTo(scopedContextKeyService);
foldingStateCtx.set(foldingState);

if (!isCodeCell) {
template.elementDisposables.add(nbViewModel.onDidFoldingStateChanged(() => {
const foldingState = (nbCell as MarkupCellViewModel).foldingState;
NotebookOutlineContext.CellFoldingState.bindTo(scopedContextKeyService).set(foldingState);
foldingStateCtx.set(foldingState);
}));
}
}

private setupToolbarListeners(toolbar: ToolBar, menu: IMenu, initActions: { primary: IAction[]; secondary: IAction[] }, entry: OutlineEntry, templateData: NotebookOutlineTemplate): void {
// same fix as in cellToolbars setupListeners re #103926
let dropdownIsVisible = false;
let deferredUpdate: (() => void) | undefined;

toolbar.setActions(initActions.primary, initActions.secondary);
templateData.elementDisposables.add(menu.onDidChange(() => {
if (dropdownIsVisible) {
const actions = getOutlineToolbarActions(menu, { notebookEditor: this._editor, outlineEntry: entry });
deferredUpdate = () => toolbar.setActions(actions.primary, actions.secondary);

return;
}

const actions = getOutlineToolbarActions(menu, { notebookEditor: this._editor, outlineEntry: entry });
toolbar.setActions(actions.primary, actions.secondary);
}));

templateData.container.classList.remove('notebook-outline-toolbar-dropdown-active');
templateData.elementDisposables.add(toolbar.onDidChangeDropdownVisibility(visible => {
dropdownIsVisible = visible;
if (visible) {
templateData.container.classList.add('notebook-outline-toolbar-dropdown-active');
} else {
templateData.container.classList.remove('notebook-outline-toolbar-dropdown-active');
}

if (deferredUpdate && !visible) {
disposableTimeout(() => {
deferredUpdate?.();
}, 0, templateData.elementDisposables);

deferredUpdate = undefined;
}
}));

}
}

function getOutlineToolbarActions(menu: IMenu, args?: NotebookSectionArgs): { primary: IAction[]; secondary: IAction[] } {
const primary: IAction[] = [];
const secondary: IAction[] = [];
const result = { primary, secondary };

// TODO: @Yoyokrazy bring the "inline" back when there's an appropriate run in section icon
createAndFillInActionBarActions(menu, { shouldForwardArgs: true, arg: args }, result); //, g => /^inline/.test(g));

return result;
}

class NotebookOutlineAccessibility implements IListAccessibilityProvider<OutlineEntry> {
getAriaLabel(element: OutlineEntry): string | null {
return element.label;
Expand Down Expand Up @@ -183,7 +308,7 @@ class NotebookQuickPickProvider implements IQuickPickDataSource<OutlineEntry> {

class NotebookComparator implements IOutlineComparator<OutlineEntry> {

private readonly _collator = new WindowIdleValue<Intl.Collator>(mainWindow, () => new Intl.Collator(undefined, { numeric: true }));
private readonly _collator = new DOM.WindowIdleValue<Intl.Collator>(mainWindow, () => new Intl.Collator(undefined, { numeric: true }));

compareByPosition(a: OutlineEntry, b: OutlineEntry): number {
return a.index - b.index;
Expand Down Expand Up @@ -251,7 +376,7 @@ export class NotebookCellOutline implements IOutline<OutlineEntry> {
installSelectionListener();
const treeDataSource: IDataSource<this, OutlineEntry> = { getChildren: parent => parent instanceof NotebookCellOutline ? (this._outlineProvider?.entries ?? []) : parent.children };
const delegate = new NotebookOutlineVirtualDelegate();
const renderers = [instantiationService.createInstance(NotebookOutlineRenderer)];
const renderers = [instantiationService.createInstance(NotebookOutlineRenderer, this._editor.getControl(), _target)];
const comparator = new NotebookComparator();

const options: IWorkbenchDataTreeOptions<OutlineEntry, FuzzyScore> = {
Expand Down Expand Up @@ -404,8 +529,15 @@ export class NotebookOutlineCreator implements IOutlineCreator<NotebookEditor, O
}
}

Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookOutlineCreator, LifecyclePhase.Eventually);
export const NotebookOutlineContext = {
CellKind: new RawContextKey<CellKind>('notebookCellKind', undefined),
CellHasChildren: new RawContextKey<boolean>('notebookCellHasChildren', false),
CellHasHeader: new RawContextKey<boolean>('notebookCellHasHeader', false),
CellFoldingState: new RawContextKey<CellFoldingState>('notebookCellFoldingState', CellFoldingState.None),
OutlineElementTarget: new RawContextKey<OutlineTarget>('notebookOutlineElementTarget', undefined),
};

Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookOutlineCreator, LifecyclePhase.Eventually);

Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
id: 'notebook',
Expand Down
Loading