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,