Skip to content

Commit c3c2030

Browse files
authored
Add path argument to plugin create command (jupyterlab#124)
* add path/name argument in plugin create command * fix test * add suggestions * fix test and cleanup * add path to skill * remove plugin suffix
1 parent 6161711 commit c3c2030

3 files changed

Lines changed: 101 additions & 13 deletions

File tree

_agents/skills/plugin-authoring/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Produce working plugin code that can be loaded with `plugin-playground:load-as-e
3232

3333
1. Prepare a TypeScript file
3434

35-
- If the user does not already have a plugin file open or specified, run `plugin-playground:create-new-plugin`.
35+
- If the user does not already have a plugin file open or specified, run `plugin-playground:create-new-plugin` with a meaningful `path` argument (for example `app.commands.execute('plugin-playground:create-new-plugin', { path: 'status-indicator.ts' })`) instead of relying on untitled defaults.
3636
- Start from the generated TypeScript scaffold and adapt it.
3737
- Focus on TypeScript/TSX plugin code. Do not scaffold Python projects (`pyproject.toml`, Python package layout) unless explicitly requested.
3838

src/index.ts

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
} from '@jupyterlab/ui-components';
3434

3535
import { IDocumentManager } from '@jupyterlab/docmanager';
36+
import { PathExt } from '@jupyterlab/coreutils';
3637

3738
import { Contents } from '@jupyterlab/services';
3839
import { ICompletionProviderManager } from '@jupyterlab/completer';
@@ -204,6 +205,7 @@ const LIST_QUERY_ARGS_SCHEMA = {
204205
}
205206
}
206207
};
208+
207209
const EXPORT_AS_EXTENSION_ARGS_SCHEMA = {
208210
type: 'object',
209211
additionalProperties: false,
@@ -215,6 +217,18 @@ const EXPORT_AS_EXTENSION_ARGS_SCHEMA = {
215217
}
216218
}
217219
};
220+
221+
const CREATE_PLUGIN_ARGS_SCHEMA = {
222+
type: 'object',
223+
additionalProperties: false,
224+
properties: {
225+
path: {
226+
type: 'string',
227+
description:
228+
'Optional file path. Relative paths are resolved from the current working directory; paths starting with "/" are resolved from the workspace root. If no extension is provided, ".ts" is appended.'
229+
}
230+
}
231+
};
218232
const LOAD_ON_SAVE_TOGGLE_TOOLBAR_ITEM = 'plugin-playground-load-on-save';
219233
const LOAD_ON_SAVE_CHECKBOX_LABEL = 'Auto Load on Save';
220234
const LOAD_ON_SAVE_SETTING = 'loadOnSave';
@@ -371,25 +385,66 @@ class PluginPlayground {
371385
app.commands.addCommand(CommandIDs.createNewFile, {
372386
label: 'TypeScript File (Playground)',
373387
caption: 'Create a new TypeScript file',
374-
describedBy: { args: null },
388+
describedBy: { args: CREATE_PLUGIN_ARGS_SCHEMA },
375389
icon: extensionIcon,
376390
execute: async args => {
377-
const model = await app.commands.execute('docmanager:new-untitled', {
378-
path: args['cwd'],
391+
const rawPathArg =
392+
typeof args.path === 'string' ? args.path.trim() : '';
393+
const isRootRelativePath = rawPathArg.startsWith('/');
394+
395+
const model = await app.serviceManager.contents.newUntitled({
379396
type: 'file',
380397
ext: 'ts'
381398
});
382-
const widget: IDocumentWidget<FileEditor> | undefined =
383-
await app.commands.execute('docmanager:open', {
384-
path: model.path,
385-
factory: 'Editor'
386-
});
387-
if (widget) {
388-
widget.content.ready.then(() => {
389-
widget.content.model.sharedModel.setSource(PLUGIN_TEMPLATE);
399+
400+
let openPath = model.path;
401+
const normalizedPathArg = normalizeContentsPath(rawPathArg);
402+
if (normalizedPathArg) {
403+
const baseDirectory = normalizeContentsPath(
404+
PathExt.dirname(model.path)
405+
);
406+
let targetPath = isRootRelativePath
407+
? normalizedPathArg
408+
: normalizeContentsPath(
409+
PathExt.join(baseDirectory, normalizedPathArg)
410+
);
411+
412+
if (!/\.[^/]+$/.test(targetPath)) {
413+
targetPath = `${targetPath}.ts`;
414+
}
415+
416+
if (targetPath !== model.path) {
417+
openPath = (
418+
await app.serviceManager.contents.rename(model.path, targetPath)
419+
).path;
420+
}
421+
}
422+
423+
await app.commands.execute('docmanager:open', {
424+
path: openPath,
425+
factory: 'Editor'
426+
});
427+
428+
const normalizedOpenPath = normalizeContentsPath(openPath);
429+
let widget: IDocumentWidget<FileEditor> | null = null;
430+
editorTracker.forEach(candidate => {
431+
if (
432+
!widget &&
433+
normalizeContentsPath(candidate.context.path) === normalizedOpenPath
434+
) {
435+
widget = candidate;
436+
}
437+
});
438+
if (!widget) {
439+
widget = editorTracker.currentWidget;
440+
}
441+
const activeWidget = widget;
442+
if (activeWidget) {
443+
activeWidget.content.ready.then(() => {
444+
activeWidget.content.model.sharedModel.setSource(PLUGIN_TEMPLATE);
390445
});
391446
}
392-
return widget;
447+
return activeWidget;
393448
}
394449
});
395450

ui-tests/tests/plugin-playground.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,39 @@ test('opens a dummy extension example from the sidebar', async ({ page }) => {
261261
}, expectedReadmePath);
262262
});
263263

264+
test('creates a plugin file with an explicit path argument', async ({
265+
page,
266+
tmpPath
267+
}) => {
268+
const requestedPath = `/${tmpPath}/named-by-command`;
269+
const expectedPath = `${tmpPath}/named-by-command.ts`;
270+
271+
try {
272+
await page.goto();
273+
await page.waitForCondition(() =>
274+
page.evaluate((id: string) => {
275+
return window.jupyterapp.commands.hasCommand(id);
276+
}, CREATE_FILE_COMMAND)
277+
);
278+
279+
const openPath = await page.evaluate(
280+
async ({ id, path }) => {
281+
await window.jupyterapp.commands.execute(id, { path });
282+
const current = window.jupyterapp.shell
283+
.currentWidget as FileEditorWidget | null;
284+
return current?.context?.path ?? null;
285+
},
286+
{
287+
id: CREATE_FILE_COMMAND,
288+
path: requestedPath
289+
}
290+
);
291+
expect(openPath).toBe(expectedPath);
292+
} finally {
293+
await page.unrouteAll({ behavior: 'ignoreErrors' });
294+
}
295+
});
296+
264297
test('lists tokens and searches commands via command APIs', async ({
265298
page
266299
}) => {

0 commit comments

Comments
 (0)