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:
Родитель
df51f5af27
Коммит
42238bdaab
|
@ -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 }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13220,7 +13220,7 @@ declare module 'vscode' {
|
|||
export interface NotebookDocumentCellChange {
|
||||
|
||||
/**
|
||||
* The affected notebook.
|
||||
* The affected cell.
|
||||
*/
|
||||
readonly cell: NotebookCell;
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче