Skip to content

Commit 42238bd

Browse files
Michael Livelyrebornix
andauthored
notebook image cleaning automation (microsoft#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 <[email protected]>
1 parent df51f5a commit 42238bd

File tree

11 files changed

+569
-59
lines changed

11 files changed

+569
-59
lines changed

extensions/ipynb/.vscode/launch.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"args": [
9+
"--extensionDevelopmentPath=${workspaceFolder}"
10+
],
11+
"name": "Launch Extension",
12+
"outFiles": [
13+
"${workspaceFolder}/out/**/*.js"
14+
],
15+
"request": "launch",
16+
"type": "extensionHost"
17+
}
18+
]
19+
}

extensions/ipynb/package.json

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"vscode": "^1.57.0"
1010
},
1111
"enabledApiProposals": [
12-
"documentPaste"
12+
"documentPaste"
1313
],
1414
"activationEvents": [
1515
"*"
@@ -27,21 +27,21 @@
2727
}
2828
},
2929
"contributes": {
30-
"configuration":[
31-
{
32-
"properties": {
33-
"ipynb.experimental.pasteImages.enabled":{
34-
"type": "boolean",
35-
"scope": "resource",
36-
"markdownDescription": "%ipynb.experimental.pasteImages.enabled%",
37-
"default": false,
38-
"tags": [
39-
"experimental"
40-
]
41-
}
42-
}
43-
}
44-
],
30+
"configuration": [
31+
{
32+
"properties": {
33+
"ipynb.experimental.pasteImages.enabled": {
34+
"type": "boolean",
35+
"scope": "resource",
36+
"markdownDescription": "%ipynb.experimental.pasteImages.enabled%",
37+
"default": false,
38+
"tags": [
39+
"experimental"
40+
]
41+
}
42+
}
43+
}
44+
],
4545
"commands": [
4646
{
4747
"command": "ipynb.newUntitledIpynb",
@@ -52,6 +52,10 @@
5252
{
5353
"command": "ipynb.openIpynbInNotebookEditor",
5454
"title": "Open ipynb file in notebook editor"
55+
},
56+
{
57+
"command": "ipynb.cleanInvalidImageAttachment",
58+
"title": "Clean invalid image attachment reference"
5559
}
5660
],
5761
"notebooks": [
@@ -66,16 +70,16 @@
6670
"priority": "default"
6771
}
6872
],
69-
"notebookRenderer": [
70-
{
71-
"id": "vscode.markdown-it-cell-attachment-renderer",
72-
"displayName": "Markdown it ipynb Cell Attachment renderer",
73-
"entrypoint": {
74-
"extends": "vscode.markdown-it-renderer",
75-
"path": "./notebook-out/cellAttachmentRenderer.js"
76-
}
77-
}
78-
],
73+
"notebookRenderer": [
74+
{
75+
"id": "vscode.markdown-it-cell-attachment-renderer",
76+
"displayName": "Markdown it ipynb Cell Attachment renderer",
77+
"entrypoint": {
78+
"extends": "vscode.markdown-it-renderer",
79+
"path": "./notebook-out/cellAttachmentRenderer.js"
80+
}
81+
}
82+
],
7983
"menus": {
8084
"file/newFile": [
8185
{
@@ -90,6 +94,10 @@
9094
{
9195
"command": "ipynb.openIpynbInNotebookEditor",
9296
"when": "false"
97+
},
98+
{
99+
"command": "ipynb.cleanInvalidImageAttachment",
100+
"when": "false"
93101
}
94102
]
95103
}

extensions/ipynb/src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import * as vscode from 'vscode';
7+
68
export const defaultNotebookFormat = { major: 4, minor: 2 };
9+
export const ATTACHMENT_CLEANUP_COMMANDID = 'ipynb.cleanInvalidImageAttachment';
10+
11+
export const JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR: vscode.DocumentSelector = { notebookType: 'jupyter-notebook', language: 'markdown' };

extensions/ipynb/src/helper.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
export function deepClone<T>(obj: T): T {
7+
if (!obj || typeof obj !== 'object') {
8+
return obj;
9+
}
10+
if (obj instanceof RegExp) {
11+
// See https://github.com/microsoft/TypeScript/issues/10990
12+
return obj as any;
13+
}
14+
const result: any = Array.isArray(obj) ? [] : {};
15+
Object.keys(<any>obj).forEach((key: string) => {
16+
if ((<any>obj)[key] && typeof (<any>obj)[key] === 'object') {
17+
result[key] = deepClone((<any>obj)[key]);
18+
} else {
19+
result[key] = (<any>obj)[key];
20+
}
21+
});
22+
return result;
23+
}
24+
25+
// from https://github.com/microsoft/vscode/blob/43ae27a30e7b5e8711bf6b218ee39872ed2b8ef6/src/vs/base/common/objects.ts#L117
26+
export function objectEquals(one: any, other: any) {
27+
if (one === other) {
28+
return true;
29+
}
30+
if (one === null || one === undefined || other === null || other === undefined) {
31+
return false;
32+
}
33+
if (typeof one !== typeof other) {
34+
return false;
35+
}
36+
if (typeof one !== 'object') {
37+
return false;
38+
}
39+
if ((Array.isArray(one)) !== (Array.isArray(other))) {
40+
return false;
41+
}
42+
43+
let i: number;
44+
let key: string;
45+
46+
if (Array.isArray(one)) {
47+
if (one.length !== other.length) {
48+
return false;
49+
}
50+
for (i = 0; i < one.length; i++) {
51+
if (!objectEquals(one[i], other[i])) {
52+
return false;
53+
}
54+
}
55+
} else {
56+
const oneKeys: string[] = [];
57+
58+
for (key in one) {
59+
oneKeys.push(key);
60+
}
61+
oneKeys.sort();
62+
const otherKeys: string[] = [];
63+
for (key in other) {
64+
otherKeys.push(key);
65+
}
66+
otherKeys.sort();
67+
if (!objectEquals(oneKeys, otherKeys)) {
68+
return false;
69+
}
70+
for (i = 0; i < oneKeys.length; i++) {
71+
if (!objectEquals(one[oneKeys[i]], other[oneKeys[i]])) {
72+
return false;
73+
}
74+
}
75+
}
76+
77+
return true;
78+
}
79+
80+
interface Options<T> {
81+
callback: (value: T) => void;
82+
83+
merge?: (input: T[]) => T;
84+
delay?: number;
85+
}
86+
87+
88+
export class DebounceTrigger<T> {
89+
90+
private _isPaused = 0;
91+
protected _queue: T[] = [];
92+
private _callbackFn: (value: T) => void;
93+
private _mergeFn?: (input: T[]) => T;
94+
private readonly _delay: number;
95+
private _handle: any | undefined;
96+
97+
constructor(options: Options<T>) {
98+
this._callbackFn = options.callback;
99+
this._mergeFn = options.merge;
100+
this._delay = options.delay ?? 100;
101+
}
102+
103+
private pause(): void {
104+
this._isPaused++;
105+
}
106+
107+
private resume(): void {
108+
if (this._isPaused !== 0 && --this._isPaused === 0) {
109+
if (this._mergeFn) {
110+
const items = Array.from(this._queue);
111+
this._queue = [];
112+
this._callbackFn(this._mergeFn(items));
113+
114+
} else {
115+
while (!this._isPaused && this._queue.length !== 0) {
116+
this._callbackFn(this._queue.shift()!);
117+
}
118+
}
119+
}
120+
}
121+
122+
trigger(item: T): void {
123+
if (!this._handle) {
124+
this.pause();
125+
this._handle = setTimeout(() => {
126+
this._handle = undefined;
127+
this.resume();
128+
}, this._delay);
129+
}
130+
131+
if (this._isPaused !== 0) {
132+
this._queue.push(item);
133+
} else {
134+
this._callbackFn(item);
135+
}
136+
}
137+
}

extensions/ipynb/src/ipynbMain.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { ensureAllNewCellsHaveCellIds } from './cellIdService';
87
import { NotebookSerializer } from './notebookSerializer';
9-
import * as NotebookImagePaste from './notebookImagePaste';
8+
import { ensureAllNewCellsHaveCellIds } from './cellIdService';
9+
import { notebookImagePasteSetup } from './notebookImagePaste';
10+
import { AttachmentCleaner } from './notebookAttachmentCleaner';
1011

1112
// From {nbformat.INotebookMetadata} in @jupyterlab/coreutils
1213
type NotebookMetadata = {
@@ -78,7 +79,13 @@ export function activate(context: vscode.ExtensionContext) {
7879
await vscode.window.showNotebookDocument(document);
7980
}));
8081

81-
context.subscriptions.push(NotebookImagePaste.imagePasteSetup());
82+
context.subscriptions.push(notebookImagePasteSetup());
83+
84+
const enabled = vscode.workspace.getConfiguration('ipynb').get('experimental.pasteImages.enabled', false);
85+
if (enabled) {
86+
const cleaner = new AttachmentCleaner();
87+
context.subscriptions.push(cleaner);
88+
}
8289

8390
// Update new file contribution
8491
vscode.extensions.onDidChange(() => {

0 commit comments

Comments
 (0)