Файли
openidea/src/platform/search.ts
T
l0ng-ai 1e00ea5ee2 feat: Third pass aligning core experience with IntelliJ IDEA
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
2026-10-25 21:24:00 +08:00

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";