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 <penn.lv@gmail.com>
This commit is contained in:
Michael Lively 2022-09-09 11:32:10 -07:00 коммит произвёл GitHub
Родитель df51f5af27
Коммит 42238bdaab
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 569 добавлений и 59 удалений

19
extensions/ipynb/.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -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"
}
]
}

Просмотреть файл

@ -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"
}
]
}

Просмотреть файл

@ -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' };

Просмотреть файл

@ -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<T>(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(<any>obj).forEach((key: string) => {
if ((<any>obj)[key] && typeof (<any>obj)[key] === 'object') {
result[key] = deepClone((<any>obj)[key]);
} else {
result[key] = (<any>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<T> {
callback: (value: T) => void;
merge?: (input: T[]) => T;
delay?: number;
}
export class DebounceTrigger<T> {
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<T>) {
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);
}
}
}

Просмотреть файл

@ -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(() => {

Просмотреть файл

@ -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<string /** uri */, Map<string /** cell fragment*/, Map<string /** attachment filename */, IAttachmentData>>> = new Map();
private _disposables: vscode.Disposable[];
private _imageDiagnosticCollection: vscode.DiagnosticCollection;
constructor() {
this._disposables = [];
const debounceTrigger = new DebounceTrigger<AttachmentCleanRequest>({
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<string, IAttachmentData>();
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<string, IAttachmentData>();
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<string, IAttachmentData>();
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<string, { valid: boolean; ranges: vscode.Range[] }> = new Map();
const re = /!\[.*?\]\(attachment:(?<filename>.*?)\)/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());
}
}

Просмотреть файл

@ -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'],
});
}

Просмотреть файл

@ -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);

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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 }));
}
}

2
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;