Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 1 addition & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,15 @@ html, body, #root {

/* Don't have text overflow in the structure dialog */
.structure-tree {
overflow: auto;
overflow: auto;
}

.full-text-search-item {
cursor: pointer;
}

.full-text-search-item:hover {
background-color: rgba(255, 255, 255, 0.1);
}

.ant-tabs-content {
Expand Down
5 changes: 3 additions & 2 deletions src/logic/Keybinds.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BehaviorSubject, filter, fromEvent, Observable, tap } from "rxjs";
import { focusSearch, showStructure, type KeybindSetting } from "./Settings";
import { focusSearch, fullTextSearchBind, showStructure, type KeybindSetting } from "./Settings";

// Set to true when the user is currently capturing a keybind
export const capturingKeybind = new BehaviorSubject<string | null>(null);
Expand All @@ -19,4 +19,5 @@ function keyBindEvent(setting: KeybindSetting): Observable<KeyboardEvent> {
}

export const focusSearchEvent = keyBindEvent(focusSearch);
export const showStructureEvent = keyBindEvent(showStructure);
export const showStructureEvent = keyBindEvent(showStructure);
export const fullTextSearchEvent = keyBindEvent(fullTextSearchBind);
1 change: 1 addition & 0 deletions src/logic/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export const bytecode = new BooleanSetting('bytecode', false);
export const unifiedDiff = new BooleanSetting('unified_diff', false);
export const focusSearch = new KeybindSetting('focus_search', 'Ctrl+ ');
export const showStructure = new KeybindSetting('show_structure', 'Ctrl+F12');
export const fullTextSearchBind = new KeybindSetting('full_text_search', 'Ctrl+Shift+f');

export const preferWasmDecompiler = new BooleanSetting('prefer_wasm_decompiler', true);
preferWasmDecompiler.observable
Expand Down
167 changes: 167 additions & 0 deletions src/ui/FullTextSearchModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useEffect, useRef, useState } from "react";
import { fullTextSearchEvent } from "../logic/Keybinds";
import { useObservable } from "../utils/UseObservable";
import { Button, Flex, Input, List, Modal, type InputRef } from "antd";
import { BehaviorSubject, catchError, combineLatest, distinctUntilChanged, from, map, of, startWith, switchMap } from "rxjs";
import { fullTextSearch } from "../workers/full-text-search/client";
import type { FullTextSearchResult } from "../workers/full-text-search/worker";
import { openCodeTab } from "../logic/tabs";

const SearchState = (r: SearchState) => r;
type SearchState =
| { state: "loading"; }
| { state: "ok"; results: FullTextSearchResult[]; }
| { state: "error"; error: string; };

function parseRegexQuery(input: string): { pattern: string; flags: string } | null {
const m = input.match(/^\/(.+?)(?:\/([gimsuy]*))?$/);
if (!m) return null;
try {
new RegExp(m[1], m[2] ?? "");
return { pattern: m[1], flags: m[2] ?? "" };
} catch {
return null;
}
}

const query = new BehaviorSubject("");
const search$ = combineLatest([fullTextSearch, query]).pipe(
distinctUntilChanged(),
switchMap(([fts, query]) => {
if (query.startsWith("/")) {
if (query.length < 2) return of(SearchState({
state: "error",
error: "Enter a regex pattern: /pattern/flags"
}));

const regex = parseRegexQuery(query);
if (!regex) return of(SearchState({
state: "error",
error: "Invalid regex"
}));

return from(fts.findByRegex(regex.pattern, regex.flags, { maxTokens: 11 })).pipe(
map(results => SearchState({ state: "ok", results })),
startWith(SearchState({ state: "loading" })),
catchError(error => of(SearchState({ state: "error", error: String(error) }))));
}

if (query.length < 3) return of(SearchState({
state: "error",
error: "Query must be at least 3 characters"
}));

return from(fts.find(query, { maxTokens: 11 })).pipe(
map(results => SearchState({ state: "ok", results })),
startWith(SearchState({ state: "loading" })),
catchError(error => of(SearchState({ state: "error", error: String(error) }))));
}));

type FullTextSearchResultElementProps = {
result: FullTextSearchResult;
};

const FullTextSearchResultElement: React.FC<FullTextSearchResultElementProps> = ({ result }) => {
const [expand, setExpand] = useState(false);

const sliced = expand ? result.regions : result.regions.slice(0, 5);
const canToggleExpand = expand || sliced.length < result.regions.length;

return (
<List.Item.Meta
title={result.key}
description={(
<>
{canToggleExpand && (
<Button
type="link"
onClick={(e) => { e.stopPropagation(); setExpand(!expand); }}
>
{expand ? "Hide" : "Show all"}
</Button>
)}
{sliced.map((r, i) => (
<div
key={i}
style={{
fontFamily: "monospace",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{r.snippet}
</div>
))}
</>
)}
/>
);
};

const FullTextSearchModal = () => {
const showEvent = useObservable(fullTextSearchEvent);
const search = useObservable(search$) ?? { state: "ok", results: [] };
const [open, setOpen] = useState(false);
const inputRef = useRef<InputRef>(null);

useEffect(() => {
if (showEvent) {
setOpen(true);
}
}, [showEvent]);

function openResult(result: FullTextSearchResult) {
setOpen(false);
openCodeTab(result.key);
}

let resultsElement;
if (search.state === "loading") {
resultsElement = (<div>Loading...</div>);
} else if (search.state === "error") {
resultsElement = (<div>{search.error}</div>);
} else if (search.results.length === 0) {
resultsElement = (<div>No results</div>);
} else {
resultsElement = (
<List
dataSource={search.results}
renderItem={result => (
<List.Item
onClick={() => openResult(result)}
className="full-text-search-item"
>
<FullTextSearchResultElement result={result} />
</List.Item>
)}
/>
);
}

return (
<Modal
title="Full Text Search"
open={open}
onCancel={() => setOpen(false)}
afterOpenChange={open => open && inputRef.current?.focus()}
footer={null}
width="50%"
>
<Flex vertical gap="medium">
<Input.Search
ref={inputRef}
placeholder="Search… or /regex/flags for regex"
onSearch={q => query.next(q.trim())}
/>
<div style={{
maxHeight: "70vh",
overflow: "scroll"
}}>
{resultsElement}
</div>
</Flex>
</Modal>
);
};
export default FullTextSearchModal;
2 changes: 2 additions & 0 deletions src/ui/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SettingsModal from "./SettingsModal";
import StructureModal from "./StructureModal";
import { JarDecompilerModal, JarDecompilerProgressModal } from "./JarDecompilerModal";
import IndexProgressNotification from "./IndexProgressNotification";
import FullTextSearchModal from "./FullTextSearchModal";

const Modals = () => {
return (
Expand All @@ -19,6 +20,7 @@ const Modals = () => {
<StructureModal />
<JarDecompilerModal />
<JarDecompilerProgressModal />
<FullTextSearchModal />
</>
);
};
Expand Down
13 changes: 7 additions & 6 deletions src/ui/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button, Modal, type CheckboxProps, Form, Tooltip, InputNumber, type Inp
import { SettingOutlined, SunOutlined, MoonOutlined, DesktopOutlined } from '@ant-design/icons';
import { Checkbox } from 'antd';
import { useObservable } from "../utils/UseObservable";
import { BooleanSetting, enableTabs, displayLambdas, focusSearch, KeybindSetting, type KeybindValue, bytecode, showStructure, NumberSetting, preferWasmDecompiler, compactPackages, theme } from "../logic/Settings";
import { BooleanSetting, enableTabs, displayLambdas, focusSearch, KeybindSetting, type KeybindValue, bytecode, showStructure, NumberSetting, preferWasmDecompiler, compactPackages, fullTextSearchBind, theme } from "../logic/Settings";
import { capturingKeybind, rawKeydownEvent } from "../logic/Keybinds";
import { BehaviorSubject } from "rxjs";
import type React from "react";
Expand Down Expand Up @@ -35,9 +35,10 @@ const SettingsModal = () => {
<BooleanOption setting={compactPackages} title={"Compact Packages"} tooltip="Collapse packages with one child into one." />
<BooleanOption setting={displayLambdas} title={"Lambda Names"} tooltip="Display lambda names as inline comments. Does not support permalinking." disabled={bytecodeValue} />
<BooleanOption setting={bytecode} title={"Show Bytecode"} tooltip="Show bytecode instructions alongside decompiled source. Does not support permalinking." disabled={displayLambdasValue} />
<BooleanOption setting={preferWasmDecompiler} title={"Prefer WASM Decompiler"} tooltip="WASM decompiler might be faster than JavaScript."/>
<BooleanOption setting={preferWasmDecompiler} title={"Prefer WASM Decompiler"} tooltip="WASM decompiler might be faster than JavaScript." />
<KeybindOption setting={focusSearch} title={"Focus Search"} captureId="focus_search" />
<KeybindOption setting={showStructure} title={"Show Structure"} captureId="show_structure" />
<KeybindOption setting={fullTextSearchBind} title={"Full Text Search"} captureId="full_text_search" />
</Form>
</Modal>
);
Expand Down Expand Up @@ -105,18 +106,18 @@ export interface NumberOptionProps {
testid?: string;
}

export const NumberOption: React.FC<NumberOptionProps> = ({ setting, title, min, max, testid}) => {
export const NumberOption: React.FC<NumberOptionProps> = ({ setting, title, min, max, testid }) => {
const value = useObservable(setting.observable);
const onChange: InputNumberProps<number>["onChange"] = (e) => {
setting.value = e ?? setting.defaultValue;
}
};

return (
<Form.Item label={title}>
<InputNumber data-testid={testid} min={min} max={max} value={value} onChange={onChange}/>
<InputNumber data-testid={testid} min={min} max={max} value={value} onChange={onChange} />
</Form.Item>
);
}
};

interface KeybindOptionProps {
setting: KeybindSetting;
Expand Down
Loading
Loading