From 42238bdaabb321f066f05feffa29ab148995b0dc Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Fri, 9 Sep 2022 11:32:10 -0700 Subject: [PATCH] notebook image cleaning automation (#159212) * cache and cleaner complete, needs debounce * minor renaming and reformatting * bugfix for paste into new cell * cleaning functionality complete * refer to metadata as copy of current cell's * check undef before reading from cache * working state, pending cache restructure * dots -> brackets * pre-class refactor * massive cleaner refactor * cache typing, closed nb check, workspaceEdit only if metadata is changed * undefined access fix * proper debouncer * get it up to work again * no need to loop * cell metadata uri parsing regression * diagnostic * Show diagnostics on document open * transfer cache before file renames * disable word wrap in notebook diff editor * Avoid early notebook cell metadata deep clone * No special case empty cell * rename * better naming * Quick fix for invalid image attachment * cleanup * Add code action metadata Co-authored-by: rebornix --- extensions/ipynb/.vscode/launch.json | 19 + extensions/ipynb/package.json | 60 +-- extensions/ipynb/src/constants.ts | 5 + extensions/ipynb/src/helper.ts | 137 +++++++ extensions/ipynb/src/ipynbMain.ts | 13 +- .../ipynb/src/notebookAttachmentCleaner.ts | 348 ++++++++++++++++++ extensions/ipynb/src/notebookImagePaste.ts | 6 +- .../notebook/browser/diff/diffComponents.ts | 8 +- .../notebook/browser/notebook.contribution.ts | 2 +- .../contrib/notebook/common/notebookCommon.ts | 28 +- src/vscode-dts/vscode.d.ts | 2 +- 11 files changed, 569 insertions(+), 59 deletions(-) create mode 100644 extensions/ipynb/.vscode/launch.json create mode 100644 extensions/ipynb/src/helper.ts create mode 100644 extensions/ipynb/src/notebookAttachmentCleaner.ts diff --git a/extensions/ipynb/.vscode/launch.json b/extensions/ipynb/.vscode/launch.json new file mode 100644 index 00000000000..30130a429d5 --- /dev/null +++ b/extensions/ipynb/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "name": "Launch Extension", + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "request": "launch", + "type": "extensionHost" + } + ] +} \ No newline at end of file diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index 7814fb7241f..cf58f3c7556 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -9,7 +9,7 @@ "vscode": "^1.57.0" }, "enabledApiProposals": [ - "documentPaste" + "documentPaste" ], "activationEvents": [ "*" @@ -27,21 +27,21 @@ } }, "contributes": { - "configuration":[ - { - "properties": { - "ipynb.experimental.pasteImages.enabled":{ - "type": "boolean", - "scope": "resource", - "markdownDescription": "%ipynb.experimental.pasteImages.enabled%", - "default": false, - "tags": [ - "experimental" - ] - } - } - } - ], + "configuration": [ + { + "properties": { + "ipynb.experimental.pasteImages.enabled": { + "type": "boolean", + "scope": "resource", + "markdownDescription": "%ipynb.experimental.pasteImages.enabled%", + "default": false, + "tags": [ + "experimental" + ] + } + } + } + ], "commands": [ { "command": "ipynb.newUntitledIpynb", @@ -52,6 +52,10 @@ { "command": "ipynb.openIpynbInNotebookEditor", "title": "Open ipynb file in notebook editor" + }, + { + "command": "ipynb.cleanInvalidImageAttachment", + "title": "Clean invalid image attachment reference" } ], "notebooks": [ @@ -66,16 +70,16 @@ "priority": "default" } ], - "notebookRenderer": [ - { - "id": "vscode.markdown-it-cell-attachment-renderer", - "displayName": "Markdown it ipynb Cell Attachment renderer", - "entrypoint": { - "extends": "vscode.markdown-it-renderer", - "path": "./notebook-out/cellAttachmentRenderer.js" - } - } - ], + "notebookRenderer": [ + { + "id": "vscode.markdown-it-cell-attachment-renderer", + "displayName": "Markdown it ipynb Cell Attachment renderer", + "entrypoint": { + "extends": "vscode.markdown-it-renderer", + "path": "./notebook-out/cellAttachmentRenderer.js" + } + } + ], "menus": { "file/newFile": [ { @@ -90,6 +94,10 @@ { "command": "ipynb.openIpynbInNotebookEditor", "when": "false" + }, + { + "command": "ipynb.cleanInvalidImageAttachment", + "when": "false" } ] } diff --git a/extensions/ipynb/src/constants.ts b/extensions/ipynb/src/constants.ts index 17419359414..43e13b3b510 100644 --- a/extensions/ipynb/src/constants.ts +++ b/extensions/ipynb/src/constants.ts @@ -3,4 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; + export const defaultNotebookFormat = { major: 4, minor: 2 }; +export const ATTACHMENT_CLEANUP_COMMANDID = 'ipynb.cleanInvalidImageAttachment'; + +export const JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR: vscode.DocumentSelector = { notebookType: 'jupyter-notebook', language: 'markdown' }; diff --git a/extensions/ipynb/src/helper.ts b/extensions/ipynb/src/helper.ts new file mode 100644 index 00000000000..5fa03514640 --- /dev/null +++ b/extensions/ipynb/src/helper.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function deepClone(obj: T): T { + if (!obj || typeof obj !== 'object') { + return obj; + } + if (obj instanceof RegExp) { + // See https://github.com/microsoft/TypeScript/issues/10990 + return obj as any; + } + const result: any = Array.isArray(obj) ? [] : {}; + Object.keys(obj).forEach((key: string) => { + if ((obj)[key] && typeof (obj)[key] === 'object') { + result[key] = deepClone((obj)[key]); + } else { + result[key] = (obj)[key]; + } + }); + return result; +} + +// from https://github.com/microsoft/vscode/blob/43ae27a30e7b5e8711bf6b218ee39872ed2b8ef6/src/vs/base/common/objects.ts#L117 +export function objectEquals(one: any, other: any) { + if (one === other) { + return true; + } + if (one === null || one === undefined || other === null || other === undefined) { + return false; + } + if (typeof one !== typeof other) { + return false; + } + if (typeof one !== 'object') { + return false; + } + if ((Array.isArray(one)) !== (Array.isArray(other))) { + return false; + } + + let i: number; + let key: string; + + if (Array.isArray(one)) { + if (one.length !== other.length) { + return false; + } + for (i = 0; i < one.length; i++) { + if (!objectEquals(one[i], other[i])) { + return false; + } + } + } else { + const oneKeys: string[] = []; + + for (key in one) { + oneKeys.push(key); + } + oneKeys.sort(); + const otherKeys: string[] = []; + for (key in other) { + otherKeys.push(key); + } + otherKeys.sort(); + if (!objectEquals(oneKeys, otherKeys)) { + return false; + } + for (i = 0; i < oneKeys.length; i++) { + if (!objectEquals(one[oneKeys[i]], other[oneKeys[i]])) { + return false; + } + } + } + + return true; +} + +interface Options { + callback: (value: T) => void; + + merge?: (input: T[]) => T; + delay?: number; +} + + +export class DebounceTrigger { + + private _isPaused = 0; + protected _queue: T[] = []; + private _callbackFn: (value: T) => void; + private _mergeFn?: (input: T[]) => T; + private readonly _delay: number; + private _handle: any | undefined; + + constructor(options: Options) { + this._callbackFn = options.callback; + this._mergeFn = options.merge; + this._delay = options.delay ?? 100; + } + + private pause(): void { + this._isPaused++; + } + + private resume(): void { + if (this._isPaused !== 0 && --this._isPaused === 0) { + if (this._mergeFn) { + const items = Array.from(this._queue); + this._queue = []; + this._callbackFn(this._mergeFn(items)); + + } else { + while (!this._isPaused && this._queue.length !== 0) { + this._callbackFn(this._queue.shift()!); + } + } + } + } + + trigger(item: T): void { + if (!this._handle) { + this.pause(); + this._handle = setTimeout(() => { + this._handle = undefined; + this.resume(); + }, this._delay); + } + + if (this._isPaused !== 0) { + this._queue.push(item); + } else { + this._callbackFn(item); + } + } +} diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index 003c4c8f266..a1fd97f84b9 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ensureAllNewCellsHaveCellIds } from './cellIdService'; import { NotebookSerializer } from './notebookSerializer'; -import * as NotebookImagePaste from './notebookImagePaste'; +import { ensureAllNewCellsHaveCellIds } from './cellIdService'; +import { notebookImagePasteSetup } from './notebookImagePaste'; +import { AttachmentCleaner } from './notebookAttachmentCleaner'; // From {nbformat.INotebookMetadata} in @jupyterlab/coreutils type NotebookMetadata = { @@ -78,7 +79,13 @@ export function activate(context: vscode.ExtensionContext) { await vscode.window.showNotebookDocument(document); })); - context.subscriptions.push(NotebookImagePaste.imagePasteSetup()); + context.subscriptions.push(notebookImagePasteSetup()); + + const enabled = vscode.workspace.getConfiguration('ipynb').get('experimental.pasteImages.enabled', false); + if (enabled) { + const cleaner = new AttachmentCleaner(); + context.subscriptions.push(cleaner); + } // Update new file contribution vscode.extensions.onDidChange(() => { diff --git a/extensions/ipynb/src/notebookAttachmentCleaner.ts b/extensions/ipynb/src/notebookAttachmentCleaner.ts new file mode 100644 index 00000000000..a87783fe5b8 --- /dev/null +++ b/extensions/ipynb/src/notebookAttachmentCleaner.ts @@ -0,0 +1,348 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { ATTACHMENT_CLEANUP_COMMANDID, JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR } from './constants'; +import { DebounceTrigger, deepClone, objectEquals } from './helper'; + +interface AttachmentCleanRequest { + notebook: vscode.NotebookDocument; + document: vscode.TextDocument; + cell: vscode.NotebookCell; +} + +interface IAttachmentData { + [key: string /** mimetype */]: string;/** b64-encoded */ +} + +interface IAttachmentDiagnostic { + name: string; + ranges: vscode.Range[]; +} + +export enum DiagnosticCode { + missing_attachment = 'notebook.missing-attachment' +} + +export class AttachmentCleaner implements vscode.CodeActionProvider { + private _attachmentCache: + Map>> = new Map(); + + private _disposables: vscode.Disposable[]; + private _imageDiagnosticCollection: vscode.DiagnosticCollection; + constructor() { + this._disposables = []; + const debounceTrigger = new DebounceTrigger({ + callback: (change: AttachmentCleanRequest) => { + this.cleanNotebookAttachments(change); + }, + delay: 500 + }); + + this._imageDiagnosticCollection = vscode.languages.createDiagnosticCollection('Notebook Image Attachment'); + this._disposables.push(this._imageDiagnosticCollection); + + this._disposables.push(vscode.commands.registerCommand(ATTACHMENT_CLEANUP_COMMANDID, async (document: vscode.Uri, range: vscode.Range) => { + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.delete(document, range); + await vscode.workspace.applyEdit(workspaceEdit); + })); + + this._disposables.push(vscode.languages.registerCodeActionsProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, this, { + providedCodeActionKinds: [ + vscode.CodeActionKind.QuickFix + ], + })); + + this._disposables.push(vscode.workspace.onDidChangeNotebookDocument(e => { + e.cellChanges.forEach(change => { + if (!change.document) { + return; + } + + if (change.cell.kind !== vscode.NotebookCellKind.Markup) { + return; + } + + debounceTrigger.trigger({ + notebook: e.notebook, + cell: change.cell, + document: change.document + }); + }); + })); + + this._disposables.push(vscode.workspace.onDidCloseNotebookDocument(e => { + this._attachmentCache.delete(e.uri.toString()); + })); + + this._disposables.push(vscode.workspace.onWillRenameFiles(e => { + const re = /\.ipynb$/; + for (const file of e.files) { + if (!re.exec(file.oldUri.toString())) { + continue; + } + + // transfer cache to new uri + if (this._attachmentCache.has(file.oldUri.toString())) { + this._attachmentCache.set(file.newUri.toString(), this._attachmentCache.get(file.oldUri.toString())!); + this._attachmentCache.delete(file.oldUri.toString()); + } + } + })); + + this._disposables.push(vscode.workspace.onDidOpenTextDocument(e => { + this.analyzeMissingAttachments(e); + })); + + this._disposables.push(vscode.workspace.onDidCloseTextDocument(e => { + this.analyzeMissingAttachments(e); + })); + + vscode.workspace.textDocuments.forEach(document => { + this.analyzeMissingAttachments(document); + }); + } + + provideCodeActions(document: vscode.TextDocument, _range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, _token: vscode.CancellationToken): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + const fixes: vscode.CodeAction[] = []; + + for (const diagnostic of context.diagnostics) { + switch (diagnostic.code) { + case DiagnosticCode.missing_attachment: + { + const fix = new vscode.CodeAction( + 'Remove invalid image attachment reference', + vscode.CodeActionKind.QuickFix); + + fix.command = { + command: ATTACHMENT_CLEANUP_COMMANDID, + title: 'Remove invalid image attachment reference', + arguments: [document.uri, diagnostic.range], + }; + fixes.push(fix); + } + break; + } + } + + return fixes; + } + + /** + * take in a NotebookDocumentChangeEvent, and clean the attachment data for the cell(s) that have had their markdown source code changed + * @param e NotebookDocumentChangeEvent from the onDidChangeNotebookDocument listener + */ + private cleanNotebookAttachments(e: AttachmentCleanRequest) { + if (e.notebook.isClosed) { + return; + } + const document = e.document; + const cell = e.cell; + + const markdownAttachmentsInUse: { [key: string /** filename */]: IAttachmentData } = {}; + const cellFragment = cell.document.uri.fragment; + const notebookUri = e.notebook.uri.toString(); + const diagnostics: IAttachmentDiagnostic[] = []; + const markdownAttachmentsRefedInCell = this.getAttachmentNames(document); + + if (markdownAttachmentsRefedInCell.size === 0) { + // no attachments used in this cell, cache all images from cell metadata + this.saveAllAttachmentsToCache(cell.metadata, notebookUri, cellFragment); + } + + if (this.checkMetadataAttachmentsExistence(cell.metadata)) { + // the cell metadata contains attachments, check if any are used in the markdown source + + for (const currFilename of Object.keys(cell.metadata.custom.attachments)) { + // means markdown reference is present in the metadata, rendering will work properly + // therefore, we don't need to check it in the next loop either + if (markdownAttachmentsRefedInCell.has(currFilename)) { + // attachment reference is present in the markdown source, no need to cache it + markdownAttachmentsRefedInCell.get(currFilename)!.valid = true; + markdownAttachmentsInUse[currFilename] = cell.metadata.custom.attachments[currFilename]; + } else { + // attachment reference is not present in the markdown source, cache it + this.saveAttachmentToCache(notebookUri, cellFragment, currFilename, cell.metadata); + } + } + } + + for (const [currFilename, attachment] of markdownAttachmentsRefedInCell) { + if (attachment.valid) { + // attachment reference is present in both the markdown source and the metadata, no op + continue; + } + + // if image is referenced in markdown source but not in metadata -> check if we have image in the cache + const cachedImageAttachment = this._attachmentCache.get(notebookUri)?.get(cellFragment)?.get(currFilename); + if (cachedImageAttachment) { + markdownAttachmentsInUse[currFilename] = cachedImageAttachment; + this._attachmentCache.get(notebookUri)?.get(cellFragment)?.delete(currFilename); + } else { + // if image is not in the cache, show warning + diagnostics.push({ name: currFilename, ranges: attachment.ranges }); + } + } + + if (!objectEquals(markdownAttachmentsInUse, cell.metadata.custom.attachments)) { + const updateMetadata: { [key: string]: any } = deepClone(cell.metadata); + updateMetadata.custom.attachments = markdownAttachmentsInUse; + const metadataEdit = vscode.NotebookEdit.updateCellMetadata(cell.index, updateMetadata); + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.set(e.notebook.uri, [metadataEdit]); + vscode.workspace.applyEdit(workspaceEdit); + } + + this.updateDiagnostics(cell.document.uri, diagnostics); + } + + private analyzeMissingAttachments(document: vscode.TextDocument): void { + if (document.uri.scheme !== 'vscode-notebook-cell') { + // not notebook + return; + } + + if (document.isClosed) { + this.updateDiagnostics(document.uri, []); + return; + } + + let notebook: vscode.NotebookDocument | undefined; + let activeCell: vscode.NotebookCell | undefined; + for (const notebookDocument of vscode.workspace.notebookDocuments) { + const cell = notebookDocument.getCells().find(cell => cell.document === document); + if (cell) { + notebook = notebookDocument; + activeCell = cell; + break; + } + } + + if (!notebook || !activeCell) { + return; + } + + const diagnostics: IAttachmentDiagnostic[] = []; + const markdownAttachments = this.getAttachmentNames(document); + if (this.checkMetadataAttachmentsExistence(activeCell.metadata)) { + for (const [currFilename, attachment] of markdownAttachments) { + if (!activeCell.metadata.custom.attachments[currFilename]) { + // no attachment reference in the metadata + diagnostics.push({ name: currFilename, ranges: attachment.ranges }); + } + } + } + + this.updateDiagnostics(activeCell.document.uri, diagnostics); + } + + private updateDiagnostics(cellUri: vscode.Uri, diagnostics: IAttachmentDiagnostic[]) { + const vscodeDiagnostics: vscode.Diagnostic[] = []; + for (const currDiagnostic of diagnostics) { + currDiagnostic.ranges.forEach(range => { + const diagnostic = new vscode.Diagnostic(range, `The image named: '${currDiagnostic.name}' is not present in cell metadata.`, vscode.DiagnosticSeverity.Warning); + diagnostic.code = DiagnosticCode.missing_attachment; + vscodeDiagnostics.push(diagnostic); + }); + } + + this._imageDiagnosticCollection.set(cellUri, vscodeDiagnostics); + } + + /** + * remove attachment from metadata and add it to the cache + * @param notebookUri uri of the notebook currently being edited + * @param cellFragment fragment of the cell currently being edited + * @param currFilename filename of the image being pulled into the cell + * @param metadata metadata of the cell currently being edited + */ + private saveAttachmentToCache(notebookUri: string, cellFragment: string, currFilename: string, metadata: { [key: string]: any }): void { + const documentCache = this._attachmentCache.get(notebookUri); + if (!documentCache) { + // no cache for this notebook yet + const cellCache = new Map(); + cellCache.set(currFilename, this.getMetadataAttachment(metadata, currFilename)); + const documentCache = new Map(); + documentCache.set(cellFragment, cellCache); + this._attachmentCache.set(notebookUri, documentCache); + } else if (!documentCache.has(cellFragment)) { + // no cache for this cell yet + const cellCache = new Map(); + cellCache.set(currFilename, this.getMetadataAttachment(metadata, currFilename)); + documentCache.set(cellFragment, cellCache); + } else { + // cache for this cell already exists + // add to cell cache + documentCache.get(cellFragment)?.set(currFilename, this.getMetadataAttachment(metadata, currFilename)); + } + } + + /** + * get an attachment entry from the given metadata + * @param metadata metadata to extract image data from + * @param currFilename filename of image being extracted + * @returns + */ + private getMetadataAttachment(metadata: { [key: string]: any }, currFilename: string): { [key: string]: any } { + return metadata.custom.attachments[currFilename]; + } + + /** + * returns a boolean that represents if there are any images in the attachment field of a cell's metadata + * @param metadata metadata of cell + * @returns boolean representing the presence of any attachments + */ + private checkMetadataAttachmentsExistence(metadata: { [key: string]: any }): boolean { + return !!(metadata.custom?.attachments); + } + + /** + * given metadata from a cell, cache every image (used in cases with no image links in markdown source) + * @param metadata metadata for a cell with no images in markdown source + * @param notebookUri uri for the notebook being edited + * @param cellFragment fragment of cell being edited + */ + private saveAllAttachmentsToCache(metadata: { [key: string]: any }, notebookUri: string, cellFragment: string): void { + const documentCache = this._attachmentCache.get(notebookUri) ?? new Map(); + this._attachmentCache.set(notebookUri, documentCache); + const cellCache = documentCache.get(cellFragment) ?? new Map(); + documentCache.set(cellFragment, cellCache); + + for (const currFilename of Object.keys(metadata.custom.attachments)) { + cellCache.set(currFilename, metadata.custom.attachments[currFilename]); + } + } + + /** + * pass in all of the markdown source code, and get a dictionary of all images referenced in the markdown. keys are image filenames, values are render state + * @param document the text document for the cell, formatted as a string + */ + private getAttachmentNames(document: vscode.TextDocument) { + const source = document.getText(); + const filenames: Map = new Map(); + const re = /!\[.*?\]\(attachment:(?.*?)\)/gm; + + let match; + while ((match = re.exec(source))) { + if (match.groups?.filename) { + const index = match.index; + const length = match[0].length; + const startPosition = document.positionAt(index); + const endPosition = document.positionAt(index + length); + const range = new vscode.Range(startPosition, endPosition); + const filename = filenames.get(match.groups.filename) ?? { valid: false, ranges: [] }; + filenames.set(match.groups.filename, filename); + filename.ranges.push(range); + } + } + return filenames; + } + + dispose() { + this._disposables.forEach(d => d.dispose()); + } +} + diff --git a/extensions/ipynb/src/notebookImagePaste.ts b/extensions/ipynb/src/notebookImagePaste.ts index bf2c8b4467e..4157d544a53 100644 --- a/extensions/ipynb/src/notebookImagePaste.ts +++ b/extensions/ipynb/src/notebookImagePaste.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR } from './constants'; class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider { @@ -150,9 +151,8 @@ function buildAttachment(b64: string, cell: vscode.NotebookCell, filename: strin }; } -export function imagePasteSetup() { - const selector: vscode.DocumentSelector = { notebookType: 'jupyter-notebook', language: 'markdown' }; // this is correct provider - return vscode.languages.registerDocumentPasteEditProvider(selector, new CopyPasteEditProvider(), { +export function notebookImagePasteSetup() { + return vscode.languages.registerDocumentPasteEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, new CopyPasteEditProvider(), { pasteMimeTypes: ['image/png'], }); } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 4cdd6394c84..80b22c4cee1 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -95,6 +95,8 @@ export const fixedDiffEditorOptions: IDiffEditorConstructionOptions = { readOnly: false, isInEmbeddedEditor: true, renderOverviewRuler: false, + wordWrap: 'off', + diffWordWrap: 'off', diffAlgorithm: 'smart', }; @@ -549,8 +551,8 @@ abstract class AbstractElementRenderer extends Disposable { this._metadataEditorContainer?.classList.add('diff'); - const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellUri(this.cell.originalDocument.uri, this.cell.original!.handle, Schemas.vscodeNotebookCellMetadata)); - const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellUri(this.cell.modifiedDocument.uri, this.cell.modified!.handle, Schemas.vscodeNotebookCellMetadata)); + const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.originalDocument.uri, this.cell.original!.handle, Schemas.vscodeNotebookCellMetadata)); + const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.modifiedDocument.uri, this.cell.modified!.handle, Schemas.vscodeNotebookCellMetadata)); this._metadataEditor.setModel({ original: originalMetadataModel.object.textEditorModel, modified: modifiedMetadataModel.object.textEditorModel @@ -612,7 +614,7 @@ abstract class AbstractElementRenderer extends Disposable { ? this.cell.modified!.handle : this.cell.original!.handle; - const modelUri = CellUri.generateCellUri(uri, handle, Schemas.vscodeNotebookCellMetadata); + const modelUri = CellUri.generateCellPropertyUri(uri, handle, Schemas.vscodeNotebookCellMetadata); const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); this._metadataEditor.setModel(metadataModel); this._metadataEditorDisposeStore.add(metadataModel); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index c6f5c162e10..489c3a3670d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -380,7 +380,7 @@ class CellInfoContentProvider { return existing; } - const data = CellUri.parseCellUri(resource, Schemas.vscodeNotebookCellMetadata); + const data = CellUri.parseCellPropertyUri(resource, Schemas.vscodeNotebookCellMetadata); if (!data) { return null; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 2e0b1580f12..361c0baa2b1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -560,9 +560,6 @@ export namespace CellUri { }; } - - const _regex = /^(\d{8,})(\w[\w\d+.-]*)$/; - export function generateCellOutputUri(notebook: URI, outputId?: string) { return notebook.with({ scheme: Schemas.vscodeNotebookCellOutput, @@ -591,29 +588,16 @@ export namespace CellUri { }; } - export function generateCellUri(notebook: URI, handle: number, scheme: string): URI { - return notebook.with({ - scheme: scheme, - fragment: `ch${handle.toString().padStart(7, '0')}${notebook.scheme !== Schemas.file ? notebook.scheme : ''}` - }); + export function generateCellPropertyUri(notebook: URI, handle: number, scheme: string): URI { + return CellUri.generate(notebook, handle).with({ scheme: scheme }); } - export function parseCellUri(metadata: URI, scheme: string) { - if (metadata.scheme !== scheme) { + export function parseCellPropertyUri(uri: URI, propertyScheme: string) { + if (uri.scheme !== propertyScheme) { return undefined; } - const match = _regex.exec(metadata.fragment); - if (!match) { - return undefined; - } - const handle = Number(match[1]); - return { - handle, - notebook: metadata.with({ - scheme: metadata.fragment.substring(match[0].length) || Schemas.file, - fragment: null - }) - }; + + return CellUri.parse(uri.with({ scheme: scheme })); } } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 20ac638f80e..2dad23406e6 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -13220,7 +13220,7 @@ declare module 'vscode' { export interface NotebookDocumentCellChange { /** - * The affected notebook. + * The affected cell. */ readonly cell: NotebookCell;