mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
Project & files - Project view modes (Project Files / Open Files / All Changed Files / Scratches); External Libraries for Python, JDK (src.zip), Node, Cargo, Go, Maven/Gradle jars; read-only jar/zip browsing - ⌘N New popup with type search and more file kinds; New Project, Rename Project, Reload All from Disk, Export/Import Settings, Power Save Mode - File type associations, extra content roots and exclude patterns (indexed and watched without reopening the project) Editor & navigation - Find bar: search scope (comments/strings), Preserve Case, Exclude, multiline, history; Find in Files: Module scope, custom pattern scopes, multiline - Search Everywhere filters, Assign Shortcut, settings results; named bookmark lists, file/folder bookmarks; Structure filters and Show Inherited; Recent Locations timestamps and breadcrumbs; Code Vision author - Completion options, editor tab placement/layout, inline completion (LSP 3.18) - Spell checking (Typo inspection, ide-spell) and Inspections settings with custom profiles; Code Style schemes, formatter markers, Do not format Run, VCS, services, database - Typed run configurations (Shell, Program, npm, Gradle, Maven, Docker, Compound), Modify options, Before launch, env dialog, Current File, Emulate terminal (PTY) - Git log graph/view options, Go to Hash/Branch/Tag, date filter keeps the graph; Pull/Merge/Rebase dialogs, Manage Remotes, Worktrees - Generic Services host (Docker connections/registries, run sessions); TODO scope/changes tabs with changelists; Coverage report import - Data sources: wire-compatible vendors, URL import, SSL certs, Options, Schemas and Advanced tabs, color and comment Workbench - Run Anything, Customize Main Toolbar, named tool window layouts, menu completeness, Code Cleanup, macros, "Don't Show Again", editor banners - Chinese translations for the new UI Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
187 lines
6.3 KiB
TypeScript
187 lines
6.3 KiB
TypeScript
/**
|
|
* The action system ("everything is an action"), modelled on IDEA's ActionManager.
|
|
*
|
|
* Menus, keybindings, toolbar buttons, context menus and the Actions tab of
|
|
* Search Everywhere all resolve to the same registry, so a feature registers
|
|
* its behaviour once:
|
|
*
|
|
* registerAction({ id: "editor.formatDocument", title: "Reformat Code",
|
|
* category: "Code", when: "editorFocus", run: () => … });
|
|
*
|
|
* Default keybindings live in keymaps (`keymap.ts`), not on the action.
|
|
*
|
|
* Titles are written in English; `action.<id>`, `action.<id>.description` and
|
|
* `action.category.<category>` catalog keys, else the source-text catalog
|
|
* (`tx`), translate them, and the registry re-localises on locale change. The
|
|
* English title stays searchable.
|
|
*/
|
|
import { create } from "zustand";
|
|
import { evaluateWhen, getContext, type ContextKeys } from "./context";
|
|
import { formatMessage, lookup, tx, useI18n } from "./i18n";
|
|
|
|
export interface ActionContext {
|
|
ctx: ContextKeys;
|
|
/** Optional argument from the invoker (e.g. a menu passing a file path). */
|
|
arg?: unknown;
|
|
}
|
|
|
|
export interface Action {
|
|
/** Dotted, stable id, e.g. `navigate.searchEverywhere`. */
|
|
id: string;
|
|
title: string;
|
|
/** Group shown in Search Everywhere and the keymap editor, e.g. `Navigate`. */
|
|
category?: string;
|
|
description?: string;
|
|
/** Icon name from `ui/icons`. */
|
|
icon?: string;
|
|
/** Enablement; evaluated against context keys. */
|
|
when?: string;
|
|
/**
|
|
* Extra condition for keyboard shortcuts only (menus and Find Action ignore
|
|
* it), e.g. to yield a shared shortcut to an editor action while typing.
|
|
*/
|
|
keyWhen?: string;
|
|
/** Extra search terms for the Actions tab. */
|
|
keywords?: string[];
|
|
/** Hidden from Search Everywhere (internal / parameterised actions). */
|
|
hidden?: boolean;
|
|
/**
|
|
* Marks a toggle action (IDEA's ToggleAction): returns the current state.
|
|
* Search Everywhere shows an ON/OFF switch and keeps the popup open.
|
|
*/
|
|
toggled?: () => boolean;
|
|
run: (c: ActionContext) => void | Promise<void>;
|
|
}
|
|
|
|
interface ActionStore {
|
|
actions: Map<string, Action>;
|
|
}
|
|
|
|
export const useActions = create<ActionStore>(() => ({ actions: new Map() }));
|
|
|
|
/** Registered actions as written (English), by id; the store holds their localised copies. */
|
|
const sources = new Map<string, Action>();
|
|
|
|
function translated(key: string): string | undefined {
|
|
const message = lookup(key);
|
|
return message === undefined ? undefined : formatMessage(message, {}, useI18n.getState().locale);
|
|
}
|
|
|
|
/** Catalog key, else the source-text catalog; `undefined` when untranslated. */
|
|
function localized(key: string, text: string | undefined): string | undefined {
|
|
if (!text) return undefined;
|
|
const v = translated(key) ?? tx(text);
|
|
return v === text ? undefined : v;
|
|
}
|
|
|
|
function localize(action: Action): Action {
|
|
const title = localized(`action.${action.id}`, action.title);
|
|
const description = localized(`action.${action.id}.description`, action.description);
|
|
const category = localized(`action.category.${action.category}`, action.category);
|
|
if (!title && !description && !category) return action;
|
|
return {
|
|
...action,
|
|
title: title ?? action.title,
|
|
description: description ?? action.description,
|
|
category: category ?? action.category,
|
|
keywords: title ? [...(action.keywords ?? []), action.title] : action.keywords,
|
|
};
|
|
}
|
|
|
|
useI18n.subscribe((s, prev) => {
|
|
if ((s.messages === prev.messages && s.texts === prev.texts) || !sources.size) return;
|
|
useActions.setState({ actions: new Map([...sources].map(([id, a]) => [id, localize(a)])) });
|
|
});
|
|
|
|
/** Registers an action; returns a disposer. Re-registering an id replaces it. */
|
|
export function registerAction(action: Action): () => void {
|
|
sources.set(action.id, action);
|
|
useActions.setState((s) => ({ actions: new Map(s.actions).set(action.id, localize(action)) }));
|
|
return () => {
|
|
if (sources.get(action.id) !== action) return;
|
|
sources.delete(action.id);
|
|
useActions.setState((s) => {
|
|
const actions = new Map(s.actions);
|
|
actions.delete(action.id);
|
|
return { actions };
|
|
});
|
|
};
|
|
}
|
|
|
|
export function registerActions(actions: Action[]): () => void {
|
|
const disposers = actions.map(registerAction);
|
|
return () => disposers.forEach((d) => d());
|
|
}
|
|
|
|
export function getAction(id: string): Action | undefined {
|
|
return useActions.getState().actions.get(id);
|
|
}
|
|
|
|
export function allActions(): Action[] {
|
|
return [...useActions.getState().actions.values()];
|
|
}
|
|
|
|
export function isActionEnabled(action: Action, ctx: ContextKeys = getContext()): boolean {
|
|
return evaluateWhen(action.when, ctx);
|
|
}
|
|
|
|
/** Whether a keyboard shortcut may run `action` (`when` and `keyWhen`). */
|
|
export function isActionKeyEnabled(action: Action, ctx: ContextKeys = getContext()): boolean {
|
|
return isActionEnabled(action, ctx) && evaluateWhen(action.keyWhen, ctx);
|
|
}
|
|
|
|
/** Current state of a toggle action, or `undefined` for plain actions. */
|
|
export function actionToggled(action: Action | undefined): boolean | undefined {
|
|
if (!action?.toggled) return undefined;
|
|
try {
|
|
return action.toggled();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
type ActionListener = (id: string, arg: unknown) => void;
|
|
const listeners = new Set<ActionListener>();
|
|
/** Actions running synchronously inside another action's `run`. */
|
|
let nesting = 0;
|
|
|
|
/**
|
|
* Called before each action the user runs (keyboard, menus, Find Action), not
|
|
* for the actions an action runs itself. Macro recording listens here.
|
|
*/
|
|
export function onActionExecuted(listener: ActionListener): () => void {
|
|
listeners.add(listener);
|
|
return () => void listeners.delete(listener);
|
|
}
|
|
|
|
/** Runs an action if registered and enabled. Returns whether it ran. */
|
|
export async function executeAction(id: string, arg?: unknown): Promise<boolean> {
|
|
const action = getAction(id);
|
|
if (!action) {
|
|
console.warn(`unknown action: ${id}`);
|
|
return false;
|
|
}
|
|
const ctx = getContext();
|
|
if (!isActionEnabled(action, ctx)) return false;
|
|
if (nesting === 0)
|
|
for (const l of listeners)
|
|
try {
|
|
l(id, arg);
|
|
} catch (e) {
|
|
console.error("action listener failed", e);
|
|
}
|
|
try {
|
|
let pending: void | Promise<void>;
|
|
nesting++;
|
|
try {
|
|
pending = action.run({ ctx, arg });
|
|
} finally {
|
|
nesting--;
|
|
}
|
|
await pending;
|
|
} catch (e) {
|
|
console.error(`action ${id} failed`, e);
|
|
}
|
|
return true;
|
|
}
|