From d4338d1095cb26315a7440afeeda65e7f3dcf91b Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Mon, 30 Jan 2023 16:52:58 +0000 Subject: [PATCH] Simplify project selection menus The clojure-lsp refactor project in #2020 reworked the way the project picker menus work, adding additional information on a second line indicating which files contributed to a folder being picked as a valid clojure project. This turned out significant hurt the UX experience in workspaces with many clojure projects. This commit removes the additional information from the picker menus, shrinking the list items to a single line. Some other smaller changes included: - Projects are now grouped by their workspace root which should help make it easier to sort through the list visually. - Project folders are now sorted within their groups to make the list more deterministic. - Project folders show the path relative to the workspace root instead of the absolute path. This should help make scanning the list easier by removing unnecessary/duplicate information. Addresses #2041 Closes #2043 --- src/lsp/commands/vscode-commands.ts | 24 +++-- src/project-root.ts | 136 +++++++++++++++++++--------- src/state.ts | 7 +- 3 files changed, 113 insertions(+), 54 deletions(-) diff --git a/src/lsp/commands/vscode-commands.ts b/src/lsp/commands/vscode-commands.ts index 937f89f02..86100b6c4 100644 --- a/src/lsp/commands/vscode-commands.ts +++ b/src/lsp/commands/vscode-commands.ts @@ -44,7 +44,10 @@ const startHandler = async (clients: defs.LSPClientMap, handler: StartHandler, u roots.filter((root) => root.valid_project).map((root) => root.uri) ); - const selected_root = await project_utils.pickProjectRoot(roots, pre_selected); + const selected_root = await project_utils.pickProjectRoot( + roots.map((root) => root.uri), + pre_selected + ); if (!selected_root) { return; } @@ -148,16 +151,23 @@ const manageHandler = async ( ); } - const inactive_roots = await project_utils.findProjectRootsWithReasons().then((roots) => { - return filterOutRootsWithClients(roots, clients).map((root) => { + const inactive_roots = filterOutRootsWithClients(roots, clients) + .sort((a, b) => { + if (a.uri.path < b.uri.path) { + return -1; + } + if (a.uri.path > b.uri.path) { + return 1; + } + return 0; + }) + .map((root) => { return { - label: `$(circle-outline) ${root.uri.path}`, + label: `$(circle-outline) ${root.label}`, value: root.uri.path, - detail: root.reason, active: false, }; }); - }); const active_roots = Array.from(clients.entries()) .filter(([, client]) => client.state !== vscode_lsp.State.Stopped) @@ -168,7 +178,7 @@ const manageHandler = async ( } return { - label: `${icon} ${key}`, + label: `${icon} ${project_utils.getPathRelativeToWorkspace(vscode.Uri.parse(key))}`, value: key, active: true, }; diff --git a/src/project-root.ts b/src/project-root.ts index 1f2530612..9bac68d53 100644 --- a/src/project-root.ts +++ b/src/project-root.ts @@ -1,20 +1,23 @@ import * as config from './config'; import * as vscode from 'vscode'; import * as path from 'path'; +import * as _ from 'lodash'; export type ProjectRoot = { uri: vscode.Uri; + label: string; reason: string; workspace_root?: boolean; valid_project?: boolean; }; -export const rootToUri = (root_or_uri: ProjectRoot | vscode.Uri) => { - if (root_or_uri instanceof vscode.Uri) { - return root_or_uri; +export const getPathRelativeToWorkspace = (uri: vscode.Uri) => { + const root = vscode.workspace.getWorkspaceFolder(uri); + if (!root) { + return uri.path; } - return root_or_uri.uri; + return path.relative(path.dirname(root.uri.path), uri.path); }; type FindRootParams = { @@ -46,6 +49,7 @@ export async function findProjectRootsWithReasons(params?: FindRootParams) { const wsRootPaths = vscode.workspace.workspaceFolders.map((f) => { return { uri: f.uri, + label: path.basename(f.uri.path), reason: 'Workspace Root', workspace_root: true, }; @@ -57,6 +61,7 @@ export async function findProjectRootsWithReasons(params?: FindRootParams) { const dir = uri.with({ path: path.dirname(uri.fsPath) }); return { uri: dir, + label: getPathRelativeToWorkspace(uri), reason: path.basename(uri.path), valid_project: true, }; @@ -131,41 +136,101 @@ export function findFurthestParent(from: vscode.Uri, uris: vscode.Uri[]) { }); } -export async function pickProjectRoot( - uris: Array, - selected?: vscode.Uri -) { +const groupByProject = (uris: vscode.Uri[]) => { + return Object.values( + _.groupBy(uris, (uri) => { + return vscode.workspace.getWorkspaceFolder(uri)?.uri.path; + }) + ); +}; + +const groupsToChoices = (groups: vscode.Uri[][]) => { + return groups.reduce((choices: (vscode.QuickPickItem & { value?: vscode.Uri })[], uris) => { + choices.push({ + kind: vscode.QuickPickItemKind.Separator, + label: 'Workspace Root', + }); + + const items = uris.map((uri) => { + return { + label: getPathRelativeToWorkspace(uri), + value: uri, + }; + }); + + choices.push(...items); + + return choices; + }, []); +}; + +function sortPreSelectedFirst(groups: vscode.Uri[][], selected: vscode.Uri) { + // First sort the groups, bringing the group containing the preselected item to the top. + const sorted_groups = groups.sort((a, b) => { + if (!selected) { + return 0; + } + function groupContainsUri(uris: vscode.Uri[]) { + return !!uris.find((uri) => uri.path === selected.path); + } + if (groupContainsUri(a)) { + return -1; + } + if (groupContainsUri(b)) { + return 1; + } + return 0; + }); + + // Next sort the items within each group, first attempting to bring the preselected item to the top and then + // falling back to sorting by path length + return sorted_groups.map((group) => { + const [root, ...remaining] = group; + if (root.path === selected.path) { + return group; + } + + const sorted = remaining.sort((a, b) => { + // Try bring the pre-selected entry to the top + if (a.path === selected.path) { + return -1; + } + if (b.path === selected.path) { + return 1; + } + + // Fall back to sorting by length + if (a.path < b.path) { + return -1; + } + if (a.path > b.path) { + return 1; + } + + return 0; + }); + + return [root, ...sorted]; + }); +} + +export async function pickProjectRoot(uris: vscode.Uri[], selected?: vscode.Uri) { if (uris.length === 0) { return; } if (uris.length === 1) { - return rootToUri(uris[0]); + return uris[0]; } - const sorted = sortPreSelectedFirst(uris, selected); - - const project_root_options = sorted.map((root_or_uri) => { - let uri; - let reason; - if (root_or_uri instanceof vscode.Uri) { - uri = root_or_uri; - } else { - uri = root_or_uri.uri; - reason = root_or_uri.reason; - } - return { - label: uri.path, - detail: reason, - picked: uri.path === selected?.path, - value: uri, - }; - }); + const grouped = groupByProject(uris); + const sorted = sortPreSelectedFirst(grouped, selected); + const choices = groupsToChoices(sorted); const picker = vscode.window.createQuickPick(); - picker.items = project_root_options; + picker.items = choices; picker.title = 'Project Selection'; picker.placeholder = 'Pick the Clojure project you want to use as the root'; - picker.activeItems = project_root_options.filter((root) => root.label === selected?.path); + picker.activeItems = choices.filter((root) => root.value?.path === selected?.path); picker.show(); const selected_root = await new Promise((resolve) => { @@ -179,16 +244,3 @@ export async function pickProjectRoot( return selected_root?.value; } - -function sortPreSelectedFirst(uris: (ProjectRoot | vscode.Uri)[], selected: vscode.Uri) { - return [...uris].sort((a, b) => { - if (!selected) { - return 0; - } - return rootToUri(a).fsPath === selected.fsPath - ? -1 - : rootToUri(b).fsPath === selected.fsPath - ? 1 - : 0; - }); -} diff --git a/src/state.ts b/src/state.ts index fb0c0f414..62077c18d 100644 --- a/src/state.ts +++ b/src/state.ts @@ -161,13 +161,10 @@ function getProjectWsFolder(): vscode.WorkspaceFolder | undefined { * Figures out the current clojure project root, and stores it in Calva state */ export async function initProjectDir() { - const candidatePaths = await projectRoot.findProjectRootsWithReasons(); + const candidatePaths = await projectRoot.findProjectRoots(); const active_uri = vscode.window.activeTextEditor?.document.uri; const closestRootPath: vscode.Uri = active_uri - ? projectRoot.findClosestParent( - active_uri, - candidatePaths.map((path) => path.uri) - ) + ? projectRoot.findClosestParent(active_uri, candidatePaths) : undefined; const projectRootPath: vscode.Uri = await projectRoot.pickProjectRoot( candidatePaths,