mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
feat: Syntax-colour Find in Files and Show Usages result lines (#143)
Result lines in the Find in Files popup and tool window and in the Show Usages popup and Usages tool window are coloured like the editor, using the whole file as context, so a line inside a block comment or a multi-line string gets the right colour. The colouring lives in editor/lineTokens and all four lists share it; each file is parsed once and cached, and the parse runs in 8 ms time slices so large files don't block the UI thread. Lines that moved since the search, and files over 500k characters, are coloured on their own. Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA Co-authored-by: jaw52 <[email protected]>
This commit is contained in:
15 files changed
+466
-44
No files matched your search
@@ -92,6 +92,40 @@
|
||||
color: var(--fg-search-match, inherit);
|
||||
border-radius: 2px;
|
||||
}
|
||||
/* Lezer classHighlighter tokens of usage lines (same colours as Find in Files results). */
|
||||
.preview :global(.tok-keyword),
|
||||
.preview :global(.tok-modifier),
|
||||
.preview :global(.tok-controlKeyword),
|
||||
.preview :global(.tok-operatorKeyword) {
|
||||
color: var(--syn-keyword);
|
||||
}
|
||||
.preview :global(.tok-string),
|
||||
.preview :global(.tok-string2) {
|
||||
color: var(--syn-string);
|
||||
}
|
||||
.preview :global(.tok-number),
|
||||
.preview :global(.tok-bool),
|
||||
.preview :global(.tok-atom) {
|
||||
color: var(--syn-number);
|
||||
}
|
||||
.preview :global(.tok-typeName),
|
||||
.preview :global(.tok-className),
|
||||
.preview :global(.tok-namespace) {
|
||||
color: var(--syn-type);
|
||||
}
|
||||
.preview :global(.tok-comment) {
|
||||
color: var(--syn-comment);
|
||||
}
|
||||
.preview :global(.tok-meta),
|
||||
.preview :global(.tok-macroName) {
|
||||
color: var(--syn-annotation);
|
||||
}
|
||||
.preview :global(.tok-propertyName) {
|
||||
color: var(--syn-property);
|
||||
}
|
||||
.preview :global(.tok-variableName.tok-definition) {
|
||||
color: var(--syn-function);
|
||||
}
|
||||
|
||||
.note {
|
||||
min-width: 0;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { useCallback, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import { create } from "zustand";
|
||||
import { tokenSpans, useLineTokens } from "@/editor/lineTokens";
|
||||
import { formatMessage, useTx } from "@/platform/i18n";
|
||||
import { shortcutLabel } from "@/platform/keymap";
|
||||
import { basename, relativeDir, useProject } from "@/platform/project";
|
||||
@@ -46,12 +47,13 @@ type Node =
|
||||
| { kind: "usage"; key: string; usage: Usage };
|
||||
|
||||
export function UsagePreview({ usage }: { usage: Usage }) {
|
||||
const { text, from, to } = trimPreview(usage);
|
||||
const { text, textCol, from, to } = trimPreview(usage);
|
||||
const tokens = useLineTokens({ path: usage.path, line: usage.line, col: textCol }, text);
|
||||
return (
|
||||
<span className={styles.preview}>
|
||||
{text.slice(0, from)}
|
||||
<mark className={styles.match}>{text.slice(from, to)}</mark>
|
||||
{text.slice(to)}
|
||||
{tokenSpans(text, 0, from, tokens, "b:")}
|
||||
<mark className={styles.match}>{tokenSpans(text, from, to, tokens, "m:")}</mark>
|
||||
{tokenSpans(text, to, text.length, tokens, "a:")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
|
||||
vi.mock(import("@/platform/ipc"), async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
commands: { fsReadText: (path: string) => path } as never,
|
||||
call: (async () => "/**\n * Uses foo()\n */\n const x = foo();\n") as never,
|
||||
}));
|
||||
vi.mock(import("@/editor/api"), async (importOriginal) => ({ ...(await importOriginal()), editorsFor: () => [] }));
|
||||
|
||||
const { UsagePreview } = await import("./UsagesView");
|
||||
|
||||
describe("UsagePreview", () => {
|
||||
it("colours the usage line with its file as context", async () => {
|
||||
const { container } = render(
|
||||
<UsagePreview usage={{ path: "/p/a.ts", line: 3, col: 12, endCol: 15, text: " const x = foo();" }} />,
|
||||
);
|
||||
await waitFor(() => expect(container.querySelector(".tok-keyword")?.textContent).toBe("const"));
|
||||
expect(container.querySelector("mark")?.textContent).toBe("foo");
|
||||
expect(container.textContent).toBe("const x = foo();");
|
||||
});
|
||||
|
||||
it("colours a usage inside a doc comment as a comment", async () => {
|
||||
const { container } = render(
|
||||
<UsagePreview usage={{ path: "/p/a.ts", line: 1, col: 8, endCol: 11, text: " * Uses foo()" }} />,
|
||||
);
|
||||
await waitFor(() => expect(container.querySelector("mark .tok-comment")?.textContent).toBe("foo"));
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Find Usages (⌥F7 → "usages" tool window) and Show Usages (⌥⌘F7 → popup). */
|
||||
import { create } from "zustand";
|
||||
import type { Location, ReferencesIncomplete, RequestId } from "@/bindings";
|
||||
import { clearLineTokens } from "@/editor/lineTokens";
|
||||
import { commands, isCancelled, nextRequestId } from "@/platform/ipc";
|
||||
import { toolWindowService } from "@/workbench/contributions";
|
||||
import {
|
||||
@@ -57,12 +58,12 @@ export function usageCount(groups: readonly UsageGroup[]): number {
|
||||
}
|
||||
|
||||
/** Trims leading whitespace of a preview line, shifting the match range accordingly. */
|
||||
export function trimPreview(u: Usage): { text: string; from: number; to: number } {
|
||||
export function trimPreview(u: Usage): { text: string; textCol: number; from: number; to: number } {
|
||||
const lead = u.text.length - u.text.trimStart().length;
|
||||
const text = u.text.trim();
|
||||
const from = Math.max(0, u.col - lead);
|
||||
const to = Math.max(from, Math.min(text.length, u.endCol - lead));
|
||||
return { text, from, to };
|
||||
return { text, textCol: lead, from, to };
|
||||
}
|
||||
|
||||
export interface UsageSearch {
|
||||
@@ -170,6 +171,8 @@ export function incompleteNote(reason: ReferencesIncomplete, word: string | null
|
||||
* Cancel with `commands.cancelRequest(request)`.
|
||||
*/
|
||||
export async function fetchUsages(c: CaretContext, request: RequestId = nextRequestId()): Promise<FetchedUsages> {
|
||||
// Usage lines are read afresh: so are the files their syntax colours come from.
|
||||
clearLineTokens();
|
||||
let locs: Location[];
|
||||
let note: string | null = null;
|
||||
try {
|
||||
@@ -259,6 +262,7 @@ export async function showUsageResults(arg: unknown) {
|
||||
const base = { id, symbol, rerun: null };
|
||||
openUsageTab({ ...base, loading: true, groups: [], error: null, note: null });
|
||||
toolWindowService().show("usages");
|
||||
clearLineTokens();
|
||||
const files = await readManyFileLines(locs.map((l) => l.path));
|
||||
replaceUsageTab(id, { ...base, loading: false, groups: groupUsages(locs, files), error: null, note: null });
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("usages", () => {
|
||||
]);
|
||||
expect(usageCount(groups)).toBe(3);
|
||||
expect(groups[0]!.usages[0]!.text).toBe("");
|
||||
expect(trimPreview(groups[1]!.usages[1]!)).toEqual({ text: "const x = foo();", from: 10, to: 13 });
|
||||
expect(trimPreview(groups[1]!.usages[1]!)).toEqual({ text: "const x = foo();", textCol: 2, from: 10, to: 13 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user