Файли
openidea/src/ui/popupState.ts
T
l0ng-ai 81e0e8e2d0 feat: Align interactions with IntelliJ IDEA (#22)
* feat(nav): Complete file paths and flag unresolved web file references

Path completion in href/src/url()/@import, Markdown links and path-like strings, folders first. HTML and CSS paths that name no file get a 'Cannot resolve file' warning. File references resolve in the backend like IDEA's: relative to the file, /-rooted against the project and resource roots.

* feat: Align editor, workbench, run/debug and VCS interactions with IntelliJ IDEA

Keymap
- Let AltGr characters through instead of firing Ctrl+Alt shortcuts (Windows/Linux)
- Double Shift: strict press/release timing, ignored while typing or after Shift+click
- Chords wait 2s and swallow an unmatched second stroke
- Drop CodeMirror defaults that clash with IDEA keys; ⌘Enter is Split Line, add
  Scroll to Center and Code Block Start/End
- Add missing IDEA defaults (⌃J, ⌘Y, ⇧⌘C, ⌥⌘Y, ⇧⌘M, F9 Resume, ⌘R Rerun, …)

Editor
- Tab inserts at the caret; smart Backspace to the proper indent; Enter continues
  a `//` comment; per-language line comment placement
- Back/Forward records navigations only; ⌘F searches the selected text
- In-place rename; ⌃Space inserts a sole candidate; commit characters only for a
  focused lookup; usage highlighting survives edits; ⌥Enter offers fixes from the
  caret line; ⌘-hover shows declaration info only for navigable targets

Workbench
- Tool windows: primary and split area per side, six Move To anchors, layout migration
- ⌘W closes tool window content; Shift+Esc from the editor; closing a tab
  activates the left neighbour; preview tab and tab limit follow IDEA defaults
- Switcher lists open editors and tool windows; Recent Files Delete removes items
- Search Everywhere tab/scope behaviour; popups close when the window deactivates
- Project view: Copy/Move dialogs on paste, skippable usage search on delete,
  new-file input cancels on blur; Settings search Esc clears the filter
- Status bar breadcrumbs open a children popup; notification timeouts

Run / Debug / Build / Terminal
- Run asks to stop a running single-instance config; Rerun restarts silently;
  "Allow multiple instances" option
- Run chooser: Enter runs, Shift debugs, F4/Delete/digits
- Type into the console for stdin; Stop covers debug sessions
- Log (Shift+click) and temporary (Alt+click) breakpoints; independent suspend/log
  options; Evaluate prefill and history; changed-value highlighting; expandable hints
- Build window opens on failure and jumps to the first error
- Terminal confirms closing a busy tab and lets IDE shortcuts through

VCS
- Correct ours/theirs labels during rebase, cherry-pick and revert
- Smart/Force Checkout; remote branch checkout updates the local branch
- Partial commit by changed block; inline before-commit check results
- F7 continues into the next file; Rollback Lines; Commit and Push; Amend message swap
- Branch delete with Restore; force push confirmation; editable push target;
  update summary with the incoming commits

* chore: Regenerate bindings
2026-10-25 09:34:02 +08:00

109 рядки
3.2 KiB
TypeScript

/**
* Stack of open layers (popups, menus, dialogs, speed search). Only the
* top-most layer reacts to Escape; clicks inside a layer above don't dismiss
* the ones below. Publishes the `popupOpen` context key so global bindings
* (e.g. Escape → focus editor) stand down while a layer owns the keyboard.
*/
import { setContext } from "@/platform/context";
interface Layer {
id: number;
el: () => HTMLElement | null;
}
const layers: Layer[] = [];
let seq = 0;
let anonymous = 0;
function publish() {
setContext("popupOpen", layers.length + anonymous > 0);
}
export interface LayerHandle {
isTop(): boolean;
/** True when `target` lies inside this layer or any layer stacked above it. */
containsFromHere(target: Node): boolean;
release(): void;
}
export function openLayer(el: () => HTMLElement | null): LayerHandle {
const layer = { id: ++seq, el };
layers.push(layer);
publish();
let released = false;
return {
isTop: () => layers[layers.length - 1]?.id === layer.id,
containsFromHere(target) {
const idx = layers.findIndex((l) => l.id === layer.id);
for (let i = Math.max(0, idx); i < layers.length; i++) if (layers[i]!.el()?.contains(target)) return true;
return false;
},
release() {
if (released) return;
released = true;
const i = layers.findIndex((l) => l.id === layer.id);
if (i >= 0) layers.splice(i, 1);
publish();
},
};
}
/** Marks a keyboard-owning UI without a DOM layer (e.g. inline speed search). */
export function acquirePopup(): () => void {
anonymous++;
publish();
let released = false;
return () => {
if (released) return;
released = true;
anonymous = Math.max(0, anonymous - 1);
publish();
};
}
/**
* Calls `fn` when the app window is deactivated (IDEA cancels popups then).
* Focus moving into one of our own frames also blurs the window: ignored.
*/
export function onWindowDeactivate(fn: () => void): () => void {
let timer: ReturnType<typeof setTimeout> | undefined;
const onBlur = () => {
clearTimeout(timer);
timer = setTimeout(() => {
if (!document.hasFocus()) fn();
}, 0);
};
window.addEventListener("blur", onBlur);
return () => {
clearTimeout(timer);
window.removeEventListener("blur", onBlur);
};
}
export function isPopupOpen(): boolean {
return layers.length + anonymous > 0;
}
export const FOCUSABLE =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
/** Keeps Tab focus cycling inside `root`. Returns true when it handled the key. */
export function trapTab(e: KeyboardEvent, root: HTMLElement): boolean {
const items = [...root.querySelectorAll<HTMLElement>(FOCUSABLE)].filter((el) => el.tabIndex >= 0);
if (!items.length) return false;
const first = items[0]!,
last = items[items.length - 1]!;
const active = document.activeElement;
if (e.shiftKey && (active === first || !root.contains(active))) {
e.preventDefault();
last.focus();
return true;
}
if (!e.shiftKey && (active === last || !root.contains(active))) {
e.preventDefault();
first.focus();
return true;
}
return false;
}