diff --git a/package.json b/package.json index a5981e1ce85..e470489427e 100644 --- a/package.json +++ b/package.json @@ -202,6 +202,11 @@ "key": "ctrl+; c", "when": "editorTextFocus && jupyter.hascodecells && !notebookEditorFocused" }, + { + "command": "jupyter.changeCellToRaw", + "key": "ctrl+; r", + "when": "editorTextFocus && jupyter.hascodecells && !notebookEditorFocused" + }, { "command": "jupyter.gotoNextCellInFile", "key": "ctrl+alt+]", diff --git a/src/commands.ts b/src/commands.ts index eaf0b659302..d8caf0f0e66 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -161,6 +161,7 @@ export interface ICommandNameArgumentTypeMapping { [DSCommands.MoveCellsDown]: []; [DSCommands.ChangeCellToMarkdown]: []; [DSCommands.ChangeCellToCode]: []; + [DSCommands.ChangeCellToRaw]: []; [DSCommands.GotoNextCellInFile]: []; [DSCommands.GotoPrevCellInFile]: []; [DSCommands.ScrollToCell]: [Uri, string]; diff --git a/src/interactive-window/commands/commandRegistry.ts b/src/interactive-window/commands/commandRegistry.ts index cd418ce1790..70283bba44a 100644 --- a/src/interactive-window/commands/commandRegistry.ts +++ b/src/interactive-window/commands/commandRegistry.ts @@ -85,6 +85,7 @@ export class CommandRegistry implements IDisposable, IExtensionSyncActivationSer this.registerCommand(Commands.MoveCellsDown, this.moveCellsDown); this.registerCommand(Commands.ChangeCellToMarkdown, this.changeCellToMarkdown); this.registerCommand(Commands.ChangeCellToCode, this.changeCellToCode); + this.registerCommand(Commands.ChangeCellToRaw, this.changeCellToRaw); this.registerCommand(Commands.GotoNextCellInFile, this.gotoNextCellInFile); this.registerCommand(Commands.GotoPrevCellInFile, this.gotoPrevCellInFile); this.registerCommand(Commands.AddCellBelow, this.addCellBelow); @@ -436,6 +437,10 @@ export class CommandRegistry implements IDisposable, IExtensionSyncActivationSer this.getCurrentCodeWatcher()?.changeCellToCode(); } + private async changeCellToRaw(): Promise { + this.getCurrentCodeWatcher()?.changeCellToRaw(); + } + private async gotoNextCellInFile(): Promise { this.getCurrentCodeWatcher()?.gotoNextCell(); } diff --git a/src/interactive-window/editor-integration/cellFactory.ts b/src/interactive-window/editor-integration/cellFactory.ts index 3a47f71dcd7..e2a5e884c59 100644 --- a/src/interactive-window/editor-integration/cellFactory.ts +++ b/src/interactive-window/editor-integration/cellFactory.ts @@ -37,12 +37,17 @@ function generateMarkdownCell(code: string[]) { return new NotebookCellData(NotebookCellKind.Markup, generateMarkdownFromCodeLines(code).join('\n'), 'markdown'); } +function generateRawCell(code: string[], matcher: CellMatcher) { + const lines = matcher.isCell(code[0]) && code.length > 1 ? code.slice(1) : code; + return new NotebookCellData(NotebookCellKind.Markup, lines.join('\n'), 'raw'); +} + export function generateCells( settings: IJupyterSettings | undefined, code: string, splitMarkdown: boolean ): NotebookCellData[] { - // Determine if we have a markdown cell/ markdown and code cell combined/ or just a code cell + // Determine if we have a markdown cell/ raw cell/ or code cell const split = splitLines(code, { trim: false }); const firstLine = split[0]; const matcher = new CellMatcher(settings); @@ -70,6 +75,9 @@ export function generateCells( // Just a single markdown cell return [generateMarkdownCell(split)]; } + } else if (matcher.isRaw(firstLine)) { + // Just a raw cell + return [generateRawCell(split, matcher)]; } else { // Just code return [generateCodeCell(split, matcher)]; diff --git a/src/interactive-window/editor-integration/cellMatcher.ts b/src/interactive-window/editor-integration/cellMatcher.ts index f1b745d0d34..b29cb282455 100644 --- a/src/interactive-window/editor-integration/cellMatcher.ts +++ b/src/interactive-window/editor-integration/cellMatcher.ts @@ -13,9 +13,11 @@ import { noop } from '../../platform/common/utils/misc'; export class CellMatcher { public codeExecRegEx: RegExp; public markdownExecRegEx: RegExp; + public rawExecRegEx: RegExp; private codeMatchRegEx: RegExp; private markdownMatchRegEx: RegExp; + private rawMatchRegEx: RegExp; private defaultCellMarker: string; constructor(settings?: IJupyterSettings) { @@ -27,25 +29,40 @@ export class CellMatcher { settings ? settings.markdownRegularExpression : undefined, RegExpValues.PythonMarkdownCellMarker ); + this.rawMatchRegEx = this.createRegExp( + undefined, // No setting for raw cells yet + RegExpValues.PythonRawCellMarker + ); this.codeExecRegEx = new RegExp(`${this.codeMatchRegEx.source}(.*)`); this.markdownExecRegEx = new RegExp(`${this.markdownMatchRegEx.source}(.*)`); + this.rawExecRegEx = new RegExp(`${this.rawMatchRegEx.source}(.*)`); this.defaultCellMarker = settings?.defaultCellMarker ? settings.defaultCellMarker : '# %%'; } public isCell(code: string): boolean { - return this.isCode(code) || this.isMarkdown(code); + return this.isCode(code) || this.isMarkdown(code) || this.isRaw(code); } public isMarkdown(code: string): boolean { return this.markdownMatchRegEx.test(code.trim()); } + public isRaw(code: string): boolean { + return this.rawMatchRegEx.test(code.trim()); + } + public isCode(code: string): boolean { return this.codeMatchRegEx.test(code.trim()) || code.trim() === this.defaultCellMarker; } public getCellType(code: string): string { - return this.isMarkdown(code) ? 'markdown' : 'code'; + if (this.isMarkdown(code)) { + return 'markdown'; + } else if (this.isRaw(code)) { + return 'raw'; + } else { + return 'code'; + } } public isEmptyCell(code: string): boolean { diff --git a/src/interactive-window/editor-integration/cellMatcher.unit.test.ts b/src/interactive-window/editor-integration/cellMatcher.unit.test.ts index 21e78569d23..06a53f863a6 100644 --- a/src/interactive-window/editor-integration/cellMatcher.unit.test.ts +++ b/src/interactive-window/editor-integration/cellMatcher.unit.test.ts @@ -23,6 +23,7 @@ suite('CellMatcher', () => { test('CellMatcher for valid code cell', () => { assert.ok(defaultMatcher.isCell(cellMarker), `"${cellMarker}" should match as a cell marker`); assert.ok(defaultMatcher.isCode(cellMarker), `"${cellMarker}" should match as a code cell marker`); + assert.equal(defaultMatcher.getCellType(cellMarker), 'code', `"${cellMarker}" should be detected as code cell type`); }); }); @@ -41,6 +42,22 @@ suite('CellMatcher', () => { test('CellMatcher for valid markdown cell', () => { assert.ok(defaultMatcher.isCell(cellMarker), `"${cellMarker}" should match as a cell marker`); assert.ok(defaultMatcher.isMarkdown(cellMarker), `"${cellMarker}" should match as a markdown cell marker`); + assert.equal(defaultMatcher.getCellType(cellMarker), 'markdown', `"${cellMarker}" should be detected as markdown cell type`); + }); + }); + + const rawCellMarkers = [ + '# %% [raw]', + '#%%[raw]', + '# %% [raw]', + '# %% [raw] extra stuff', + ' # %% [raw] ' + ]; + rawCellMarkers.forEach((cellMarker) => { + test('CellMatcher for valid raw cell', () => { + assert.ok(defaultMatcher.isCell(cellMarker), `"${cellMarker}" should match as a cell marker`); + assert.ok(defaultMatcher.isRaw(cellMarker), `"${cellMarker}" should match as a raw cell marker`); + assert.equal(defaultMatcher.getCellType(cellMarker), 'raw', `"${cellMarker}" should be detected as raw cell type`); }); }); diff --git a/src/interactive-window/editor-integration/codewatcher.ts b/src/interactive-window/editor-integration/codewatcher.ts index e1c2425ee2e..0c8bba3c034 100644 --- a/src/interactive-window/editor-integration/codewatcher.ts +++ b/src/interactive-window/editor-integration/codewatcher.ts @@ -694,6 +694,13 @@ export class CodeWatcher implements ICodeWatcher { }); } + @capturePerfTelemetry(Telemetry.ChangeCellToRaw) + public changeCellToRaw() { + this.applyToCells((editor, cell, _) => { + return this.changeCellTo(editor, cell, 'raw'); + }); + } + @capturePerfTelemetry(Telemetry.GotoNextCellInFile) public gotoNextCell() { const editor = window.activeTextEditor; @@ -743,11 +750,6 @@ export class CodeWatcher implements ICodeWatcher { } private changeCellTo(editor: TextEditor, cell: ICellRange, toCellType: nbformat.CellType) { - // change cell from code -> markdown or markdown -> code - if (toCellType === 'raw') { - throw Error('Cell Type raw not implemented'); - } - // don't change cell type if already that type if (cell.cell_type === toCellType) { return; @@ -758,18 +760,37 @@ export class CodeWatcher implements ICodeWatcher { // new definition text const cellMarker = this.getDefaultCellMarker(editor.document.uri); - const definitionMatch = - toCellType === 'markdown' - ? cellMatcher.codeExecRegEx.exec(definitionText) // code -> markdown - : cellMatcher.markdownExecRegEx.exec(definitionText); // markdown -> code + + // Determine which regex to use based on current cell type + let definitionMatch: RegExpExecArray | null = null; + if (cellMatcher.isMarkdown(definitionText)) { + definitionMatch = cellMatcher.markdownExecRegEx.exec(definitionText); + } else if (cellMatcher.isRaw(definitionText)) { + definitionMatch = cellMatcher.rawExecRegEx.exec(definitionText); + } else { + definitionMatch = cellMatcher.codeExecRegEx.exec(definitionText); + } + if (!definitionMatch) { return; } + const definitionExtra = definitionMatch[definitionMatch.length - 1]; - const newDefinitionText = - toCellType === 'markdown' - ? `${cellMarker} [markdown]${definitionExtra}` // code -> markdown - : `${cellMarker}${definitionExtra}`; // markdown -> code + + // Create the new definition text based on target cell type + let newDefinitionText: string; + switch (toCellType) { + case 'markdown': + newDefinitionText = `${cellMarker} [markdown]${definitionExtra}`; + break; + case 'raw': + newDefinitionText = `${cellMarker} [raw]${definitionExtra}`; + break; + case 'code': + default: + newDefinitionText = `${cellMarker}${definitionExtra}`; + break; + } editor .edit(async (editBuilder) => { diff --git a/src/interactive-window/editor-integration/types.ts b/src/interactive-window/editor-integration/types.ts index a7956bef48b..18ece46759a 100644 --- a/src/interactive-window/editor-integration/types.ts +++ b/src/interactive-window/editor-integration/types.ts @@ -50,6 +50,7 @@ export interface ICodeWatcher extends IDisposable { moveCellsDown(): Promise; changeCellToMarkdown(): void; changeCellToCode(): void; + changeCellToRaw(): void; debugCurrentCell(): Promise; gotoNextCell(): void; gotoPreviousCell(): void; diff --git a/src/platform/common/constants.ts b/src/platform/common/constants.ts index 0710e5e7c3d..ce44fdfcc35 100644 --- a/src/platform/common/constants.ts +++ b/src/platform/common/constants.ts @@ -213,6 +213,7 @@ export namespace Commands { export const MoveCellsDown = 'jupyter.moveCellsDown'; export const ChangeCellToMarkdown = 'jupyter.changeCellToMarkdown'; export const ChangeCellToCode = 'jupyter.changeCellToCode'; + export const ChangeCellToRaw = 'jupyter.changeCellToRaw'; export const GotoNextCellInFile = 'jupyter.gotoNextCellInFile'; export const GotoPrevCellInFile = 'jupyter.gotoPrevCellInFile'; export const ScrollToCell = 'jupyter.scrolltocell'; @@ -286,6 +287,7 @@ export namespace EditorContexts { export namespace RegExpValues { export const PythonCellMarker = /^(#\s*%%|#\s*\|#\s*In\[\d*?\]|#\s*In\[ \])/; export const PythonMarkdownCellMarker = /^(#\s*%%\s*\[markdown\]|#\s*\)/; + export const PythonRawCellMarker = /^(#\s*%%\s*\[raw\])/; export const UrlPatternRegEx = '(?https?:\\/\\/)((\\(.+\\s+or\\s+(?.+)\\))|(?[^\\s]+))(?:.+)'; export const HttpPattern = /https?:\/\//; @@ -315,6 +317,7 @@ export enum Telemetry { MoveCellsDown = 'DATASCIENCE.RUN_MOVE_CELLS_DOWN', ChangeCellToMarkdown = 'DATASCIENCE.RUN_CHANGE_CELL_TO_MARKDOWN', ChangeCellToCode = 'DATASCIENCE.RUN_CHANGE_CELL_TO_CODE', + ChangeCellToRaw = 'DATASCIENCE.RUN_CHANGE_CELL_TO_RAW', GotoNextCellInFile = 'DATASCIENCE.GOTO_NEXT_CELL_IN_FILE', GotoPrevCellInFile = 'DATASCIENCE.GOTO_PREV_CELL_IN_FILE', RunSelectionOrLine = 'DATASCIENCE.RUN_SELECTION_OR_LINE', diff --git a/src/telemetry.ts b/src/telemetry.ts index 14cb893d310..e1f72d7ee47 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -1498,6 +1498,15 @@ export class IEventNamePropertyMapping { source: 'N/A', measures: commonClassificationForDurationProperties() }; + /** + * Cell Edit Command in Interactive Window + */ + [Telemetry.ChangeCellToRaw]: TelemetryEventInfo = { + owner: 'amunger', + feature: ['InteractiveWindow'], + source: 'N/A', + measures: commonClassificationForDurationProperties() + }; /** * Cell Navigation Command in Interactive Window */