jupyterlab_nb_venv_kernels_ui_extension 1.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +47 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +518 -0
- package/package.json +206 -0
- package/src/__tests__/jupyterlab_nb_venv_kernels_ui_extension.spec.ts +570 -0
- package/src/index.ts +726 -0
- package/style/base.css +5 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Stellars Henson
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# jupyterlab_nb_venv_kernels_ui_extension
|
|
2
|
+
|
|
3
|
+
[](https://github.com/stellarshenson/jupyterlab_nb_venv_kernels_ui_extension/actions/workflows/build.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/jupyterlab_nb_venv_kernels_ui_extension)
|
|
5
|
+
[](https://pypi.org/project/jupyterlab-nb-venv-kernels-ui-extension/)
|
|
6
|
+
[](https://pepy.tech/project/jupyterlab-nb-venv-kernels-ui-extension)
|
|
7
|
+
[](https://jupyterlab.readthedocs.io/en/stable/)
|
|
8
|
+
[](https://kolomolo.com)
|
|
9
|
+
[](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
|
|
10
|
+
|
|
11
|
+
> [!IMPORTANT]
|
|
12
|
+
> **Package Renamed**: This package was renamed from `jupyterlab_launcher_navigate_to_kernel_extension` to `jupyterlab_nb_venv_kernels_ui_extension` in version 1.2.8. If you have the old package installed, please uninstall it first: `pip uninstall jupyterlab-launcher-navigate-to-kernel-extension`
|
|
13
|
+
|
|
14
|
+
Right-click on any kernel launcher card to navigate to its project directory or open a terminal there. Intended to help navigate around a busy workspace with many projects. Similar to [jupyterlab_terminal_show_in_file_browser_extension](https://github.com/stellarshenson/jupyterlab_terminal_show_in_file_browser_extension).
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
**Context Menu (right-click on kernel launcher card)**:
|
|
21
|
+
- **Show in File Browser** - Navigate to the kernel's project root
|
|
22
|
+
- **Open Terminal at Location** - Open terminal at the kernel's project directory
|
|
23
|
+
- **Unregister Kernel** - Remove kernel from `nb_venv_kernels` registry (requires `nb_venv_kernels`)
|
|
24
|
+
- **Remove Environment** - Permanently delete local `.venv` environments with confirmation (requires `nb_venv_kernels`)
|
|
25
|
+
|
|
26
|
+
**Kernel Menu**:
|
|
27
|
+
- **Scan for Virtual Environments** - Discover and register new virtual environments in your workspace (requires `nb_venv_kernels`)
|
|
28
|
+
|
|
29
|
+
**General**:
|
|
30
|
+
- **Project-aware navigation** - For `.venv` environments, navigates to project root (one level up from `.venv`)
|
|
31
|
+
- **Dynamic kernel support** - Works with `nb_conda_kernels` and `nb_venv_kernels` providers
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- JupyterLab >= 4.0.0
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install jupyterlab-nb-venv-kernels-ui-extension
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Uninstall
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip uninstall jupyterlab_nb_venv_kernels_ui_extension
|
|
47
|
+
```
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
2
|
+
import { ILauncher } from '@jupyterlab/launcher';
|
|
3
|
+
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
4
|
+
import { showErrorMessage, showDialog, Dialog } from '@jupyterlab/apputils';
|
|
5
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
6
|
+
import { URLExt, PageConfig } from '@jupyterlab/coreutils';
|
|
7
|
+
import { Widget } from '@lumino/widgets';
|
|
8
|
+
/**
|
|
9
|
+
* Command IDs for the extension.
|
|
10
|
+
*/
|
|
11
|
+
const SHOW_IN_BROWSER_CMD = 'launcher:show-kernel-in-file-browser';
|
|
12
|
+
const OPEN_TERMINAL_CMD = 'launcher:open-terminal-at-kernel';
|
|
13
|
+
const UNREGISTER_KERNEL_CMD = 'launcher:unregister-venv-kernel';
|
|
14
|
+
const REMOVE_ENVIRONMENT_CMD = 'launcher:remove-venv-environment';
|
|
15
|
+
/**
|
|
16
|
+
* Command ID for nb_venv_kernels refresh (provided by that extension).
|
|
17
|
+
*/
|
|
18
|
+
const NB_VENV_KERNELS_REFRESH_CMD = 'nb_venv_kernels:refresh';
|
|
19
|
+
/**
|
|
20
|
+
* Store for the last right-clicked kernel display name.
|
|
21
|
+
* This is set when a context menu is opened on a launcher card.
|
|
22
|
+
*/
|
|
23
|
+
let lastClickedKernelName = null;
|
|
24
|
+
/**
|
|
25
|
+
* Flag indicating whether nb_venv_kernels extension is available.
|
|
26
|
+
* Checked once at startup.
|
|
27
|
+
*/
|
|
28
|
+
let nbVenvKernelsAvailable = false;
|
|
29
|
+
/**
|
|
30
|
+
* Fetch the kernel path information from the server.
|
|
31
|
+
*
|
|
32
|
+
* @param displayName - The display name of the kernel
|
|
33
|
+
* @returns Promise resolving to kernel path info or null if not available
|
|
34
|
+
*/
|
|
35
|
+
async function fetchKernelPath(displayName) {
|
|
36
|
+
const settings = ServerConnection.makeSettings();
|
|
37
|
+
const url = URLExt.join(settings.baseUrl, 'api', 'kernel-path', encodeURIComponent(displayName));
|
|
38
|
+
try {
|
|
39
|
+
const response = await ServerConnection.makeRequest(url, {}, settings);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const data = (await response.json());
|
|
42
|
+
console.warn(`Failed to get kernel path: ${data.error}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const data = (await response.json());
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('Error fetching kernel path:', error);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if nb_venv_kernels extension is available.
|
|
55
|
+
*
|
|
56
|
+
* @returns Promise resolving to true if available, false otherwise
|
|
57
|
+
*/
|
|
58
|
+
async function checkNbVenvKernelsAvailable() {
|
|
59
|
+
const settings = ServerConnection.makeSettings();
|
|
60
|
+
const url = URLExt.join(settings.baseUrl, 'nb-venv-kernels', 'environments');
|
|
61
|
+
try {
|
|
62
|
+
const response = await ServerConnection.makeRequest(url, {}, settings);
|
|
63
|
+
return response.ok;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.debug('nb_venv_kernels extension not installed:', error);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fetch list of venv environments from nb_venv_kernels API.
|
|
72
|
+
*
|
|
73
|
+
* @returns Promise resolving to environments list or null if not available
|
|
74
|
+
*/
|
|
75
|
+
async function fetchVenvEnvironments() {
|
|
76
|
+
const settings = ServerConnection.makeSettings();
|
|
77
|
+
const url = URLExt.join(settings.baseUrl, 'nb-venv-kernels', 'environments');
|
|
78
|
+
try {
|
|
79
|
+
const response = await ServerConnection.makeRequest(url, {}, settings);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
console.warn('nb_venv_kernels API not available');
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const data = (await response.json());
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.debug('nb_venv_kernels extension not installed:', error);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Find a venv environment matching the given display name.
|
|
94
|
+
*
|
|
95
|
+
* @param displayName - The kernel display name to match
|
|
96
|
+
* @returns The matching environment or null
|
|
97
|
+
*/
|
|
98
|
+
async function findVenvEnvironment(displayName) {
|
|
99
|
+
const envData = await fetchVenvEnvironments();
|
|
100
|
+
if (!envData) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
// Find matching environment by name
|
|
104
|
+
// Display name patterns: "Python (envname)", "envname", "Python 3 (envname)"
|
|
105
|
+
for (const env of envData.environments) {
|
|
106
|
+
// Skip conda environments - they can't be unregistered via nb_venv_kernels
|
|
107
|
+
if (env.type === 'conda') {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const envName = env.name || '';
|
|
111
|
+
const customName = env.custom_name || '';
|
|
112
|
+
// Check if display_name contains the environment name
|
|
113
|
+
if ((envName && displayName.includes(envName)) ||
|
|
114
|
+
(customName && displayName.includes(customName))) {
|
|
115
|
+
return env;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Unregister a venv environment via nb_venv_kernels API.
|
|
122
|
+
*
|
|
123
|
+
* @param envPath - The path of the environment to unregister
|
|
124
|
+
* @returns Promise resolving to unregister result
|
|
125
|
+
*/
|
|
126
|
+
async function unregisterVenvKernel(envPath) {
|
|
127
|
+
const settings = ServerConnection.makeSettings();
|
|
128
|
+
const url = URLExt.join(settings.baseUrl, 'nb-venv-kernels', 'unregister');
|
|
129
|
+
try {
|
|
130
|
+
const response = await ServerConnection.makeRequest(url, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
body: JSON.stringify({ path: envPath })
|
|
133
|
+
}, settings);
|
|
134
|
+
const data = (await response.json());
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
error: data.error || 'Failed to unregister kernel'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return { success: true, message: data.message };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error('Error unregistering kernel:', error);
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
error: `Network error: ${error}`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Check if an environment path is a local .venv environment.
|
|
153
|
+
* Local environments have .venv in their path.
|
|
154
|
+
*
|
|
155
|
+
* @param envPath - The environment path to check
|
|
156
|
+
* @returns true if the environment is local (.venv based)
|
|
157
|
+
*/
|
|
158
|
+
function isLocalVenvEnvironment(envPath) {
|
|
159
|
+
return envPath.includes('/.venv') || envPath.includes('\\.venv');
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Remove a directory via the Jupyter server contents API.
|
|
163
|
+
*
|
|
164
|
+
* @param dirPath - The absolute path to the directory to remove
|
|
165
|
+
* @param serverRoot - The server root directory
|
|
166
|
+
* @returns Promise resolving to success status and optional error message
|
|
167
|
+
*/
|
|
168
|
+
async function removeDirectory(dirPath, serverRoot) {
|
|
169
|
+
const settings = ServerConnection.makeSettings();
|
|
170
|
+
// Determine relative path for the contents API
|
|
171
|
+
let relativePath = null;
|
|
172
|
+
// If path doesn't start with '/', it's already relative - use it directly
|
|
173
|
+
if (!dirPath.startsWith('/')) {
|
|
174
|
+
relativePath = dirPath;
|
|
175
|
+
console.debug(`Path is already relative: ${relativePath}`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// Convert absolute path to relative path
|
|
179
|
+
relativePath = toRelativePath(dirPath, serverRoot);
|
|
180
|
+
// If standard conversion fails, try alternative approaches
|
|
181
|
+
if (relativePath === null) {
|
|
182
|
+
console.debug(`Path conversion failed: dirPath="${dirPath}", serverRoot="${serverRoot}"`);
|
|
183
|
+
// Fallback: if serverRoot is empty or '~', try using home-relative path
|
|
184
|
+
if (!serverRoot || serverRoot === '~' || serverRoot === '') {
|
|
185
|
+
// Extract path from after '/home/username' or similar
|
|
186
|
+
const homeMatch = dirPath.match(/^\/(?:home|Users)\/[^/]+\/(.+)$/);
|
|
187
|
+
if (homeMatch) {
|
|
188
|
+
relativePath = homeMatch[1];
|
|
189
|
+
console.debug(`Using home-relative fallback path: ${relativePath}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// If still null, the path is truly outside workspace
|
|
193
|
+
if (relativePath === null) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: `Environment path "${dirPath}" is outside the workspace (root: "${serverRoot}") and cannot be removed.`
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const url = URLExt.join(settings.baseUrl, 'api', 'contents', relativePath);
|
|
202
|
+
try {
|
|
203
|
+
const response = await ServerConnection.makeRequest(url, { method: 'DELETE' }, settings);
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
const text = await response.text();
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
error: `Server error: ${response.status} ${text}`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { success: true };
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
console.error('Error removing directory:', error);
|
|
215
|
+
return {
|
|
216
|
+
success: false,
|
|
217
|
+
error: `Network error: ${error}`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Expand tilde in path using the home directory extracted from absolutePath.
|
|
223
|
+
*
|
|
224
|
+
* @param path - Path that may contain ~
|
|
225
|
+
* @param absolutePath - An absolute path to extract home directory from
|
|
226
|
+
* @returns Path with ~ expanded, or original if expansion not possible
|
|
227
|
+
*/
|
|
228
|
+
function expandTilde(path, absolutePath) {
|
|
229
|
+
if (!path.startsWith('~')) {
|
|
230
|
+
return path;
|
|
231
|
+
}
|
|
232
|
+
// Extract home directory from absolute path
|
|
233
|
+
// Matches /home/username or /Users/username
|
|
234
|
+
const match = absolutePath.match(/^(\/(?:home|Users)\/[^/]+)/);
|
|
235
|
+
if (!match) {
|
|
236
|
+
return path;
|
|
237
|
+
}
|
|
238
|
+
const homedir = match[1];
|
|
239
|
+
if (path === '~') {
|
|
240
|
+
return homedir;
|
|
241
|
+
}
|
|
242
|
+
if (path.startsWith('~/')) {
|
|
243
|
+
return homedir + path.slice(1);
|
|
244
|
+
}
|
|
245
|
+
return path;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Convert an absolute filesystem path to a path relative to the server root.
|
|
249
|
+
*
|
|
250
|
+
* @param absolutePath - The absolute filesystem path
|
|
251
|
+
* @param serverRoot - The server's root directory (may contain ~)
|
|
252
|
+
* @returns The relative path for the file browser, or null if outside server root
|
|
253
|
+
*/
|
|
254
|
+
function toRelativePath(absolutePath, serverRoot) {
|
|
255
|
+
// Normalize path - ensure no trailing slashes
|
|
256
|
+
const normalizedPath = absolutePath.replace(/\/+$/, '');
|
|
257
|
+
// Expand tilde in serverRoot and normalize
|
|
258
|
+
const normalizedRoot = expandTilde(serverRoot, absolutePath).replace(/\/+$/, '');
|
|
259
|
+
// Check if path is the server root
|
|
260
|
+
if (normalizedPath === normalizedRoot) {
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
// Check if path is inside the server root
|
|
264
|
+
const rootPrefix = normalizedRoot + '/';
|
|
265
|
+
if (normalizedPath.startsWith(rootPrefix)) {
|
|
266
|
+
return normalizedPath.slice(rootPrefix.length);
|
|
267
|
+
}
|
|
268
|
+
// Path is outside the server root
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Extract kernel display name from a launcher card element.
|
|
273
|
+
*
|
|
274
|
+
* @param element - The clicked element or its parent launcher card
|
|
275
|
+
* @returns The kernel display name or null if not found
|
|
276
|
+
*/
|
|
277
|
+
function extractKernelNameFromCard(element) {
|
|
278
|
+
// Find the launcher card (might be the element itself or a parent)
|
|
279
|
+
const card = element.closest('.jp-LauncherCard');
|
|
280
|
+
if (!card) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
// Find the label element within the card
|
|
284
|
+
const label = card.querySelector('.jp-LauncherCard-label');
|
|
285
|
+
if (label && label.textContent) {
|
|
286
|
+
return label.textContent.trim();
|
|
287
|
+
}
|
|
288
|
+
// Fallback: try the title attribute
|
|
289
|
+
if (card.title) {
|
|
290
|
+
return card.title;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Setup event listener to capture right-clicked kernel name.
|
|
296
|
+
*/
|
|
297
|
+
function setupContextMenuCapture() {
|
|
298
|
+
document.addEventListener('contextmenu', (event) => {
|
|
299
|
+
const target = event.target;
|
|
300
|
+
if (target) {
|
|
301
|
+
const card = target.closest('.jp-LauncherCard');
|
|
302
|
+
if (card) {
|
|
303
|
+
lastClickedKernelName = extractKernelNameFromCard(target);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}, true); // Use capture phase to get event before context menu
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Initialization data for the jupyterlab_nb_venv_kernels_ui_extension extension.
|
|
310
|
+
*/
|
|
311
|
+
const plugin = {
|
|
312
|
+
id: 'jupyterlab_nb_venv_kernels_ui_extension:plugin',
|
|
313
|
+
description: "Right-click kernel launcher cards to navigate file browser to kernel's location or open terminal there",
|
|
314
|
+
autoStart: true,
|
|
315
|
+
requires: [IDefaultFileBrowser],
|
|
316
|
+
optional: [ILauncher, ITerminalTracker],
|
|
317
|
+
activate: (app, fileBrowser, launcher, terminalTracker) => {
|
|
318
|
+
console.log('JupyterLab extension jupyterlab_nb_venv_kernels_ui_extension is activated!');
|
|
319
|
+
const { commands } = app;
|
|
320
|
+
// Get the server root directory from PageConfig
|
|
321
|
+
const serverRoot = PageConfig.getOption('serverRoot');
|
|
322
|
+
// Setup event listener to capture kernel name on right-click
|
|
323
|
+
setupContextMenuCapture();
|
|
324
|
+
// Check if nb_venv_kernels is available (for unregister feature)
|
|
325
|
+
checkNbVenvKernelsAvailable().then(available => {
|
|
326
|
+
nbVenvKernelsAvailable = available;
|
|
327
|
+
if (available) {
|
|
328
|
+
console.log('nb_venv_kernels extension detected - unregister feature enabled');
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
// Add the "Show in File Browser" command
|
|
332
|
+
commands.addCommand(SHOW_IN_BROWSER_CMD, {
|
|
333
|
+
label: 'Show in File Browser',
|
|
334
|
+
caption: "Navigate file browser to kernel's directory",
|
|
335
|
+
isEnabled: () => lastClickedKernelName !== null,
|
|
336
|
+
execute: async () => {
|
|
337
|
+
if (!lastClickedKernelName) {
|
|
338
|
+
await showErrorMessage('No Kernel Selected', 'Could not determine which kernel was selected.');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const kernelInfo = await fetchKernelPath(lastClickedKernelName);
|
|
342
|
+
if (!kernelInfo) {
|
|
343
|
+
await showErrorMessage('Kernel Not Found', `Could not find path information for kernel "${lastClickedKernelName}".`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Prefer env_path (conda environment) if available, otherwise use resource_dir
|
|
347
|
+
const targetPath = kernelInfo.env_path || kernelInfo.resource_dir;
|
|
348
|
+
// Convert to relative path for file browser
|
|
349
|
+
const relativePath = toRelativePath(targetPath, serverRoot);
|
|
350
|
+
// If outside workspace, fallback to workspace root
|
|
351
|
+
const navigatePath = relativePath === null ? '' : relativePath;
|
|
352
|
+
try {
|
|
353
|
+
const absolutePath = navigatePath === '' ? '/' : '/' + navigatePath;
|
|
354
|
+
await fileBrowser.model.cd(absolutePath);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.error('Failed to navigate file browser:', error);
|
|
358
|
+
await showErrorMessage('Navigation Error', `Failed to navigate to: ${targetPath}\nError: ${error}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
// Add the "Open Terminal at location" command
|
|
363
|
+
commands.addCommand(OPEN_TERMINAL_CMD, {
|
|
364
|
+
label: 'Open Terminal at Location',
|
|
365
|
+
caption: "Open a terminal at the kernel's directory",
|
|
366
|
+
isEnabled: () => lastClickedKernelName !== null && terminalTracker !== null,
|
|
367
|
+
execute: async () => {
|
|
368
|
+
if (!lastClickedKernelName) {
|
|
369
|
+
await showErrorMessage('No Kernel Selected', 'Could not determine which kernel was selected.');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const kernelInfo = await fetchKernelPath(lastClickedKernelName);
|
|
373
|
+
if (!kernelInfo) {
|
|
374
|
+
await showErrorMessage('Kernel Not Found', `Could not find path information for kernel "${lastClickedKernelName}".`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Prefer env_path (conda environment) if available, otherwise use resource_dir
|
|
378
|
+
const targetPath = kernelInfo.env_path || kernelInfo.resource_dir;
|
|
379
|
+
// Convert to relative path for terminal (terminal:create-new requires relative path)
|
|
380
|
+
const relativePath = toRelativePath(targetPath, serverRoot);
|
|
381
|
+
// If outside workspace, use empty string (workspace root)
|
|
382
|
+
const terminalCwd = relativePath === null ? '' : relativePath;
|
|
383
|
+
try {
|
|
384
|
+
// Open a new terminal with relative path
|
|
385
|
+
await commands.execute('terminal:create-new', {
|
|
386
|
+
cwd: terminalCwd
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
console.error('Failed to open terminal:', error);
|
|
391
|
+
await showErrorMessage('Terminal Error', `Failed to open terminal at: ${targetPath}\nError: ${error}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
// Add the "Unregister Kernel" command (shown when nb_venv_kernels is available)
|
|
396
|
+
commands.addCommand(UNREGISTER_KERNEL_CMD, {
|
|
397
|
+
label: 'Unregister Kernel',
|
|
398
|
+
caption: 'Remove this kernel from nb_venv_kernels registry',
|
|
399
|
+
isEnabled: () => lastClickedKernelName !== null && nbVenvKernelsAvailable,
|
|
400
|
+
isVisible: () => nbVenvKernelsAvailable,
|
|
401
|
+
execute: async () => {
|
|
402
|
+
if (!lastClickedKernelName) {
|
|
403
|
+
await showErrorMessage('No Kernel Selected', 'Could not determine which kernel was selected.');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// Find the venv environment for this kernel
|
|
407
|
+
const env = await findVenvEnvironment(lastClickedKernelName);
|
|
408
|
+
if (!env) {
|
|
409
|
+
await showErrorMessage('Cannot Unregister', `"${lastClickedKernelName}" is not managed by nb_venv_kernels.`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// Unregister the kernel
|
|
413
|
+
const result = await unregisterVenvKernel(env.path);
|
|
414
|
+
if (result.success) {
|
|
415
|
+
console.log(`Kernel unregistered: ${env.path}`);
|
|
416
|
+
// Refresh kernel list so changes are visible immediately
|
|
417
|
+
await commands.execute(NB_VENV_KERNELS_REFRESH_CMD).catch(() => {
|
|
418
|
+
// Ignore if refresh command not available
|
|
419
|
+
});
|
|
420
|
+
const bodyWidget = new Widget();
|
|
421
|
+
const p1 = document.createElement('p');
|
|
422
|
+
p1.textContent = `Successfully unregistered "${env.name}" (${env.path}).`;
|
|
423
|
+
const p2 = document.createElement('p');
|
|
424
|
+
p2.textContent = `To re-register, run: nb_venv_kernels register ${env.path}`;
|
|
425
|
+
bodyWidget.node.appendChild(p1);
|
|
426
|
+
bodyWidget.node.appendChild(p2);
|
|
427
|
+
await showDialog({
|
|
428
|
+
title: 'Kernel Unregistered',
|
|
429
|
+
body: bodyWidget,
|
|
430
|
+
buttons: [Dialog.okButton()]
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
await showErrorMessage('Unregister Failed', `Failed to unregister kernel: ${result.error}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
// Add the "Remove Environment" command (dangerous - physically removes .venv folder)
|
|
439
|
+
commands.addCommand(REMOVE_ENVIRONMENT_CMD, {
|
|
440
|
+
label: 'Remove Environment (dangerous)',
|
|
441
|
+
caption: 'Physically remove the .venv folder containing this environment',
|
|
442
|
+
isEnabled: () => lastClickedKernelName !== null && nbVenvKernelsAvailable,
|
|
443
|
+
isVisible: () => nbVenvKernelsAvailable,
|
|
444
|
+
execute: async () => {
|
|
445
|
+
if (!lastClickedKernelName) {
|
|
446
|
+
await showErrorMessage('No Kernel Selected', 'Could not determine which kernel was selected.');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// Find the venv environment for this kernel
|
|
450
|
+
const env = await findVenvEnvironment(lastClickedKernelName);
|
|
451
|
+
if (!env) {
|
|
452
|
+
await showErrorMessage('Cannot Remove', `"${lastClickedKernelName}" is not managed by nb_venv_kernels.`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Check if this is a local .venv environment
|
|
456
|
+
if (!isLocalVenvEnvironment(env.path)) {
|
|
457
|
+
const notLocalWidget = new Widget();
|
|
458
|
+
const notLocalP1 = document.createElement('p');
|
|
459
|
+
notLocalP1.textContent = `"${env.name}" is not a local .venv environment.`;
|
|
460
|
+
const notLocalP2 = document.createElement('p');
|
|
461
|
+
notLocalP2.textContent = 'Only local environments (with .venv in their path) can be removed.';
|
|
462
|
+
notLocalWidget.node.appendChild(notLocalP1);
|
|
463
|
+
notLocalWidget.node.appendChild(notLocalP2);
|
|
464
|
+
await showDialog({
|
|
465
|
+
title: 'Cannot Remove',
|
|
466
|
+
body: notLocalWidget,
|
|
467
|
+
buttons: [Dialog.okButton()]
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// Show confirmation dialog
|
|
472
|
+
const confirmWidget = new Widget();
|
|
473
|
+
const confirmP1 = document.createElement('p');
|
|
474
|
+
confirmP1.textContent = `Are you sure you want to permanently remove virtual environment "${env.name}"?`;
|
|
475
|
+
const confirmP2 = document.createElement('p');
|
|
476
|
+
confirmP2.textContent = `This will delete: ${env.path}`;
|
|
477
|
+
const confirmP3 = document.createElement('p');
|
|
478
|
+
confirmP3.style.fontWeight = 'bold';
|
|
479
|
+
confirmP3.textContent = 'This action cannot be undone!';
|
|
480
|
+
confirmWidget.node.appendChild(confirmP1);
|
|
481
|
+
confirmWidget.node.appendChild(confirmP2);
|
|
482
|
+
confirmWidget.node.appendChild(confirmP3);
|
|
483
|
+
const result = await showDialog({
|
|
484
|
+
title: 'Remove Environment',
|
|
485
|
+
body: confirmWidget,
|
|
486
|
+
buttons: [
|
|
487
|
+
Dialog.cancelButton(),
|
|
488
|
+
Dialog.warnButton({ label: 'Remove' })
|
|
489
|
+
]
|
|
490
|
+
});
|
|
491
|
+
if (!result.button.accept) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// First unregister the kernel
|
|
495
|
+
const unregisterResult = await unregisterVenvKernel(env.path);
|
|
496
|
+
if (!unregisterResult.success) {
|
|
497
|
+
await showErrorMessage('Remove Failed', `Failed to unregister kernel before removal: ${unregisterResult.error}`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// Then remove the directory
|
|
501
|
+
const removeResult = await removeDirectory(env.path, serverRoot);
|
|
502
|
+
if (removeResult.success) {
|
|
503
|
+
console.log(`Environment removed: ${env.path}`);
|
|
504
|
+
// Refresh kernel list so changes are visible immediately
|
|
505
|
+
await commands.execute(NB_VENV_KERNELS_REFRESH_CMD).catch(() => {
|
|
506
|
+
// Ignore if refresh command not available
|
|
507
|
+
});
|
|
508
|
+
await showErrorMessage('Environment Removed', `Successfully removed "${env.name}" (${env.path}).`);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
await showErrorMessage('Remove Failed', `Kernel was unregistered but failed to remove directory: ${removeResult.error}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
console.log(`Commands registered: ${SHOW_IN_BROWSER_CMD}, ${OPEN_TERMINAL_CMD}, ${UNREGISTER_KERNEL_CMD}, ${REMOVE_ENVIRONMENT_CMD}`);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
export default plugin;
|