Файли
openidea/src/editor/core/cm/smartKeys.ts
T
l0ng-ai fb3d42b76d 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
2026-10-25 00:35:33 +08:00

145 рядки
6.6 KiB
TypeScript
Неформатований Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
/**
* IDEA-style Tab, smart Backspace and Tab out of auto-inserted brackets and
* quotes.
*/
import { EditorSelection, StateEffect, countColumn, StateField, type EditorState, type Extension, type Transaction } from "@codemirror/state";
import { keymap, type Command } from "@codemirror/view";
import { getIndentUnit, getIndentation, indentString, indentUnit } from "@codemirror/language";
import { indentMore } from "@codemirror/commands";
// --- Tab -------------------------------------------------------------------------------
/**
* Tab: steps out of an auto-inserted pair (when enabled); with a selection
* holding more than whitespace, indents the selected lines; otherwise inserts
* whitespace at each caret up to the next indent stop (a tab character when
* the code style uses tabs). Live templates / Emmet get Tab first.
*/
export const insertTab: Command = (view) => {
const { state } = view;
if (state.readOnly) return false;
if (tabOutOfScope(view)) return true;
if (state.selection.ranges.some((r) => !r.empty && state.sliceDoc(r.from, r.to).trim())) return indentMore(view);
const useTab = state.facet(indentUnit) === "\t";
const size = Math.max(1, getIndentUnit(state));
const tr = state.changeByRange((r) => {
const line = state.doc.lineAt(r.from);
const col = countColumn(line.text.slice(0, r.from - line.from), state.tabSize);
const insert = useTab ? "\t" : " ".repeat(size - (col % size));
return { changes: { from: r.from, to: r.to, insert }, range: EditorSelection.cursor(r.from + insert.length) };
});
view.dispatch(state.update(tr, { scrollIntoView: true, userEvent: "input" }));
return true;
};
/** Tab ahead of the default `indentWithTab` (⇧Tab stays Unindent). */
export const tabKeymap: Extension = keymap.of([{ key: "Tab", run: insertTab }]);
// --- Backspace -------------------------------------------------------------------------------
/** Languages whose layout is indentation: IDEA keeps "to nearest indent position" there. */
const NEAREST_INDENT_LANGS = new Set(["python", "yaml", "shellscript"]);
export function smartBackspaceApplies(mode: string, language: string | undefined): boolean {
return mode === "autoindent" && !NEAREST_INDENT_LANGS.has(language ?? "");
}
/** The change one Backspace makes at `pos` in "To proper indent position" mode, or null to delete normally. */
export function autoIndentBackspace(state: EditorState, pos: number): { from: number; to: number; insert: string } | null {
const doc = state.doc;
const line = doc.lineAt(pos);
if (/[^ \t]/.test(doc.sliceString(line.from, pos))) return null;
const target = getIndentation(state, line.from);
if (target === null) return null;
const end = pos + /^[ \t]*/.exec(doc.sliceString(pos, line.to))![0].length;
const current = countColumn(doc.sliceString(line.from, end), state.tabSize);
if (current > target) return { from: line.from, to: end, insert: indentString(state, target) };
if (line.number === 1) return end > 0 ? { from: 0, to: end, insert: "" } : null;
const prev = doc.line(line.number - 1);
const prevEnd = prev.from + prev.text.trimEnd().length;
if (prevEnd === prev.from && prevEnd !== 0) {
// A blank line above: the text moves up, at its proper indent.
return { from: prevEnd, to: end, insert: indentString(state, target) };
}
const before = doc.sliceString(prev.from, prevEnd);
const after = doc.sliceString(end, line.to);
const space = !after || !before || /[([{]$/.test(before) || /^[)\]}.,;]/.test(after) ? "" : " ";
return { from: prevEnd, to: end, insert: space };
}
/** Backspace in leading whitespace: to the proper indent, or joined with the line above when already there. */
export const smartBackspace: Command = (view) => {
const { state } = view;
if (state.readOnly || state.selection.ranges.some((r) => !r.empty)) return false;
const plans = state.selection.ranges.map((r) => autoIndentBackspace(state, r.head));
if (plans.some((p) => !p)) return false;
const tr = state.changeByRange((r) => {
const p = plans[state.selection.ranges.indexOf(r)]!;
return { changes: p, range: EditorSelection.cursor(p.from + p.insert.length) };
});
view.dispatch(state.update(tr, { scrollIntoView: true, userEvent: "delete.backward" }));
return true;
};
// --- Tab out of brackets / quotes ------------------------------------------------------------
const PAIRS: Record<string, string> = { "(": ")", "[": "]", "{": "}", '"': '"', "'": "'", "`": "`" };
interface Scope {
from: number;
to: number;
}
const leaveScopes = StateEffect.define<readonly number[]>();
/** Inside each pair the editor inserted itself; Tab at a scope's end steps over the closing character. */
export const tabOutScopes = StateField.define<readonly Scope[]>({
create: () => [],
update(scopes, tr) {
for (const e of tr.effects) if (e.is(leaveScopes)) scopes = scopes.filter((s) => !e.value.includes(s.to));
if (!tr.docChanged) return scopes;
const caret = tr.startState.selection.main.head;
let next: Scope[] = scopes.filter((s) => {
let keep = true;
tr.changes.iterChangedRanges((fromA, toA) => {
const outside = fromA < s.from || toA > s.to;
if (outside && (caret < s.from || caret > s.to || (toA >= s.from && fromA <= s.to))) keep = false;
});
return keep;
});
next = next.map((s) => ({ from: tr.changes.mapPos(s.from, -1), to: tr.changes.mapPos(s.to, 1) }));
return [...next, ...insertedPairs(tr)];
},
});
/** Empty scopes between the bracket / quote pairs a typing transaction auto-inserted. */
function insertedPairs(tr: Transaction): Scope[] {
if (!tr.isUserEvent("input.type")) return [];
const out: Scope[] = [];
tr.changes.iterChanges((_fa, _ta, fromB, toB, inserted) => {
const text = inserted.toString();
if (toB - fromB !== 2 || PAIRS[text[0]!] !== text[1]) return;
const inside = fromB + 1;
if (tr.newSelection.ranges.some((r) => r.empty && r.head === inside)) out.push({ from: inside, to: inside });
});
return out;
}
export const tabOutOfScope: Command = (view) => {
const { state } = view;
const scopes = state.field(tabOutScopes, false);
if (!scopes?.length || state.selection.ranges.some((r) => !r.empty || !scopes.some((s) => s.to === r.head))) return false;
view.dispatch({
selection: EditorSelection.create(
state.selection.ranges.map((r) => EditorSelection.cursor(Math.min(r.head + 1, state.doc.length))),
state.selection.mainIndex,
),
effects: leaveScopes.of(state.selection.ranges.map((r) => r.head)),
userEvent: "select",
});
return true;
};
/** Settings › Smart Keys › "Jump outside closing bracket/quote with Tab" (Tab itself is `insertTab`). */
export const tabOut: Extension = tabOutScopes;