From eb948e1ab794c689d51578384f154394a294c241 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 26 Aug 2024 16:58:43 +0200 Subject: [PATCH 01/11] Use Worker to serialize Notebook --- extensions/ipynb/src/common.ts | 5 +++ extensions/ipynb/src/ipynbMain.ts | 4 +-- extensions/ipynb/src/notebookSerializer.ts | 33 ++++++++++++++++--- .../ipynb/src/notebookSerializerWorker.ts | 14 ++++++++ 4 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 extensions/ipynb/src/notebookSerializerWorker.ts diff --git a/extensions/ipynb/src/common.ts b/extensions/ipynb/src/common.ts index f8514216fef373..b5f7db1e0263cd 100644 --- a/extensions/ipynb/src/common.ts +++ b/extensions/ipynb/src/common.ts @@ -64,3 +64,8 @@ export interface CellMetadata { */ execution_count?: number; } + +export interface notebookSerializationWorkerData { + notebookContent: Partial; + indentAmount: string; +} diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index 58c7e100b3b0f6..a81d1809c5ed43 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -105,7 +105,7 @@ export function activate(context: vscode.ExtensionContext) { get dropCustomMetadata() { return true; }, - exportNotebook: (notebook: vscode.NotebookData): string => { + exportNotebook: (notebook: vscode.NotebookData): Promise => { return exportNotebook(notebook, serializer); }, setNotebookMetadata: async (resource: vscode.Uri, metadata: Partial): Promise => { @@ -127,7 +127,7 @@ export function activate(context: vscode.ExtensionContext) { }; } -function exportNotebook(notebook: vscode.NotebookData, serializer: NotebookSerializer): string { +function exportNotebook(notebook: vscode.NotebookData, serializer: NotebookSerializer): Promise { return serializer.serializeNotebookToString(notebook); } diff --git a/extensions/ipynb/src/notebookSerializer.ts b/extensions/ipynb/src/notebookSerializer.ts index 3304853d894ff0..ead5baa341ba3d 100644 --- a/extensions/ipynb/src/notebookSerializer.ts +++ b/extensions/ipynb/src/notebookSerializer.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'node:path'; +import { Worker } from 'node:worker_threads'; import type * as nbformat from '@jupyterlab/nbformat'; import * as detectIndent from 'detect-indent'; import * as vscode from 'vscode'; @@ -10,6 +12,7 @@ import { defaultNotebookFormat } from './constants'; import { getPreferredLanguage, jupyterNotebookModelToNotebookData } from './deserializers'; import { createJupyterCellFromNotebookCell, pruneCell, sortObjectPropertiesRecursively } from './serializers'; import * as fnv from '@enonic/fnv-plus'; +import { notebookSerializationWorkerData } from './common'; export class NotebookSerializer implements vscode.NotebookSerializer { constructor(readonly context: vscode.ExtensionContext) { @@ -70,11 +73,25 @@ export class NotebookSerializer implements vscode.NotebookSerializer { return data; } - public serializeNotebook(data: vscode.NotebookData, _token: vscode.CancellationToken): Uint8Array { - return new TextEncoder().encode(this.serializeNotebookToString(data)); + public async serializeNotebook(data: vscode.NotebookData, _token: vscode.CancellationToken): Promise { + return new TextEncoder().encode(await this.serializeNotebookToString(data)); } - public serializeNotebookToString(data: vscode.NotebookData): string { + private serializeViaWorker(workerData: notebookSerializationWorkerData): Promise { + return new Promise((resolve, reject) => { + const workerFile = path.join(__dirname, 'notebookSerializerWorker.js'); + const worker = new Worker(workerFile, { workerData }); + worker.on('message', resolve); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Worker stopped with exit code ${code}`)); + } + }); + }); + } + + public serializeNotebookToString(data: vscode.NotebookData): Promise { const notebookContent = getNotebookMetadata(data); // use the preferred language from document metadata or the first cell language as the notebook preferred cell language const preferredCellLanguage = notebookContent.metadata?.language_info?.name ?? data.cells.find(cell => cell.kind === vscode.NotebookCellKind.Code)?.languageId; @@ -83,11 +100,17 @@ export class NotebookSerializer implements vscode.NotebookSerializer { .map(cell => createJupyterCellFromNotebookCell(cell, preferredCellLanguage)) .map(pruneCell); + // ipynb always ends with a trailing new line (we add this so that SCMs do not show unnecessary changes, resulting from a missing trailing new line). + const sorted = sortObjectPropertiesRecursively(notebookContent); + const indentAmount = data.metadata && 'indentAmount' in data.metadata && typeof data.metadata.indentAmount === 'string' ? data.metadata.indentAmount : ' '; - // ipynb always ends with a trailing new line (we add this so that SCMs do not show unnecessary changes, resulting from a missing trailing new line). - return JSON.stringify(sortObjectPropertiesRecursively(notebookContent), undefined, indentAmount) + '\n'; + + return this.serializeViaWorker({ + notebookContent: sorted, + indentAmount + }); } } diff --git a/extensions/ipynb/src/notebookSerializerWorker.ts b/extensions/ipynb/src/notebookSerializerWorker.ts new file mode 100644 index 00000000000000..052821aa069c2f --- /dev/null +++ b/extensions/ipynb/src/notebookSerializerWorker.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { notebookSerializationWorkerData } from './common'; + +const { workerData, parentPort } = require('worker_threads'); + +if (parentPort) { + const { notebookContent, indentAmount } = workerData; + const json = JSON.stringify(notebookContent, undefined, indentAmount) + '\n'; + parentPort.postMessage(json); +} From 366df5a69d3738aeacae65eef269ade06296a84d Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 28 Aug 2024 10:59:51 +0200 Subject: [PATCH 02/11] Put worker behind setting --- extensions/ipynb/package.json | 6 ++++++ extensions/ipynb/package.nls.json | 1 + extensions/ipynb/src/notebookSerializer.ts | 13 +++++++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index d881eb8ca22163..784a352ad96000 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -39,6 +39,12 @@ "scope": "resource", "markdownDescription": "%ipynb.pasteImagesAsAttachments.enabled%", "default": true + }, + "ipynb.experimental.serialization": { + "type": "boolean", + "scope": "resource", + "markdownDescription": "%ipynb.experimental.serialization%", + "default": false } } } diff --git a/extensions/ipynb/package.nls.json b/extensions/ipynb/package.nls.json index 7a3d95181cfcd9..61eb74e242df2d 100644 --- a/extensions/ipynb/package.nls.json +++ b/extensions/ipynb/package.nls.json @@ -2,6 +2,7 @@ "displayName": ".ipynb Support", "description": "Provides basic support for opening and reading Jupyter's .ipynb notebook files", "ipynb.pasteImagesAsAttachments.enabled": "Enable/disable pasting of images into Markdown cells in ipynb notebook files. Pasted images are inserted as attachments to the cell.", + "ipynb.experimental.serialization": "Experimental feature to serialize the Jupyter notebook in a worker thread.", "newUntitledIpynb.title": "New Jupyter Notebook", "newUntitledIpynb.shortTitle": "Jupyter Notebook", "openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor", diff --git a/extensions/ipynb/src/notebookSerializer.ts b/extensions/ipynb/src/notebookSerializer.ts index ead5baa341ba3d..97d83c34db978c 100644 --- a/extensions/ipynb/src/notebookSerializer.ts +++ b/extensions/ipynb/src/notebookSerializer.ts @@ -107,10 +107,15 @@ export class NotebookSerializer implements vscode.NotebookSerializer { data.metadata.indentAmount : ' '; - return this.serializeViaWorker({ - notebookContent: sorted, - indentAmount - }); + const experimentalSave = vscode.workspace.getConfiguration('ipynb').get('experimental.serialization', false); + if (experimentalSave) { + return this.serializeViaWorker({ + notebookContent: sorted, + indentAmount + }); + } else { + return Promise.resolve(JSON.stringify(sorted, undefined, indentAmount)); + } } } From c363defd7374b9992bf73f9d586bd902174fcc34 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 2 Sep 2024 11:58:12 +0200 Subject: [PATCH 03/11] Update worker file to esm --- extensions/ipynb/src/notebookSerializerWorker.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/ipynb/src/notebookSerializerWorker.ts b/extensions/ipynb/src/notebookSerializerWorker.ts index 052821aa069c2f..c09e7a6c690096 100644 --- a/extensions/ipynb/src/notebookSerializerWorker.ts +++ b/extensions/ipynb/src/notebookSerializerWorker.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { notebookSerializationWorkerData } from './common'; - -const { workerData, parentPort } = require('worker_threads'); +import { workerData, parentPort } from 'node:worker_threads'; if (parentPort) { const { notebookContent, indentAmount } = workerData; From e3caeae15d58bca56978672ae427c220f4f7a406 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 2 Sep 2024 11:58:33 +0200 Subject: [PATCH 04/11] Verify that node worker is only used in node context. --- extensions/ipynb/src/notebookSerializer.ts | 39 +++++++++++++--------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/extensions/ipynb/src/notebookSerializer.ts b/extensions/ipynb/src/notebookSerializer.ts index 97d83c34db978c..e86432fa0cc5ce 100644 --- a/extensions/ipynb/src/notebookSerializer.ts +++ b/extensions/ipynb/src/notebookSerializer.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'node:path'; -import { Worker } from 'node:worker_threads'; import type * as nbformat from '@jupyterlab/nbformat'; import * as detectIndent from 'detect-indent'; import * as vscode from 'vscode'; @@ -77,8 +75,12 @@ export class NotebookSerializer implements vscode.NotebookSerializer { return new TextEncoder().encode(await this.serializeNotebookToString(data)); } - private serializeViaWorker(workerData: notebookSerializationWorkerData): Promise { - return new Promise((resolve, reject) => { + private async serializeViaWorker(workerData: notebookSerializationWorkerData): Promise { + const workerThreads = await import('node:worker_threads'); + const path = await import('node:path'); + const { Worker } = workerThreads; + + return await new Promise((resolve, reject) => { const workerFile = path.join(__dirname, 'notebookSerializerWorker.js'); const worker = new Worker(workerFile, { workerData }); worker.on('message', resolve); @@ -91,6 +93,22 @@ export class NotebookSerializer implements vscode.NotebookSerializer { }); } + private serializeNotebookToJSON(notebookContent: Partial, indentAmount: string): Promise { + // ipynb always ends with a trailing new line (we add this so that SCMs do not show unnecessary changes, resulting from a missing trailing new line). + const sorted = sortObjectPropertiesRecursively(notebookContent); + + const isInNodeJSContext = typeof process !== 'undefined' && process.release && process.release.name === 'node'; + const experimentalSave = vscode.workspace.getConfiguration('ipynb').get('experimental.serialization', false); + if (isInNodeJSContext && experimentalSave) { + return this.serializeViaWorker({ + notebookContent: sorted, + indentAmount + }); + } else { + return Promise.resolve(JSON.stringify(sorted, undefined, indentAmount)); + } + } + public serializeNotebookToString(data: vscode.NotebookData): Promise { const notebookContent = getNotebookMetadata(data); // use the preferred language from document metadata or the first cell language as the notebook preferred cell language @@ -100,22 +118,11 @@ export class NotebookSerializer implements vscode.NotebookSerializer { .map(cell => createJupyterCellFromNotebookCell(cell, preferredCellLanguage)) .map(pruneCell); - // ipynb always ends with a trailing new line (we add this so that SCMs do not show unnecessary changes, resulting from a missing trailing new line). - const sorted = sortObjectPropertiesRecursively(notebookContent); - const indentAmount = data.metadata && 'indentAmount' in data.metadata && typeof data.metadata.indentAmount === 'string' ? data.metadata.indentAmount : ' '; - const experimentalSave = vscode.workspace.getConfiguration('ipynb').get('experimental.serialization', false); - if (experimentalSave) { - return this.serializeViaWorker({ - notebookContent: sorted, - indentAmount - }); - } else { - return Promise.resolve(JSON.stringify(sorted, undefined, indentAmount)); - } + return this.serializeNotebookToJSON(notebookContent, indentAmount); } } From 12abcccc8147cb30d5004fb41366b96670bdbf6c Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 2 Sep 2024 14:31:07 +0200 Subject: [PATCH 05/11] Duplicate sortObjectPropertiesRecursively --- extensions/ipynb/src/notebookSerializer.ts | 7 ++++--- .../ipynb/src/notebookSerializerWorker.ts | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/extensions/ipynb/src/notebookSerializer.ts b/extensions/ipynb/src/notebookSerializer.ts index e86432fa0cc5ce..f4868d22450fcc 100644 --- a/extensions/ipynb/src/notebookSerializer.ts +++ b/extensions/ipynb/src/notebookSerializer.ts @@ -94,17 +94,18 @@ export class NotebookSerializer implements vscode.NotebookSerializer { } private serializeNotebookToJSON(notebookContent: Partial, indentAmount: string): Promise { - // ipynb always ends with a trailing new line (we add this so that SCMs do not show unnecessary changes, resulting from a missing trailing new line). - const sorted = sortObjectPropertiesRecursively(notebookContent); const isInNodeJSContext = typeof process !== 'undefined' && process.release && process.release.name === 'node'; const experimentalSave = vscode.workspace.getConfiguration('ipynb').get('experimental.serialization', false); if (isInNodeJSContext && experimentalSave) { return this.serializeViaWorker({ - notebookContent: sorted, + notebookContent, indentAmount }); } else { + // ipynb always ends with a trailing new line (we add this so that SCMs do not show unnecessary changes, resulting from a missing trailing new line). + const sorted = sortObjectPropertiesRecursively(notebookContent); + return Promise.resolve(JSON.stringify(sorted, undefined, indentAmount)); } } diff --git a/extensions/ipynb/src/notebookSerializerWorker.ts b/extensions/ipynb/src/notebookSerializerWorker.ts index c09e7a6c690096..438d88bb145a06 100644 --- a/extensions/ipynb/src/notebookSerializerWorker.ts +++ b/extensions/ipynb/src/notebookSerializerWorker.ts @@ -6,8 +6,25 @@ import { notebookSerializationWorkerData } from './common'; import { workerData, parentPort } from 'node:worker_threads'; +function sortObjectPropertiesRecursively(obj: any): any { + if (Array.isArray(obj)) { + return obj.map(sortObjectPropertiesRecursively); + } + if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) { + return ( + Object.keys(obj) + .sort() + .reduce>((sortedObj, prop) => { + sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]); + return sortedObj; + }, {}) as any + ); + } + return obj; +} + if (parentPort) { const { notebookContent, indentAmount } = workerData; - const json = JSON.stringify(notebookContent, undefined, indentAmount) + '\n'; + const json = JSON.stringify(sortObjectPropertiesRecursively(notebookContent), undefined, indentAmount) + '\n'; parentPort.postMessage(json); } From eccf86a18e563fa84a98ebed596919e864fa5ef4 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 5 Sep 2024 10:43:53 +0200 Subject: [PATCH 06/11] Copy worker to dist folder --- extensions/ipynb/extension.webpack.config.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/extensions/ipynb/extension.webpack.config.js b/extensions/ipynb/extension.webpack.config.js index 784411bb066662..5efed185ba692b 100644 --- a/extensions/ipynb/extension.webpack.config.js +++ b/extensions/ipynb/extension.webpack.config.js @@ -7,6 +7,7 @@ 'use strict'; +const CopyPlugin = require('copy-webpack-plugin'); const withDefaults = require('../shared.webpack.config'); module.exports = withDefaults({ @@ -16,5 +17,16 @@ module.exports = withDefaults({ }, output: { filename: 'ipynbMain.js' - } + }, + plugins: [ + ...withDefaults.nodePlugins(__dirname), // add plugins, don't replace inherited + new CopyPlugin({ + patterns: [ + { + from: './out/notebookSerializerWorker.js', + to: './dist', + } + ], + }), + ] }); From b6669a7bff9bdf649f31efb356399ec616ef2e29 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 5 Sep 2024 10:46:14 +0200 Subject: [PATCH 07/11] Mention desktop application in setting description. --- extensions/ipynb/package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ipynb/package.nls.json b/extensions/ipynb/package.nls.json index 61eb74e242df2d..fa64baaae07c1d 100644 --- a/extensions/ipynb/package.nls.json +++ b/extensions/ipynb/package.nls.json @@ -2,7 +2,7 @@ "displayName": ".ipynb Support", "description": "Provides basic support for opening and reading Jupyter's .ipynb notebook files", "ipynb.pasteImagesAsAttachments.enabled": "Enable/disable pasting of images into Markdown cells in ipynb notebook files. Pasted images are inserted as attachments to the cell.", - "ipynb.experimental.serialization": "Experimental feature to serialize the Jupyter notebook in a worker thread.", + "ipynb.experimental.serialization": "Experimental feature to serialize the Jupyter notebook in a worker thread when using desktop application.", "newUntitledIpynb.title": "New Jupyter Notebook", "newUntitledIpynb.shortTitle": "Jupyter Notebook", "openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor", From cdc45ffb426874eef44c307c4b672415190ec947 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 5 Sep 2024 10:46:28 +0200 Subject: [PATCH 08/11] Include ending new line --- extensions/ipynb/src/notebookSerializer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ipynb/src/notebookSerializer.ts b/extensions/ipynb/src/notebookSerializer.ts index f4868d22450fcc..73d6867efb83f2 100644 --- a/extensions/ipynb/src/notebookSerializer.ts +++ b/extensions/ipynb/src/notebookSerializer.ts @@ -106,7 +106,7 @@ export class NotebookSerializer implements vscode.NotebookSerializer { // ipynb always ends with a trailing new line (we add this so that SCMs do not show unnecessary changes, resulting from a missing trailing new line). const sorted = sortObjectPropertiesRecursively(notebookContent); - return Promise.resolve(JSON.stringify(sorted, undefined, indentAmount)); + return Promise.resolve(JSON.stringify(sorted, undefined, indentAmount) + '\n'); } } From f7e0eabb6de3de83727e154acc712b1560f48d9c Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Thu, 5 Sep 2024 22:17:11 +0200 Subject: [PATCH 09/11] Update extensions/ipynb/package.nls.json Co-authored-by: Aaron Munger --- extensions/ipynb/package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ipynb/package.nls.json b/extensions/ipynb/package.nls.json index fa64baaae07c1d..9fc163137f8866 100644 --- a/extensions/ipynb/package.nls.json +++ b/extensions/ipynb/package.nls.json @@ -2,7 +2,7 @@ "displayName": ".ipynb Support", "description": "Provides basic support for opening and reading Jupyter's .ipynb notebook files", "ipynb.pasteImagesAsAttachments.enabled": "Enable/disable pasting of images into Markdown cells in ipynb notebook files. Pasted images are inserted as attachments to the cell.", - "ipynb.experimental.serialization": "Experimental feature to serialize the Jupyter notebook in a worker thread when using desktop application.", + "ipynb.experimental.serialization": "Experimental feature to serialize the Jupyter notebook in a worker thread. Not supported when the Extension host is running as a web worker.", "newUntitledIpynb.title": "New Jupyter Notebook", "newUntitledIpynb.shortTitle": "Jupyter Notebook", "openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor", From fc8fda56e50c909b43466d40afa496611a26145a Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 6 Sep 2024 22:16:04 -0700 Subject: [PATCH 10/11] fix worker import --- extensions/ipynb/extension.webpack.config.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/extensions/ipynb/extension.webpack.config.js b/extensions/ipynb/extension.webpack.config.js index 5efed185ba692b..6637eac6ae4b02 100644 --- a/extensions/ipynb/extension.webpack.config.js +++ b/extensions/ipynb/extension.webpack.config.js @@ -7,26 +7,15 @@ 'use strict'; -const CopyPlugin = require('copy-webpack-plugin'); const withDefaults = require('../shared.webpack.config'); module.exports = withDefaults({ context: __dirname, entry: { extension: './src/ipynbMain.ts', + notebookSerializerWorker: './src/notebookSerializerWorker.ts', }, output: { - filename: 'ipynbMain.js' - }, - plugins: [ - ...withDefaults.nodePlugins(__dirname), // add plugins, don't replace inherited - new CopyPlugin({ - patterns: [ - { - from: './out/notebookSerializerWorker.js', - to: './dist', - } - ], - }), - ] + filename: '[name].js' + } }); From f9adfe48ad6fa2ac345e2e12feb1c9dd103ca23d Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 6 Sep 2024 22:57:15 -0700 Subject: [PATCH 11/11] Fix cell status bar target check --- .../browser/view/cellParts/cellStatusPart.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index ed204686f0907b..f58ff0aa23fd15 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -105,7 +105,17 @@ export class CellEditorStatusBar extends CellContentPart { event: e }); } else { - if ((e.target as HTMLElement).classList.contains('cell-status-item-has-command')) { + const target = e.target; + let itemHasCommand = false; + if (target && DOM.isHTMLElement(target)) { + const targetElement = target; + if (targetElement.classList.contains('cell-status-item-has-command')) { + itemHasCommand = true; + } else if (targetElement.parentElement && targetElement.parentElement.classList.contains('cell-status-item-has-command')) { + itemHasCommand = true; + } + } + if (itemHasCommand) { this._onDidClick.fire({ type: ClickTargetType.ContributedCommandItem, event: e