From 13e368a8c53569720deb9a5ae5347202c22c136a Mon Sep 17 00:00:00 2001 From: thech0sen0ne Date: Fri, 18 Sep 2026 19:45:34 -0400 Subject: [PATCH] feat: drag and drop workspaces into or out of workspace groups - Implement TreeDragAndDropController in WorkspacesProvider - Drop onto a workspace group (or a project inside one) assigns the dragged workspaces to that group - Drop onto a project outside all groups or empty tree space removes them from their group - Add batch addWorkspaces/removeWorkspaces helpers to WorkspaceGroupsState - Fix stale group reference in addWorkspace (lookup by id) - Bump engine to ^1.66.0 (required for the DnD API) and declare the missing types via a local module augmentation to keep @types/vscode at 1.56.0 --- .eslintignore | 3 +- CHANGELOG.md | 6 +++ README.md | 1 + package-lock.json | 9 +++-- package.json | 2 +- src/@types/vscode.d.ts | 30 ++++++++++++++ src/@types/workspaces.ts | 1 + src/commands/workspaces.ts | 2 + src/sidebar/WorkspacesProvider.ts | 65 ++++++++++++++++++++++++++++-- src/states/WorkspaceGroupsState.ts | 51 +++++++++++++++++++++-- 10 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 src/@types/vscode.d.ts diff --git a/.eslintignore b/.eslintignore index 61c7eb9..bb529f0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,4 +4,5 @@ out/ plugins/ tasks/ test/ -gulpfile.js \ No newline at end of file +gulpfile.js +src/@types/vscode.d.ts \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 20b089b..1c05629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to the "Projects" extension will be documented in this file. +## [Unreleased] + +### Added + +- Added drag and drop to the workspaces view. Drop one or more workspaces into a workspace group to assign them, or drop them on a workspace outside of all groups to remove them from their current group. + ## [2.0.0] - 2024-09-30 ### Added diff --git a/README.md b/README.md index a89878e..f98da12 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,7 @@ If the key bindings don't work, please check `Preferences -> Keyboard Shortcuts` ### Workspaces * `Click` - Open a workspace. If `l13Projects.openInNewWindow` is true the workspace opens in a new window. +* `Drag and Drop` - Drag one or more selected workspaces into a workspace group to add them, or drop them on a workspace outside of all groups to remove them from their current group. Dropping a workspace on another workspace inside a group adds it to the same group. * Tooltip shows path of the workspace. ### Workspaces Context Icons diff --git a/package-lock.json b/package-lock.json index cc4fc4d..35af1f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "l13-projects", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "l13-projects", - "version": "1.0.0", + "version": "2.0.0", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "jsonc-parser": "^3.0.0" @@ -33,7 +33,7 @@ "vscode-test": "^1.0.2" }, "engines": { - "vscode": "^1.56.0" + "vscode": "^1.66.0" } }, "node_modules/@babel/code-frame": { @@ -311,7 +311,8 @@ "version": "1.56.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.56.0.tgz", "integrity": "sha512-Q5VmQxOx+L1Y6lIJiGcJzwcyV3pQo/eiW8P+7sNLhFI16tJCwtua2DLjHRcpjbCLNVYpQM73kzfFo1Z0HyP9eQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "4.22.1", diff --git a/package.json b/package.json index 1832f13..0d13f9b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.56.0" + "vscode": "^1.66.0" }, "categories": [ "Other" diff --git a/src/@types/vscode.d.ts b/src/@types/vscode.d.ts new file mode 100644 index 0000000..850429c --- /dev/null +++ b/src/@types/vscode.d.ts @@ -0,0 +1,30 @@ +// Type augmentation for the VS Code tree drag-and-drop API introduced in 1.66.0. +// The project keeps @types/vscode at 1.56.0 to avoid breaking changes in TreeItem.iconPath, +// so the missing drag-and-drop types are declared here. + +declare module 'vscode' { + + export class DataTransferItem { + constructor(value: string | object); + public asString(): Thenable; + public asFile(): DataTransferFile | undefined; + } + + export class DataTransfer { + public get(mimeType: string): DataTransferItem | undefined; + public set(mimeType: string, value: DataTransferItem): void; + public forEach(callbackfn: (item: DataTransferItem, mimeType: string) => void): void; + } + + export interface TreeDragAndDropController { + readonly dragMimeTypes: readonly string[]; + readonly dropMimeTypes: readonly string[]; + handleDrag?(source: T[], dataTransfer: DataTransfer, token: CancellationToken): Thenable | void; + handleDrop?(target: T | undefined, dataTransfer: DataTransfer, token: CancellationToken): Thenable | void; + } + + export interface TreeViewOptions { + dragAndDropController?: TreeDragAndDropController; + } + +} diff --git a/src/@types/workspaces.ts b/src/@types/workspaces.ts index 991e4da..a2c2d00 100644 --- a/src/@types/workspaces.ts +++ b/src/@types/workspaces.ts @@ -146,6 +146,7 @@ export type WorkspacesStates = { tags: Tag[], workspaces: Project[], workspaceGroups: WorkspaceGroup[], + workspaceGroupsState: WorkspaceGroupsState, }; export type RefreshWorkspacesStates = { diff --git a/src/commands/workspaces.ts b/src/commands/workspaces.ts index efc850a..4acb5c5 100644 --- a/src/commands/workspaces.ts +++ b/src/commands/workspaces.ts @@ -82,6 +82,7 @@ export function activate (context: vscode.ExtensionContext) { tags: tagsState.get(), workspaces: workspacesState.cache, workspaceGroups: workspaceGroupsState.get(), + workspaceGroupsState, }); const tagSorter = new TagSorter(workspacesProvider, workspaceGroupsState); @@ -102,6 +103,7 @@ export function activate (context: vscode.ExtensionContext) { const treeView = vscode.window.createTreeView('l13ProjectsWorkspaces', { showCollapseAll: true, treeDataProvider: workspacesProvider, + dragAndDropController: workspacesProvider, }); // Tree View diff --git a/src/sidebar/WorkspacesProvider.ts b/src/sidebar/WorkspacesProvider.ts index d86367d..1d18722 100644 --- a/src/sidebar/WorkspacesProvider.ts +++ b/src/sidebar/WorkspacesProvider.ts @@ -25,8 +25,11 @@ import * as workspaces from '../common/workspaces'; import { ProjectsState } from '../states/ProjectsState'; import { HotkeySlotsState } from '../states/HotkeySlotsState'; +import type { WorkspaceGroupsState } from '../states/WorkspaceGroupsState'; +import { WorkspaceGroupTreeItem } from './trees/groups/WorkspaceGroupTreeItem'; import { ColorPickerTreeItem } from './trees/items/ColorPickerTreeItem'; +import { CurrentWorkspaceTreeItem } from './trees/items/CurrentWorkspaceTreeItem'; import { WorkspaceTreeItem } from './trees/items/WorkspaceTreeItem'; import { UnknownProjectTreeItem } from './trees/items/UnknownProjectTreeItem'; @@ -40,7 +43,12 @@ import { UnknownProjectTreeItem } from './trees/items/UnknownProjectTreeItem'; // Exports ____________________________________________________________________ -export class WorkspacesProvider implements vscode.TreeDataProvider { +const DRAG_MIME_TYPE = 'application/vnd.code.tree.l13ProjectsWorkspaces'; + +export class WorkspacesProvider implements vscode.TreeDataProvider, vscode.TreeDragAndDropController { + + public dragMimeTypes = [DRAG_MIME_TYPE]; + public dropMimeTypes = [DRAG_MIME_TYPE]; public static current: WorkspacesProvider; @@ -84,15 +92,18 @@ export class WorkspacesProvider implements vscode.TreeDataProvider workspaceGroup.paths.includes(target.project.path)); + // A project outside of any group means "remove from all groups". + if (!group) this.workspaceGroupsState.removeWorkspaces(paths); + return; + } else if (target) { + // Any other item (tags, root or type groups) is not a valid drop target. + return; + } + + const projects = paths + .map((path) => this.workspaces.find((project) => project.path === path)) + .filter((project) => project); + + if (group) this.workspaceGroupsState.addWorkspaces(projects, group); + else this.workspaceGroupsState.removeWorkspaces(paths); + + } + } -// Functions __________________________________________________________________ - diff --git a/src/states/WorkspaceGroupsState.ts b/src/states/WorkspaceGroupsState.ts index 35c54eb..d8be85d 100644 --- a/src/states/WorkspaceGroupsState.ts +++ b/src/states/WorkspaceGroupsState.ts @@ -131,14 +131,57 @@ export class WorkspaceGroupsState { public addWorkspace (workspace: Project, workspaceGroup: WorkspaceGroup) { const workspaceGroups = this.get(); + const targetGroup = workspaceGroups.find((group) => group.id === workspaceGroup.id); - if (!workspaceGroup.paths.includes(workspace.path)) { + if (targetGroup && !targetGroup.paths.includes(workspace.path)) { const previousWorkspaceGroup = workspaceGroups.find((group) => remove(group.paths, workspace.path)); - workspaceGroup.paths.push(workspace.path); - workspaceGroup.paths.sort(); + targetGroup.paths.push(workspace.path); + targetGroup.paths.sort(); this.save(workspaceGroups); if (previousWorkspaceGroup) this._onDidUpdateWorkspaceGroup.fire(previousWorkspaceGroup); - this._onDidUpdateWorkspaceGroup.fire(workspaceGroup); + this._onDidUpdateWorkspaceGroup.fire(targetGroup); + this._onDidChangeWorkspaceGroups.fire(workspaceGroups); + } + + } + + public addWorkspaces (workspaces: Project[], workspaceGroup: WorkspaceGroup) { + + const workspaceGroups = this.get(); + const targetGroup = workspaceGroups.find((group) => group.id === workspaceGroup.id); + if (!targetGroup) return; + + let hasChanged = false; + + for (const workspace of workspaces) { + if (targetGroup.paths.includes(workspace.path)) continue; + workspaceGroups.find((group) => remove(group.paths, workspace.path)); + targetGroup.paths.push(workspace.path); + hasChanged = true; + } + + if (hasChanged) { + targetGroup.paths.sort(); + this.save(workspaceGroups); + this._onDidUpdateWorkspaceGroup.fire(targetGroup); + this._onDidChangeWorkspaceGroups.fire(workspaceGroups); + } + + } + + public removeWorkspaces (paths: string[]) { + + const workspaceGroups = this.get(); + let hasChanged = false; + + for (const workspaceGroup of workspaceGroups) { + for (const path of paths) { + if (remove(workspaceGroup.paths, path)) hasChanged = true; + } + } + + if (hasChanged) { + this.save(workspaceGroups); this._onDidChangeWorkspaceGroups.fire(workspaceGroups); }