дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
Search Everywhere and Recent Files lists now support multi-selection: Shift+Up/Down and Shift+click extend a range from the anchor, Cmd/Ctrl+click toggles a row, and Enter opens every selected file, symbol or text result in list order, leaving focus on the lead row. Actions in a mixed selection are skipped; a single selection behaves as before. Selection resets when the query or tab changes. The row-selection model used by Find in Files moves to src/ui so the shared List can opt into modifier selection. Fixes #647
75 рядки
2.4 KiB
TypeScript
75 рядки
2.4 KiB
TypeScript
import { hasPrimaryModifier } from "@/platform/keymap";
|
|
|
|
/**
|
|
* Multi-selection of list rows: click / ↑↓ select one row,
|
|
* ⇧ extends from the anchor, ⌘/Ctrl toggles a row.
|
|
*/
|
|
|
|
export interface RowSelection {
|
|
/** Active row (keyboard focus, preview). */
|
|
lead: number;
|
|
/** Start of ⇧ ranges. */
|
|
anchor: number;
|
|
/** Keys of the selected rows; `null` when only the lead is selected. */
|
|
keys: ReadonlySet<string> | null;
|
|
}
|
|
|
|
export const SINGLE_FIRST: RowSelection = { lead: 0, anchor: 0, keys: null };
|
|
|
|
export function selectRow<T extends { key: string }>(
|
|
rows: readonly T[],
|
|
sel: RowSelection,
|
|
index: number,
|
|
mode: "single" | "range" | "toggle" = "single",
|
|
isSection?: (row: T) => boolean,
|
|
): RowSelection {
|
|
if (index < 0 || index >= rows.length || isSection?.(rows[index]!)) return sel;
|
|
if (mode === "range") {
|
|
const anchor = Math.min(Math.max(0, sel.anchor), rows.length - 1);
|
|
const [a, b] = anchor <= index ? [anchor, index] : [index, anchor];
|
|
return {
|
|
lead: index,
|
|
anchor,
|
|
keys: new Set(
|
|
rows
|
|
.slice(a, b + 1)
|
|
.filter((r) => !isSection?.(r))
|
|
.map((r) => r.key),
|
|
),
|
|
};
|
|
}
|
|
if (mode === "toggle") {
|
|
const keys = new Set(sel.keys ?? (rows[sel.lead] ? [rows[sel.lead]!.key] : []));
|
|
const key = rows[index]!.key;
|
|
if (!keys.has(key)) return { lead: index, anchor: index, keys: keys.add(key) };
|
|
keys.delete(key);
|
|
const rest = rows.findLastIndex((r) => keys.has(r.key));
|
|
if (rest < 0) return { lead: index, anchor: index, keys: null };
|
|
return { lead: rest, anchor: rest, keys };
|
|
}
|
|
return { lead: index, anchor: index, keys: null };
|
|
}
|
|
|
|
/** The selected rows in list order (the lead when nothing else is selected). */
|
|
export function selectedRows<T extends { key: string }>(rows: readonly T[], sel: RowSelection): T[] {
|
|
if (!rows.length || sel.lead < 0) return [];
|
|
const lead = Math.min(sel.lead, rows.length - 1);
|
|
if (!sel.keys) return [rows[lead]!];
|
|
const out = rows.filter((r) => sel.keys!.has(r.key));
|
|
return out.length ? out : [rows[lead]!];
|
|
}
|
|
|
|
export function isRowSelected(sel: RowSelection, key: string): boolean {
|
|
return !!sel.keys?.has(key);
|
|
}
|
|
|
|
export function rowSelectionMode(e?: {
|
|
shiftKey: boolean;
|
|
metaKey: boolean;
|
|
ctrlKey: boolean;
|
|
type: string;
|
|
}): "single" | "range" | "toggle" {
|
|
if (e?.shiftKey) return "range";
|
|
return e?.type === "click" && hasPrimaryModifier(e) ? "toggle" : "single";
|
|
}
|