дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02: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
150 рядки
5.3 KiB
TypeScript
150 рядки
5.3 KiB
TypeScript
/**
|
|
* Search pieces shared by Find / Replace in the editor and Find in Files:
|
|
* the search context filter, Preserve Case, the recent-search history and the
|
|
* user's custom (named) scopes, persisted per project.
|
|
*/
|
|
import { useEffect } from "react";
|
|
import { create } from "zustand";
|
|
import type { SearchContext } from "./ipc";
|
|
import { useProject } from "./project";
|
|
|
|
export type { SearchContext };
|
|
|
|
/** IDEA's search context filter: anywhere, only / except comments and string literals. */
|
|
export const SEARCH_CONTEXTS: ReadonlyArray<{ value: SearchContext; label: string }> = [
|
|
{ value: "any", label: "Anywhere" },
|
|
{ value: "inComments", label: "In Comments" },
|
|
{ value: "inStringLiterals", label: "In String Literals" },
|
|
{ value: "exceptComments", label: "Except Comments" },
|
|
{ value: "exceptStringLiterals", label: "Except String Literals" },
|
|
{ value: "exceptCommentsAndStringLiterals", label: "Except Comments and String Literals" },
|
|
];
|
|
|
|
export type LiteralKind = "comment" | "string";
|
|
|
|
/** Whether a match starting in `kind` (`null`: plain code) is kept (as the backend's `SearchContext::accepts`). */
|
|
export function contextAccepts(context: SearchContext, kind: LiteralKind | null): boolean {
|
|
switch (context) {
|
|
case "any":
|
|
return true;
|
|
case "inComments":
|
|
return kind === "comment";
|
|
case "inStringLiterals":
|
|
return kind === "string";
|
|
case "exceptComments":
|
|
return kind !== "comment";
|
|
case "exceptStringLiterals":
|
|
return kind !== "string";
|
|
case "exceptCommentsAndStringLiterals":
|
|
return kind === null;
|
|
}
|
|
}
|
|
|
|
/** IDEA's "Preserve case" (same rules as the backend's `preserve_case`). */
|
|
export function preserveCase(found: string, replacement: string): string {
|
|
const letters = [...found].filter((c) => c.toLowerCase() !== c.toUpperCase());
|
|
const first = letters[0];
|
|
if (!first || !replacement) return replacement;
|
|
const isUpper = (c: string) => c === c.toUpperCase() && c !== c.toLowerCase();
|
|
if (letters.length > 1 && letters.every(isUpper)) return replacement.toUpperCase();
|
|
const [head = "", ...rest] = [...replacement];
|
|
return (isUpper(first) ? head.toUpperCase() : head.toLowerCase()) + rest.join("");
|
|
}
|
|
|
|
function load(key: string): unknown {
|
|
try {
|
|
const raw = globalThis.localStorage?.getItem(key);
|
|
return raw ? JSON.parse(raw) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function save(key: string, value: unknown) {
|
|
try {
|
|
globalThis.localStorage?.setItem(key, JSON.stringify(value));
|
|
} catch {
|
|
/* storage unavailable */
|
|
}
|
|
}
|
|
|
|
const HISTORY_KEYS = { find: "openidea.search.findHistory.v1", replace: "openidea.search.replaceHistory.v1" };
|
|
const MAX_HISTORY = 20;
|
|
|
|
/** Recent search (or replace) strings, newest first; shared by the find bar and Find in Files. */
|
|
export function searchHistory(kind: "find" | "replace"): string[] {
|
|
const v = load(HISTORY_KEYS[kind]);
|
|
return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
|
|
}
|
|
|
|
export function rememberSearch(kind: "find" | "replace", text: string) {
|
|
if (!text.trim()) return;
|
|
save(HISTORY_KEYS[kind], [text, ...searchHistory(kind).filter((x) => x !== text)].slice(0, MAX_HISTORY));
|
|
}
|
|
|
|
/**
|
|
* A user-defined scope: a name and an IDEA-style pattern
|
|
* (`file:src//*.ts&&!file:*test*`), matched by the backend (`SearchScope::Pattern`).
|
|
*/
|
|
export interface CustomScope {
|
|
name: string;
|
|
pattern: string;
|
|
}
|
|
|
|
interface CustomScopesState {
|
|
/** Project the scopes belong to. */
|
|
root: string | null;
|
|
scopes: CustomScope[];
|
|
}
|
|
|
|
export const useCustomScopes = create<CustomScopesState>(() => ({ root: null, scopes: [] }));
|
|
|
|
const scopesKey = (root: string) => `openidea.scopes.v1:${root}`;
|
|
|
|
function validScopes(v: unknown): CustomScope[] {
|
|
if (!Array.isArray(v)) return [];
|
|
return v.flatMap((x) => {
|
|
const s = x as Partial<CustomScope> | null;
|
|
return s && typeof s.name === "string" && typeof s.pattern === "string" && s.name.trim()
|
|
? [{ name: s.name, pattern: s.pattern }]
|
|
: [];
|
|
});
|
|
}
|
|
|
|
/** Custom scopes of the open project (loaded on first use after the project changes). */
|
|
export function customScopes(): CustomScope[] {
|
|
const root = useProject.getState().project?.root ?? null;
|
|
const s = useCustomScopes.getState();
|
|
if (s.root === root) return s.scopes;
|
|
const scopes = root ? validScopes(load(scopesKey(root))) : [];
|
|
useCustomScopes.setState({ root, scopes });
|
|
return scopes;
|
|
}
|
|
|
|
/** Reactive {@link customScopes}. */
|
|
export function useCustomScopeList(): CustomScope[] {
|
|
const root = useProject((p) => p.project?.root ?? null);
|
|
const state = useCustomScopes();
|
|
const stale = state.root !== root;
|
|
useEffect(() => {
|
|
if (stale) customScopes();
|
|
}, [stale]);
|
|
return stale ? NO_SCOPES : state.scopes;
|
|
}
|
|
|
|
const NO_SCOPES: CustomScope[] = [];
|
|
|
|
export function saveCustomScopes(scopes: CustomScope[]) {
|
|
const root = useProject.getState().project?.root ?? null;
|
|
const clean = validScopes(scopes).map((s) => ({ name: s.name.trim(), pattern: s.pattern.trim() }));
|
|
useCustomScopes.setState({ root, scopes: clean });
|
|
if (root) save(scopesKey(root), clean);
|
|
}
|
|
|
|
export function findCustomScope(name: string): CustomScope | null {
|
|
return customScopes().find((s) => s.name === name) ?? null;
|
|
}
|
|
|
|
/** Action that opens the scope editor (registered by the search feature); `arg`: `{ select?: string }`. */
|
|
export const EDIT_SCOPES_ACTION = "search.editScopes";
|