Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add kwargs passthrough to backend driver, implement openWith #233

Merged
merged 5 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@
"mkdirp": "^3.0.1",
"rimraf": "^6.0.1",
"shx": "^0.3.4",
"source-map-loader": "^4.0.2",
"ts-jest": "^29.2.5",
"typescript": "^4.9.5"
},
Expand Down
57 changes: 21 additions & 36 deletions js/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion js/schema/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"enum": ["pyfs", "fsspec"],
"default": "pyfs"
},
"preferred_dir": {
"preferredDir": {
"description": "Directory to be first opened (e.g., myDir/mySubdir)",
"type": "string"
},
Expand All @@ -68,6 +68,11 @@
"description": "Fallback for determining if resource is writeable. Used only if the underlying PyFilesystem does not provide this information (eg S3)",
"type": "boolean",
"default": true
},
"kwargs": {
"description": "Generic kwargs JSON to pass through to resource construction, for any arguments that cannot go in the resource url",
"type": "string",
"default": "{}"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions js/src/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { WidgetTracker, showErrorMessage } from "@jupyterlab/apputils";
import { Drive } from "@jupyterlab/services";
import { ClipboardModel, ContentsModel, IContentRow, Path } from "@tree-finder/base";
Expand Down
121 changes: 118 additions & 3 deletions js/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion */

import { JupyterFrontEnd } from "@jupyterlab/application";
import { Dialog, showDialog } from "@jupyterlab/apputils";
import { DocumentRegistry } from "@jupyterlab/docregistry";
import {
closeIcon,
copyIcon,
Expand All @@ -20,9 +22,12 @@ import {
refreshIcon,
fileIcon,
newFolderIcon,
RankedMenu,
IDisposableMenuItem,
} from "@jupyterlab/ui-components";
import { map } from "@lumino/algorithm";
import { DisposableSet, IDisposable } from "@lumino/disposable";
import { Menu } from "@lumino/widgets";
import { ContextMenu, Menu } from "@lumino/widgets";
import { Content, Path } from "@tree-finder/base";


Expand All @@ -42,6 +47,7 @@ export const commandNames = [
"cut",
"delete",
"open",
"openWith",
"paste",
"refresh",
"rename",
Expand Down Expand Up @@ -116,13 +122,15 @@ async function _commandKeyForSnippet(snippet: Snippet): Promise<string> {
return `jupyterfs:snippet-${snippet.label}-${await _digestString(snippet.label + snippet.caption + snippet.pattern.source + snippet.template)}`;
}

function _openWithKeyForFactory(factory: string): string {
return `jupyterfs:openwith-${factory}`;
}

function _normalizedUrlForSnippet(content: Content<ContentsProxy.IJupyterContentRow>, baseUrl: string): string {
const path = splitPathstrDrive(content.pathstr)[1];
return `${baseUrl}/${path}${content.hasChildren ? "/" : ""}`;
}


/**
* Create commands that will have the same IDs indepent of settings/resources
*
Expand Down Expand Up @@ -189,6 +197,7 @@ export function createStaticCommands(
label: "Open",
isEnabled: () => !!tracker.currentWidget,
}),

app.commands.addCommand(commandIDs.paste, {
execute: args => clipboard.model.pasteSelection(tracker.currentWidget!.treefinder.model!),
icon: pasteIcon,
Expand Down Expand Up @@ -420,6 +429,64 @@ export async function createDynamicCommands(
}));
}

const openWithCommands = [] as IDisposable[];
const openWithMenu = new Menu({ commands: app.commands });
openWithMenu.title.label = "Open With";
openWithMenu.id = "treefinder:open-with";
const { docRegistry } = app;

let items: IDisposableMenuItem[] = [];

function updateOpenWithMenu(contextMenu: ContextMenu) {
const openWith =
(contextMenu.menu.items.find(
item =>
item.type === "submenu" &&
item.submenu?.id === "treefinder:open-with"
)?.submenu as RankedMenu) ?? null;

if (!openWith) {
return; // Bail early if the open with menu is not displayed
}

// clear the current menu items
// items.forEach(item => item.dispose());
items.length = 0;
// Ensure that the menu is empty
openWith.clearItems();

// clear the commands
openWithCommands.forEach(item => item.dispose());
openWithCommands.length = 0;

// get the widget factories that could be used to open all of the items
// in the current filebrowser selection
const widget = tracker.currentWidget!;
const treefinder = widget.treefinder;
const model = treefinder.model!;
const factories = tracker.currentWidget
? intersection<DocumentRegistry.WidgetFactory>(
map(model.selection, i => getFactories(docRegistry, widget, i))
)
: new Set<DocumentRegistry.WidgetFactory>();

// make new menu items from the widget factories
items = [...factories].map(factory => {
const key = _openWithKeyForFactory(factory.label || factory.name);
const label = factory.label || factory.name;
openWithCommands.push(app.commands.addCommand(key, {
execute: args => Promise.all(Array.from(map(model.selection, item => app.commands.execute("docmanager:open", { path: Path.fromarray(item.row.path), ...args })))),
label,
isVisible: () => true,
}));
return openWith.addItem({
args: { factory: factory.name, label: factory.label || factory.name },
command: key,
});
});
}
app.contextMenu.opened.connect(updateOpenWithMenu);

const selector = ".jp-tree-finder-sidebar";
let contextMenuRank = 1;

Expand All @@ -433,7 +500,12 @@ export async function createDynamicCommands(
selector,
rank: contextMenuRank++,
}),

app.contextMenu.addItem({
type: "submenu",
submenu: openWithMenu,
selector,
rank: contextMenuRank++,
}),
app.contextMenu.addItem({
command: commandIDs.copy,
selector,
Expand Down Expand Up @@ -521,3 +593,46 @@ export async function createDynamicCommands(
set.add(d); return set;
}, new DisposableSet());
}


export function getFactories(
docRegistry: DocumentRegistry,
widget: TreeFinderSidebar,
item: Content<ContentsProxy.IJupyterContentRow>,
): DocumentRegistry.WidgetFactory[] {
const path = [widget.url.trimEnd().replace(/\/+$/, ""), item.getPathAtDepth(1).join("/")].join("/");
const factories = docRegistry.preferredWidgetFactories(path);
const notebookFactory = docRegistry.getWidgetFactory("notebook");
if (
notebookFactory &&
item.row.kind === "notebook" &&
factories.indexOf(notebookFactory) === -1
) {
factories.unshift(notebookFactory);
}
return factories;
}

function intersection<T>(iterables: Iterable<Iterable<T>>): Set<T> {
let accumulator: Set<T> | undefined;
for (const current of iterables) {
// Initialize accumulator.
if (accumulator === undefined) {
accumulator = new Set(current);
continue;
}
// Return early if empty.
if (accumulator.size === 0) {
return accumulator;
}
// Keep the intersection of accumulator and current.
const intersection_set = new Set<T>();
for (const value of current) {
if (accumulator.has(value)) {
intersection_set.add(value);
}
}
accumulator = intersection_set;
}
return accumulator ?? new Set();
}
Loading
Loading