Skip to content

Commit

Permalink
re #102503. allow fold/unfold with levels and direction in notebook.
Browse files Browse the repository at this point in the history
  • Loading branch information
rebornix committed Aug 27, 2020
1 parent 4d7ad58 commit b4b67b8
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 71 deletions.
224 changes: 155 additions & 69 deletions src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import * as DOM from 'vs/base/browser/dom';
import { CellFoldingState, FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { MenuRegistry, MenuId, ICommandAction } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { getActiveNotebookEditor, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions';
import { localize } from 'vs/nls';
import { FoldingRegion } from 'vs/editor/contrib/folding/foldingRanges';

export class FoldingController extends Disposable implements INotebookEditorContribution {
static id: string = 'workbench.notebook.findController';
Expand Down Expand Up @@ -65,19 +65,31 @@ export class FoldingController extends Disposable implements INotebookEditorCont
this._updateEditorFoldingRanges();
}

setFoldingState(index: number, state: CellFoldingState) {
if (!this._foldingModel) {
return;
setFoldingStateDown(index: number, state: CellFoldingState, levels: number) {
const doCollapse = state === CellFoldingState.Collapsed;
let region = this._foldingModel!.getRegionAtLine(index + 1);
let regions: FoldingRegion[] = [];
if (region) {
if (region.isCollapsed !== doCollapse) {
regions.push(region);
}
if (levels > 1) {
let regionsInside = this._foldingModel!.getRegionsInside(region, (r, level: number) => r.isCollapsed !== doCollapse && level < levels);
regions.push(...regionsInside);
}
}

const range = this._foldingModel.regions.findRange(index + 1);
const startIndex = this._foldingModel.regions.getStartLineNumber(range) - 1;
regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed));
this._updateEditorFoldingRanges();
}

if (startIndex !== index) {
setFoldingStateUp(index: number, state: CellFoldingState, levels: number) {
if (!this._foldingModel) {
return;
}

this._foldingModel.setCollapsed(range, state === CellFoldingState.Collapsed);
let regions = this._foldingModel.getAllRegionsAtLine(index + 1, (region, level) => region.isCollapsed !== (state === CellFoldingState.Collapsed) && level <= levels);
regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed));
this._updateEditorFoldingRanges();
}

Expand Down Expand Up @@ -121,7 +133,7 @@ export class FoldingController extends Disposable implements INotebookEditorCont
return;
}

this.setFoldingState(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed);
this.setFoldingStateUp(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed, 1);
}

return;
Expand All @@ -130,90 +142,164 @@ export class FoldingController extends Disposable implements INotebookEditorCont

registerNotebookContribution(FoldingController.id, FoldingController);

registerAction2(class extends Action2 {
constructor() {
super({
id: 'notebook.fold',
title: { value: localize('fold.cell', "Fold Cell"), original: 'Fold Cell' },
category: NOTEBOOK_ACTIONS_CATEGORY,
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET,
mac: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET,
secondary: [KeyCode.LeftArrow],
},
secondary: [KeyCode.LeftArrow],
weight: KeybindingWeight.WorkbenchContrib
},
precondition: NOTEBOOK_IS_ACTIVE_EDITOR,
f1: true
});
}

async run(accessor: ServicesAccessor): Promise<void> {
const NOTEBOOK_UNFOLD_COMMAND_ID = 'notebook.unfold';
const NOTEBOOK_UNFOLD_COMMAND_LABEL = localize('unfold.cell', "Unfold Cell");
const NOTEBOOK_FOLD_COMMAND_ID = 'notebook.fold';
const NOTEBOOK_FOLD_COMMAND_LABEL = localize('fold.cell', "Fold Cell");

KeybindingsRegistry.registerCommandAndKeybindingRule({
weight: KeybindingWeight.WorkbenchContrib,
when: null,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET,
mac: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET,
secondary: [KeyCode.RightArrow],
},
secondary: [KeyCode.RightArrow],
id: NOTEBOOK_UNFOLD_COMMAND_ID,
description: {
description: NOTEBOOK_UNFOLD_COMMAND_LABEL,
args: [
{
name: 'index', description: 'The cell index', schema: {
'type': 'object',
'required': ['index', 'direction'],
'properties': {
'index': {
'type': 'number'
},
'direction': {
'type': 'string',
'enum': ['up', 'down'],
'default': 'down'
},
'levels': {
'type': 'number',
'default': 1
},
}
}
}
]
},
handler: async (accessor, args?: { index: number, levels: number, direction: 'up' | 'down' }) => {
const editorService = accessor.get(IEditorService);

const editor = getActiveNotebookEditor(editorService);
if (!editor) {
return;
}

const activeCell = editor.getActiveCell();
if (!activeCell) {
return;
const levels = args && args.levels || 1;
const direction = args && args.direction === 'up' ? 'up' : 'down';
let index: number | undefined = undefined;

if (args) {
index = args.index;
} else {
const activeCell = editor.getActiveCell();
if (!activeCell) {
return;
}
index = editor.viewModel?.viewCells.indexOf(activeCell);
}

const controller = editor.getContribution<FoldingController>(FoldingController.id);

const index = editor.viewModel?.viewCells.indexOf(activeCell);

if (index !== undefined) {
controller.setFoldingState(index, CellFoldingState.Collapsed);
if (direction === 'up') {
controller.setFoldingStateUp(index, CellFoldingState.Expanded, levels);
} else {
controller.setFoldingStateDown(index, CellFoldingState.Expanded, levels);
}
}
}
});

registerAction2(class extends Action2 {
constructor() {
super({
id: 'notebook.unfold',
title: { value: localize('unfold.cell', "Unfold Cell"), original: 'Unfold Cell' },
category: NOTEBOOK_ACTIONS_CATEGORY,
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET,
mac: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET,
secondary: [KeyCode.RightArrow],
},
secondary: [KeyCode.RightArrow],
weight: KeybindingWeight.WorkbenchContrib
},
precondition: NOTEBOOK_IS_ACTIVE_EDITOR,
f1: true
});
}

async run(accessor: ServicesAccessor): Promise<void> {
const unfoldCommand: ICommandAction = {
id: NOTEBOOK_UNFOLD_COMMAND_ID,
title: { value: NOTEBOOK_UNFOLD_COMMAND_LABEL, original: 'Unfold Cell' },
category: NOTEBOOK_ACTIONS_CATEGORY,
precondition: NOTEBOOK_IS_ACTIVE_EDITOR
};

MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: unfoldCommand, when: unfoldCommand.precondition });
MenuRegistry.addCommand(unfoldCommand);


KeybindingsRegistry.registerCommandAndKeybindingRule({
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET,
mac: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET,
secondary: [KeyCode.LeftArrow],
},
secondary: [KeyCode.LeftArrow],
weight: KeybindingWeight.WorkbenchContrib,
id: NOTEBOOK_FOLD_COMMAND_ID,
description: {
description: NOTEBOOK_FOLD_COMMAND_LABEL,
args: [
{
name: 'index', description: 'The cell index', schema: {
'type': 'object',
'required': ['index', 'direction'],
'properties': {
'index': {
'type': 'number'
},
'direction': {
'type': 'string',
'enum': ['up', 'down'],
'default': 'down'
},
'levels': {
'type': 'number',
'default': 1
},
}
}
}
]
},
handler: async (accessor, args?: { index: number, levels: number, direction: 'up' | 'down' }) => {
const editorService = accessor.get(IEditorService);

const editor = getActiveNotebookEditor(editorService);
if (!editor) {
return;
}

const activeCell = editor.getActiveCell();
if (!activeCell) {
return;
const levels = args && args.levels || 1;
const direction = args && args.direction === 'up' ? 'up' : 'down';
let index: number | undefined = undefined;

if (args) {
index = args.index;
} else {
const activeCell = editor.getActiveCell();
if (!activeCell) {
return;
}
index = editor.viewModel?.viewCells.indexOf(activeCell);
}

const controller = editor.getContribution<FoldingController>(FoldingController.id);

const index = editor.viewModel?.viewCells.indexOf(activeCell);

if (index !== undefined) {
controller.setFoldingState(index, CellFoldingState.Expanded);
if (direction === 'up') {
controller.setFoldingStateUp(index, CellFoldingState.Collapsed, levels);
} else {
controller.setFoldingStateDown(index, CellFoldingState.Collapsed, levels);
}
}
}
});

const foldCommand: ICommandAction = {
id: NOTEBOOK_FOLD_COMMAND_ID,
title: { value: NOTEBOOK_FOLD_COMMAND_LABEL, original: 'Fold Cell' },
category: NOTEBOOK_ACTIONS_CATEGORY,
precondition: NOTEBOOK_IS_ACTIVE_EDITOR
};

MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: foldCommand, when: foldCommand.precondition });
MenuRegistry.addCommand(foldCommand);
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { TrackedRangeStickiness } from 'vs/editor/common/model';
import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges';
import { FoldingRegion, FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges';
import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider';
import { ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';

type RegionFilter = (r: FoldingRegion) => boolean;
type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean;


export class FoldingModel extends Disposable {
private _viewModel: NotebookViewModel | null = null;
private _viewModelStore = new DisposableStore();
Expand Down Expand Up @@ -73,7 +77,70 @@ export class FoldingModel extends Disposable {
this.recompute();
}

public setCollapsed(index: number, newState: boolean) {
getRegionAtLine(lineNumber: number): FoldingRegion | null {
if (this._regions) {
let index = this._regions.findRange(lineNumber);
if (index >= 0) {
return this._regions.toRegion(index);
}
}
return null;
}

getRegionsInside(region: FoldingRegion | null, filter?: RegionFilter | RegionFilterWithLevel): FoldingRegion[] {
let result: FoldingRegion[] = [];
let index = region ? region.regionIndex + 1 : 0;
let endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;

if (filter && filter.length === 2) {
const levelStack: FoldingRegion[] = [];
for (let i = index, len = this._regions.length; i < len; i++) {
let current = this._regions.toRegion(i);
if (this._regions.getStartLineNumber(i) < endLineNumber) {
while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {
levelStack.pop();
}
levelStack.push(current);
if (filter(current, levelStack.length)) {
result.push(current);
}
} else {
break;
}
}
} else {
for (let i = index, len = this._regions.length; i < len; i++) {
let current = this._regions.toRegion(i);
if (this._regions.getStartLineNumber(i) < endLineNumber) {
if (!filter || (filter as RegionFilter)(current)) {
result.push(current);
}
} else {
break;
}
}
}
return result;
}

getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] {
let result: FoldingRegion[] = [];
if (this._regions) {
let index = this._regions.findRange(lineNumber);
let level = 1;
while (index >= 0) {
let current = this._regions.toRegion(index);
if (!filter || filter(current, level)) {
result.push(current);
}
level++;
index = current.parentIndex;
}
}
return result;
}

setCollapsed(index: number, newState: boolean) {
this._regions.setCollapsed(index, newState);
}

Expand Down

0 comments on commit b4b67b8

Please sign in to comment.