Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 { CellRevealType, ICellModelDecorations, ICellModelDeltaDecorations, INotebookEditor, INotebookEditorOptions, INotebookEditorPane } 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,12 @@ 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';

class NotebookOutlineTemplate {

Expand All @@ -47,7 +54,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 +65,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 +100,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 +140,63 @@ 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 ? 'code' : 'markdown');
NotebookOutlineContext.CellHasChildren.bindTo(scopedContextKeyService).set(length > 0);
NotebookOutlineContext.CellHasHeader.bindTo(scopedContextKeyService).set(node.element.level !== 7);
NotebookOutlineContext.OutlineElementTarget.bindTo(scopedContextKeyService).set('outline');

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 updateActions = () => {
const actions = getOutlineToolbarActions(menu, { notebookEditor: this._editor, outlineEntry: node.element });
outlineEntryToolbar.setActions(actions.primary, actions.secondary);
};
updateActions();
}
}

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);
}
}

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 +255,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 +323,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 +476,14 @@ export class NotebookOutlineCreator implements IOutlineCreator<NotebookEditor, O
}
}

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

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

Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
id: 'notebook',
Expand Down
147 changes: 147 additions & 0 deletions src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { localize, localize2 } from 'vs/nls';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { NotebookOutlineContext } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline';
import { FoldingController } from 'vs/workbench/contrib/notebook/browser/controller/foldingController';
import { CellFoldingState, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons';
import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry';
import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';

export type NotebookSectionArgs = {
notebookEditor: INotebookEditor | undefined;
outlineEntry: OutlineEntry;
};

export class NotebookRunSingleCellInSection extends Action2 {
constructor() {
super({
id: 'notebook.section.runSingleCell',
title: {
...localize2('runCell', "Run Cell"),
mnemonicTitle: localize({ key: 'mirunCell', comment: ['&& denotes a mnemonic'] }, "&&Run Cell"),
},
shortTitle: localize('runCell', "Run Cell"),
icon: icons.executeIcon,
menu: [
{
id: MenuId.NotebookOutlineActionMenu,
group: 'inline',
order: 1,
when: ContextKeyExpr.and(
NotebookOutlineContext.CellKind.isEqualTo('code'),
NotebookOutlineContext.OutlineElementTarget.isEqualTo('outline'),
NotebookOutlineContext.CellHasChildren.toNegated(),
NotebookOutlineContext.CellHasHeader.toNegated(),
)
}
]
});
}

override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise<void> {
context.notebookEditor?.executeNotebookCells([context.outlineEntry.cell]);
}
}

export class NotebookRunCellsInSection extends Action2 {
constructor() {
super({
id: 'notebook.section.runCells',
title: {
...localize2('runCellsInSection', "Run Cells In Section"),
mnemonicTitle: localize({ key: 'mirunCellsInSection', comment: ['&& denotes a mnemonic'] }, "&&Run Cells In Section"),
},
shortTitle: localize('runCellsInSection', "Run Cells In Section"),
// icon: icons.executeBelowIcon, // TODO @Yoyokrazy replace this with new icon later
menu: [
{
id: MenuId.NotebookStickyScrollContext,
group: 'notebookExecution',
order: 1
},
{
id: MenuId.NotebookOutlineActionMenu,
group: 'inline',
order: 1,
when: ContextKeyExpr.and(
NotebookOutlineContext.CellKind.isEqualTo('markdown'),
NotebookOutlineContext.OutlineElementTarget.isEqualTo('outline'),
NotebookOutlineContext.CellHasChildren,
NotebookOutlineContext.CellHasHeader,
)
}
]
});
}

override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise<void> {
const cell = context.outlineEntry.cell;

const idx = context.notebookEditor?.getViewModel()?.getCellIndex(cell);
if (idx === undefined) {
return;
}
const length = context.notebookEditor?.getViewModel()?.getFoldedLength(idx);
if (length === undefined) {
return;
}

const cells = context.notebookEditor?.getCellsInRange({ start: idx, end: idx + length + 1 });
context.notebookEditor?.executeNotebookCells(cells);
}
}

export class NotebookToggleFoldingSection extends Action2 {
constructor() {
super({
id: 'notebook.section.foldSection',
title: {
...localize2('foldSection', "Fold Section"),
mnemonicTitle: localize({ key: 'mifoldSection', comment: ['&& denotes a mnemonic'] }, "&&Fold Section"),
},
shortTitle: localize('foldSection', "Fold Section"),
menu: [
{
id: MenuId.NotebookOutlineActionMenu,
group: 'notebookFolding',
order: 2,
when: ContextKeyExpr.and(
NotebookOutlineContext.CellKind.isEqualTo('markdown'),
NotebookOutlineContext.OutlineElementTarget.isEqualTo('outline'),
NotebookOutlineContext.CellHasChildren,
NotebookOutlineContext.CellHasHeader,
)
}
]
});
}

override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise<void> {
if (context.notebookEditor) {
this.toggleFoldRange(context.outlineEntry, context.notebookEditor);
}
}

private toggleFoldRange(entry: OutlineEntry, notebookEditor: INotebookEditor) {
const foldingController = notebookEditor.getContribution<FoldingController>(FoldingController.id);
const currentState = (entry.cell as MarkupCellViewModel).foldingState;

const index = entry.index;
const headerLevel = entry.level;
const newFoldingState = (currentState === CellFoldingState.Collapsed) ? CellFoldingState.Expanded : CellFoldingState.Collapsed;

foldingController.setFoldingStateUp(index, newFoldingState, headerLevel);
}

}

registerAction2(NotebookRunSingleCellInSection);
registerAction2(NotebookRunCellsInSection);
registerAction2(NotebookToggleFoldingSection);
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,15 @@
/* Don't show markers inline with breadcrumbs */
display: none;
}

.monaco-list-row .notebook-outline-element .action-label {
display: none;
}

.monaco-list-row.focused.selected .notebook-outline-element .action-label {
display: flex;
}

.monaco-list-row:hover .notebook-outline-element .action-label {
display: flex;
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook
import 'vs/workbench/contrib/notebook/browser/controller/coreActions';
import 'vs/workbench/contrib/notebook/browser/controller/insertCellActions';
import 'vs/workbench/contrib/notebook/browser/controller/executeActions';
import 'vs/workbench/contrib/notebook/browser/controller/sectionActions';
import 'vs/workbench/contrib/notebook/browser/controller/layoutActions';
import 'vs/workbench/contrib/notebook/browser/controller/editActions';
import 'vs/workbench/contrib/notebook/browser/controller/cellOutputActions';
Expand Down
Loading