Path: blob/main/src/vs/workbench/contrib/notebook/browser/controller/foldingController.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';6import { NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from '../../common/notebookContextKeys.js';7import { INotebookEditor, INotebookEditorMouseEvent, INotebookEditorContribution, getNotebookEditorFromEditorPane, CellFoldingState } from '../notebookBrowser.js';8import { FoldingModel } from '../viewModel/foldingModel.js'; import { CellKind } from '../../common/notebookCommon.js';9import { ICellRange } from '../../common/notebookRange.js';10import { registerNotebookContribution } from '../notebookEditorExtensions.js';11import { registerAction2, Action2 } from '../../../../../platform/actions/common/actions.js';12import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';13import { InputFocusedContextKey } from '../../../../../platform/contextkey/common/contextkeys.js';14import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';15import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';16import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';17import { IEditorService } from '../../../../services/editor/common/editorService.js';18import { NOTEBOOK_ACTIONS_CATEGORY } from './coreActions.js';19import { localize, localize2 } from '../../../../../nls.js';20import { FoldingRegion } from '../../../../../editor/contrib/folding/browser/foldingRanges.js';21import { ICommandMetadata } from '../../../../../platform/commands/common/commands.js';22import { NotebookViewModel } from '../viewModel/notebookViewModelImpl.js';2324export class FoldingController extends Disposable implements INotebookEditorContribution {25static id: string = 'workbench.notebook.foldingController';2627private _foldingModel: FoldingModel | null = null;28private readonly _localStore = this._register(new DisposableStore());2930constructor(private readonly _notebookEditor: INotebookEditor) {31super();3233this._register(this._notebookEditor.onMouseUp(e => { this.onMouseUp(e); }));3435this._register(this._notebookEditor.onDidChangeModel(() => {36this._localStore.clear();3738if (!this._notebookEditor.hasModel()) {39return;40}4142this._localStore.add(this._notebookEditor.onDidChangeCellState(e => {43if (e.source.editStateChanged && e.cell.cellKind === CellKind.Markup) {44this._foldingModel?.recompute();45}46}));4748this._foldingModel = new FoldingModel();49this._localStore.add(this._foldingModel);50this._foldingModel.attachViewModel(this._notebookEditor.getViewModel());5152this._localStore.add(this._foldingModel.onDidFoldingRegionChanged(() => {53this._updateEditorFoldingRanges();54}));55}));56}5758saveViewState(): ICellRange[] {59return this._foldingModel?.getMemento() || [];60}6162restoreViewState(state: ICellRange[] | undefined) {63this._foldingModel?.applyMemento(state || []);64this._updateEditorFoldingRanges();65}6667setFoldingStateDown(index: number, state: CellFoldingState, levels: number) {68const doCollapse = state === CellFoldingState.Collapsed;69const region = this._foldingModel!.getRegionAtLine(index + 1);70const regions: FoldingRegion[] = [];71if (region) {72if (region.isCollapsed !== doCollapse) {73regions.push(region);74}75if (levels > 1) {76const regionsInside = this._foldingModel!.getRegionsInside(region, (r, level: number) => r.isCollapsed !== doCollapse && level < levels);77regions.push(...regionsInside);78}79}8081regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed));82this._updateEditorFoldingRanges();83}8485setFoldingStateUp(index: number, state: CellFoldingState, levels: number) {86if (!this._foldingModel) {87return;88}8990const regions = this._foldingModel.getAllRegionsAtLine(index + 1, (region, level) => region.isCollapsed !== (state === CellFoldingState.Collapsed) && level <= levels);91regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed));92this._updateEditorFoldingRanges();93}9495private _updateEditorFoldingRanges() {96if (!this._foldingModel) {97return;98}99100if (!this._notebookEditor.hasModel()) {101return;102}103104const vm = this._notebookEditor.getViewModel() as NotebookViewModel;105106vm.updateFoldingRanges(this._foldingModel.regions);107const hiddenRanges = vm.getHiddenRanges();108this._notebookEditor.setHiddenAreas(hiddenRanges);109}110111onMouseUp(e: INotebookEditorMouseEvent) {112if (!e.event.target) {113return;114}115116if (!this._notebookEditor.hasModel()) {117return;118}119120const viewModel = this._notebookEditor.getViewModel() as NotebookViewModel;121const target = e.event.target as HTMLElement;122123if (target.classList.contains('codicon-notebook-collapsed') || target.classList.contains('codicon-notebook-expanded')) {124const parent = target.parentElement as HTMLElement;125126if (!parent.classList.contains('notebook-folding-indicator')) {127return;128}129130// folding icon131132const cellViewModel = e.target;133const modelIndex = viewModel.getCellIndex(cellViewModel);134const state = viewModel.getFoldingState(modelIndex);135136if (state === CellFoldingState.None) {137return;138}139140this.setFoldingStateUp(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed, 1);141this._notebookEditor.focusElement(cellViewModel);142}143144return;145}146147recompute() {148this._foldingModel?.recompute();149}150}151152registerNotebookContribution(FoldingController.id, FoldingController);153154155const NOTEBOOK_FOLD_COMMAND_LABEL = localize('fold.cell', "Fold Cell");156const NOTEBOOK_UNFOLD_COMMAND_LABEL = localize2('unfold.cell', "Unfold Cell");157158const FOLDING_COMMAND_ARGS: Pick<ICommandMetadata, 'args'> = {159args: [{160isOptional: true,161name: 'index',162description: 'The cell index',163schema: {164'type': 'object',165'required': ['index', 'direction'],166'properties': {167'index': {168'type': 'number'169},170'direction': {171'type': 'string',172'enum': ['up', 'down'],173'default': 'down'174},175'levels': {176'type': 'number',177'default': 1178},179}180}181}]182};183184registerAction2(class extends Action2 {185constructor() {186super({187id: 'notebook.fold',188title: localize2('fold.cell', "Fold Cell"),189category: NOTEBOOK_ACTIONS_CATEGORY,190keybinding: {191when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),192primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketLeft,193mac: {194primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.BracketLeft,195secondary: [KeyCode.LeftArrow],196},197secondary: [KeyCode.LeftArrow],198weight: KeybindingWeight.WorkbenchContrib199},200metadata: {201description: NOTEBOOK_FOLD_COMMAND_LABEL,202args: FOLDING_COMMAND_ARGS.args203},204precondition: NOTEBOOK_IS_ACTIVE_EDITOR,205f1: true206});207}208209async run(accessor: ServicesAccessor, args?: { index: number; levels: number; direction: 'up' | 'down' }): Promise<void> {210const editorService = accessor.get(IEditorService);211212const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane);213if (!editor) {214return;215}216217if (!editor.hasModel()) {218return;219}220221const levels = args && args.levels || 1;222const direction = args && args.direction === 'up' ? 'up' : 'down';223let index: number | undefined = undefined;224225if (args) {226index = args.index;227} else {228const activeCell = editor.getActiveCell();229if (!activeCell) {230return;231}232index = editor.getCellIndex(activeCell);233}234235const controller = editor.getContribution<FoldingController>(FoldingController.id);236if (index !== undefined) {237const targetCell = (index < 0 || index >= editor.getLength()) ? undefined : editor.cellAt(index);238if (targetCell?.cellKind === CellKind.Code && direction === 'down') {239return;240}241242if (direction === 'up') {243controller.setFoldingStateUp(index, CellFoldingState.Collapsed, levels);244} else {245controller.setFoldingStateDown(index, CellFoldingState.Collapsed, levels);246}247248const viewIndex = editor.getViewModel().getNearestVisibleCellIndexUpwards(index);249editor.focusElement(editor.cellAt(viewIndex));250}251}252});253254registerAction2(class extends Action2 {255constructor() {256super({257id: 'notebook.unfold',258title: NOTEBOOK_UNFOLD_COMMAND_LABEL,259category: NOTEBOOK_ACTIONS_CATEGORY,260keybinding: {261when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),262primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketRight,263mac: {264primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.BracketRight,265secondary: [KeyCode.RightArrow],266},267secondary: [KeyCode.RightArrow],268weight: KeybindingWeight.WorkbenchContrib269},270metadata: {271description: NOTEBOOK_UNFOLD_COMMAND_LABEL,272args: FOLDING_COMMAND_ARGS.args273},274precondition: NOTEBOOK_IS_ACTIVE_EDITOR,275f1: true276});277}278279async run(accessor: ServicesAccessor, args?: { index: number; levels: number; direction: 'up' | 'down' }): Promise<void> {280const editorService = accessor.get(IEditorService);281282const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane);283if (!editor) {284return;285}286287const levels = args && args.levels || 1;288const direction = args && args.direction === 'up' ? 'up' : 'down';289let index: number | undefined = undefined;290291if (args) {292index = args.index;293} else {294const activeCell = editor.getActiveCell();295if (!activeCell) {296return;297}298index = editor.getCellIndex(activeCell);299}300301const controller = editor.getContribution<FoldingController>(FoldingController.id);302if (index !== undefined) {303if (direction === 'up') {304controller.setFoldingStateUp(index, CellFoldingState.Expanded, levels);305} else {306controller.setFoldingStateDown(index, CellFoldingState.Expanded, levels);307}308}309}310});311312313