дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
Pasting or dropping OS files into a remote project tree sent a client-local source path to the server, so nothing was copied. Local-source imports are now carried separately from project copies and uploaded over the session SFTP channel: streamed in bounded chunks, created exclusively, cancellable across all selected paths under one task, with partial files removed on failure. Symlinks and Unix file modes are preserved, and special files are skipped and reported. Project-internal paste keeps working when the OS clipboard is briefly unavailable. Native drop positions were assumed to be physical pixels, which shifted the target far from the pointer on Retina displays and with IDE zoom. A shared helper converts them to CSS coordinates for the project tree, the editor and Markdown drop handling, and the agent composer. Composer image drops are now read on the client in remote windows. Verified on macOS against a live SSH host. Windows and Linux were not run: the Windows conversion is unchanged, the Linux one changed. Copy out from a remote tree to the local machine still needs a download workflow.
88 рядки
3.3 KiB
TypeScript
88 рядки
3.3 KiB
TypeScript
/**
|
|
* Files dropped from the OS onto the project (or LightEdit) window open in the
|
|
* editor, one tab each; a dropped folder opens as a project (the "Open project in"
|
|
* policy applies). Regions marked with `OWNS_FILE_DROP` (Project view, prompt box)
|
|
* handle their drops themselves.
|
|
*/
|
|
import { executeAction } from "@/platform/actions";
|
|
import { call, commands, isTauri } from "@/platform/ipc";
|
|
import { useProject } from "@/platform/project";
|
|
import { isRemoteWindow } from "@/platform/remote";
|
|
import { nativeDropPoint } from "@/platform/nativeDrop";
|
|
import { editorService, OWNS_FILE_DROP } from "./contributions";
|
|
|
|
const OWNER_ATTR = Object.keys(OWNS_FILE_DROP)[0]!;
|
|
|
|
type FileDropHandler = (paths: readonly string[], x: number, y: number) => boolean;
|
|
const fileDropHandlers = new Set<FileDropHandler>();
|
|
|
|
export function registerFileDropHandler(handler: FileDropHandler): () => void {
|
|
fileDropHandlers.add(handler);
|
|
return () => {
|
|
fileDropHandlers.delete(handler);
|
|
};
|
|
}
|
|
|
|
export interface FileDropDeps {
|
|
isDir(path: string): Promise<boolean>;
|
|
openFile(path: string, focus: boolean): Promise<void>;
|
|
openProject(path: string): Promise<void>;
|
|
}
|
|
|
|
/** Whether a drop at CSS pixel (x, y) lands on a region that handles OS drops itself. */
|
|
export function dropOwnedAt(x: number, y: number): boolean {
|
|
return !!document.elementFromPoint(x, y)?.closest(`[${OWNER_ATTR}]`);
|
|
}
|
|
|
|
/** Opens the dropped files (the last one focused), then the first dropped folder as a project. */
|
|
export async function openDropped(paths: readonly string[], deps: FileDropDeps): Promise<void> {
|
|
const kinds = await Promise.all(paths.map((p) => deps.isDir(p).catch(() => false)));
|
|
const files = paths.filter((_, i) => !kinds[i]);
|
|
const dir = paths.find((_, i) => kinds[i]);
|
|
for (const [i, path] of files.entries())
|
|
await deps.openFile(path, i === files.length - 1).catch((e) => console.warn(`could not open ${path}`, e));
|
|
if (dir) await deps.openProject(dir);
|
|
}
|
|
|
|
const defaultDeps: FileDropDeps = {
|
|
isDir: (path) => call(commands.fsReadDir(path)).then(() => true),
|
|
openFile: (path, focus) => editorService().openFile(path, { focus }),
|
|
openProject: async (path) => void (await executeAction("file.openProjectPath", path)),
|
|
};
|
|
|
|
/** Handles one drop; null when it is left to another handler (no local project or LightEdit: the Welcome screen). */
|
|
export function handleFileDrop(
|
|
paths: readonly string[],
|
|
x: number,
|
|
y: number,
|
|
deps: FileDropDeps = defaultDeps,
|
|
): Promise<void> | null {
|
|
for (const handler of fileDropHandlers) if (paths.length && handler(paths, x, y)) return Promise.resolve();
|
|
const { project, lightEdit } = useProject.getState();
|
|
if (!paths.length || !(project || lightEdit) || isRemoteWindow() || dropOwnedAt(x, y)) return null;
|
|
return openDropped(paths, deps);
|
|
}
|
|
|
|
export function installFileDrop(): () => void {
|
|
if (!isTauri()) return () => {};
|
|
let unlisten: (() => void) | undefined;
|
|
let disposed = false;
|
|
void import("@tauri-apps/api/webview").then(({ getCurrentWebview }) =>
|
|
getCurrentWebview()
|
|
.onDragDropEvent((event) => {
|
|
const p = event.payload;
|
|
if (p.type !== "drop") return;
|
|
const { x, y } = nativeDropPoint(p.position);
|
|
void handleFileDrop(p.paths, x, y);
|
|
})
|
|
.then((u) => {
|
|
if (disposed) u();
|
|
else unlisten = u;
|
|
}),
|
|
);
|
|
return () => {
|
|
disposed = true;
|
|
unlisten?.();
|
|
};
|
|
}
|