Path: blob/main/src/vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions.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 * as nls from '../../../../../nls.js';6import { DisposableStore } from '../../../../../base/common/lifecycle.js';7import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';8import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js';9import { Range } from '../../../../../editor/common/core/range.js';10import { ITextModel } from '../../../../../editor/common/model.js';11import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';12import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';13import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';14import { ILogService } from '../../../../../platform/log/common/log.js';15import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';16import { INotebookEditorService } from '../services/notebookEditorService.js';17import { NotebookSetting } from '../../common/notebookCommon.js';18import { isNotebookEditorInput } from '../../common/notebookEditorInput.js';19import { IEditorService } from '../../../../services/editor/common/editorService.js';2021export class NotebookIndentUsingTabs extends Action2 {22public static readonly ID = 'notebook.action.indentUsingTabs';2324constructor() {25super({26id: NotebookIndentUsingTabs.ID,27title: nls.localize('indentUsingTabs', "Indent Using Tabs"),28precondition: undefined,29});30}3132override run(accessor: ServicesAccessor, ...args: any[]): void {33changeNotebookIndentation(accessor, false, false);34}35}3637export class NotebookIndentUsingSpaces extends Action2 {38public static readonly ID = 'notebook.action.indentUsingSpaces';3940constructor() {41super({42id: NotebookIndentUsingSpaces.ID,43title: nls.localize('indentUsingSpaces', "Indent Using Spaces"),44precondition: undefined,45});46}4748override run(accessor: ServicesAccessor, ...args: any[]): void {49changeNotebookIndentation(accessor, true, false);50}51}5253export class NotebookChangeTabDisplaySize extends Action2 {54public static readonly ID = 'notebook.action.changeTabDisplaySize';5556constructor() {57super({58id: NotebookChangeTabDisplaySize.ID,59title: nls.localize('changeTabDisplaySize', "Change Tab Display Size"),60precondition: undefined,61});62}6364override run(accessor: ServicesAccessor, ...args: any[]): void {65changeNotebookIndentation(accessor, true, true);66}67}6869export class NotebookIndentationToSpacesAction extends Action2 {70public static readonly ID = 'notebook.action.convertIndentationToSpaces';7172constructor() {73super({74id: NotebookIndentationToSpacesAction.ID,75title: nls.localize('convertIndentationToSpaces', "Convert Indentation to Spaces"),76precondition: undefined,77});78}7980override run(accessor: ServicesAccessor, ...args: any[]): void {81convertNotebookIndentation(accessor, true);82}83}8485export class NotebookIndentationToTabsAction extends Action2 {86public static readonly ID = 'notebook.action.convertIndentationToTabs';8788constructor() {89super({90id: NotebookIndentationToTabsAction.ID,91title: nls.localize('convertIndentationToTabs', "Convert Indentation to Tabs"),92precondition: undefined,93});94}9596override run(accessor: ServicesAccessor, ...args: any[]): void {97convertNotebookIndentation(accessor, false);98}99}100101function changeNotebookIndentation(accessor: ServicesAccessor, insertSpaces: boolean, displaySizeOnly: boolean) {102const editorService = accessor.get(IEditorService);103const configurationService = accessor.get(IConfigurationService);104const notebookEditorService = accessor.get(INotebookEditorService);105const quickInputService = accessor.get(IQuickInputService);106107// keep this check here to pop on non-notebook actions108const activeInput = editorService.activeEditorPane?.input;109const isNotebook = isNotebookEditorInput(activeInput);110if (!isNotebook) {111return;112}113114// get notebook editor to access all codeEditors115const notebookEditor = notebookEditorService.retrieveExistingWidgetFromURI(activeInput.resource)?.value;116if (!notebookEditor) {117return;118}119120const picks = [1, 2, 3, 4, 5, 6, 7, 8].map(n => ({121id: n.toString(),122label: n.toString(),123}));124125// store the initial values of the configuration126const initialConfig = configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations) as any;127const initialInsertSpaces = initialConfig['editor.insertSpaces'];128// remove the initial values from the configuration129delete initialConfig['editor.indentSize'];130delete initialConfig['editor.tabSize'];131delete initialConfig['editor.insertSpaces'];132133setTimeout(() => {134quickInputService.pick(picks, { placeHolder: nls.localize({ key: 'selectTabWidth', comment: ['Tab corresponds to the tab key'] }, "Select Tab Size for Current File") }).then(pick => {135if (pick) {136const pickedVal = parseInt(pick.label, 10);137if (displaySizeOnly) {138configurationService.updateValue(NotebookSetting.cellEditorOptionsCustomizations, {139...initialConfig,140'editor.tabSize': pickedVal,141'editor.indentSize': pickedVal,142'editor.insertSpaces': initialInsertSpaces143});144} else {145configurationService.updateValue(NotebookSetting.cellEditorOptionsCustomizations, {146...initialConfig,147'editor.tabSize': pickedVal,148'editor.indentSize': pickedVal,149'editor.insertSpaces': insertSpaces150});151}152153}154});155}, 50/* quick input is sensitive to being opened so soon after another */);156}157158function convertNotebookIndentation(accessor: ServicesAccessor, tabsToSpaces: boolean): void {159const editorService = accessor.get(IEditorService);160const configurationService = accessor.get(IConfigurationService);161const logService = accessor.get(ILogService);162const textModelService = accessor.get(ITextModelService);163const notebookEditorService = accessor.get(INotebookEditorService);164const bulkEditService = accessor.get(IBulkEditService);165166// keep this check here to pop on non-notebook167const activeInput = editorService.activeEditorPane?.input;168const isNotebook = isNotebookEditorInput(activeInput);169if (!isNotebook) {170return;171}172173// get notebook editor to access all codeEditors174const notebookTextModel = notebookEditorService.retrieveExistingWidgetFromURI(activeInput.resource)?.value?.textModel;175if (!notebookTextModel) {176return;177}178179const disposable = new DisposableStore();180try {181Promise.all(notebookTextModel.cells.map(async cell => {182const ref = await textModelService.createModelReference(cell.uri);183disposable.add(ref);184const textEditorModel = ref.object.textEditorModel;185186const modelOpts = cell.textModel?.getOptions();187if (!modelOpts) {188return;189}190191const edits = getIndentationEditOperations(textEditorModel, modelOpts.tabSize, tabsToSpaces);192193bulkEditService.apply(edits, { label: nls.localize('convertIndentation', "Convert Indentation"), code: 'undoredo.convertIndentation', });194195})).then(() => {196// store the initial values of the configuration197const initialConfig = configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations) as any;198const initialIndentSize = initialConfig['editor.indentSize'];199const initialTabSize = initialConfig['editor.tabSize'];200// remove the initial values from the configuration201delete initialConfig['editor.indentSize'];202delete initialConfig['editor.tabSize'];203delete initialConfig['editor.insertSpaces'];204205configurationService.updateValue(NotebookSetting.cellEditorOptionsCustomizations, {206...initialConfig,207'editor.tabSize': initialTabSize,208'editor.indentSize': initialIndentSize,209'editor.insertSpaces': tabsToSpaces210});211disposable.dispose();212});213} catch {214logService.error('Failed to convert indentation to spaces for notebook cells.');215}216}217218function getIndentationEditOperations(model: ITextModel, tabSize: number, tabsToSpaces: boolean): ResourceTextEdit[] {219if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {220// Model is empty221return [];222}223224let spaces = '';225for (let i = 0; i < tabSize; i++) {226spaces += ' ';227}228229const spacesRegExp = new RegExp(spaces, 'gi');230231const edits: ResourceTextEdit[] = [];232for (let lineNumber = 1, lineCount = model.getLineCount(); lineNumber <= lineCount; lineNumber++) {233let lastIndentationColumn = model.getLineFirstNonWhitespaceColumn(lineNumber);234if (lastIndentationColumn === 0) {235lastIndentationColumn = model.getLineMaxColumn(lineNumber);236}237238if (lastIndentationColumn === 1) {239continue;240}241242const originalIndentationRange = new Range(lineNumber, 1, lineNumber, lastIndentationColumn);243const originalIndentation = model.getValueInRange(originalIndentationRange);244const newIndentation = (245tabsToSpaces246? originalIndentation.replace(/\t/ig, spaces)247: originalIndentation.replace(spacesRegExp, '\t')248);249edits.push(new ResourceTextEdit(model.uri, { range: originalIndentationRange, text: newIndentation }));250}251return edits;252}253254registerAction2(NotebookIndentUsingSpaces);255registerAction2(NotebookIndentUsingTabs);256registerAction2(NotebookChangeTabDisplaySize);257registerAction2(NotebookIndentationToSpacesAction);258registerAction2(NotebookIndentationToTabsAction);259260261