Skip to content

Commit

Permalink
Simplify project selection menus
Browse files Browse the repository at this point in the history
The clojure-lsp refactor project in BetterThanTomorrow#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 BetterThanTomorrow#2041
Closes BetterThanTomorrow#2043
  • Loading branch information
julienvincent committed Jan 30, 2023
1 parent ada725e commit 25ab660
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 54 deletions.
24 changes: 17 additions & 7 deletions src/lsp/commands/vscode-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)
Expand All @@ -168,7 +178,7 @@ const manageHandler = async (
}

return {
label: `${icon} ${key}`,
label: `${icon} ${project_utils.getPathRelativeToWorkspace(vscode.Uri.parse(key))}`,
value: key,
active: true,
};
Expand Down
136 changes: 94 additions & 42 deletions src/project-root.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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,
};
Expand All @@ -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,
};
Expand Down Expand Up @@ -131,41 +136,101 @@ export function findFurthestParent(from: vscode.Uri, uris: vscode.Uri[]) {
});
}

export async function pickProjectRoot(
uris: Array<vscode.Uri | ProjectRoot>,
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<any>((resolve) => {
Expand All @@ -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;
});
}
7 changes: 2 additions & 5 deletions src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 25ab660

Please sign in to comment.