дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
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
631 рядки
26 KiB
TypeScript
631 рядки
26 KiB
TypeScript
/** Editor actions (ids from keymaps/intellij-macos.ts). */
|
||
import { EditorSelection, type EditorState } from "@codemirror/state";
|
||
import type { EditorView } from "@codemirror/view";
|
||
import { addCursorAbove, addCursorBelow, moveLineDown, moveLineUp, toggleBlockComment } from "@codemirror/commands";
|
||
import { syntaxTree, toggleFold } from "@codemirror/language";
|
||
import { acceptCompletion, completionStatus } from "@codemirror/autocomplete";
|
||
import { executeAction, registerActions, type Action } from "@/platform/actions";
|
||
import { getContext } from "@/platform/context";
|
||
import { editorController, useGotoLine, useTabs } from "./controller";
|
||
import {
|
||
addCaretsToLineEnds,
|
||
commentLine,
|
||
completeStatement,
|
||
deleteLine,
|
||
duplicateLineOrSelection,
|
||
joinLines,
|
||
moveStatementDown,
|
||
moveStatementUp,
|
||
nextOccurrence,
|
||
selectAllOccurrences,
|
||
startNewLine,
|
||
startNewLineBefore,
|
||
toggleCase,
|
||
unselectOccurrence,
|
||
} from "./cm/editing";
|
||
import { emptyStack, extendSelection, shrinkSelection, type SelectionStack, type SimpleRange } from "./model/selectionStack";
|
||
import type { EditorSlot } from "./model/slot";
|
||
import { findAgain, openFindBar } from "./view/FindBar";
|
||
import { collapseAll, collapseRecursively, expandAll, expandRecursively, foldAtCaret, foldSelection, unfoldAtCaret } from "./cm/folding";
|
||
import { reverseLines, sortLines } from "./cm/lines";
|
||
import { cursorCodeBlockEnd, cursorCodeBlockStart, cursorMatchBrace, scrollToCenter, splitLine } from "./cm/caretCommands";
|
||
import { isOverwrite, toggleOverwrite } from "./cm/overwrite";
|
||
import { isColumnMode, toggleColumnMode } from "./cm/columnMode";
|
||
import { pasteAsPlainText, pasteFromHistory, unwrapAtCaret } from "./extras";
|
||
import { cyclicExpandWord } from "./wordCompletion";
|
||
import { qualifiedReference } from "./copyReference";
|
||
import { symbolChain } from "./view/outline";
|
||
import { recordClipboard } from "./clipboardHistory";
|
||
import { useDocMeta, useOutline } from "./model/document";
|
||
import { fontZoom, setFontZoom } from "./cm/prefs";
|
||
import { useEditorRegistry } from "@/editor/api";
|
||
import { useProject } from "@/platform/project";
|
||
import { toggleReadOnly } from "./fileAttributes";
|
||
import { openNavBarPopup } from "./view/NavBarPopup";
|
||
import {
|
||
cachedStructureQueries,
|
||
completeStatementStructural,
|
||
hasLezerTree,
|
||
moveStatementStructural,
|
||
type StructureQueries,
|
||
} from "./cm/structural";
|
||
import type { DocModel } from "./model/document";
|
||
import { structure } from "@/editor/structure";
|
||
|
||
/** Text-editing actions: the editor has focus, or a popup (Find Action) runs them on it. */
|
||
export const TEXT_WHEN = "editorTextFocus || textEditorActive && popupOpen";
|
||
const EDITOR_WHEN = "textEditorActive && (editorFocus || popupOpen || !toolWindowFocus)";
|
||
const TABS_WHEN = "editorTabsOpen && !toolWindowFocus";
|
||
|
||
function activeSlot(): EditorSlot | null {
|
||
const slot = editorController().activeSlot;
|
||
return slot && !slot.disposed ? slot : null;
|
||
}
|
||
|
||
function withView(run: (view: EditorView, slot: EditorSlot) => unknown) {
|
||
return () => {
|
||
const slot = activeSlot();
|
||
if (!slot?.view) return;
|
||
run(slot.view, slot);
|
||
if (!slot.view.hasFocus && !getContext().popupOpen) slot.view.focus();
|
||
};
|
||
}
|
||
|
||
// --- extend / shrink selection ---------------------------------------------------
|
||
|
||
/** One stack per selection range (by index): ⌥↑ / ⌥↓ work on every caret. */
|
||
const stacks = new WeakMap<EditorSlot, SelectionStack[]>();
|
||
|
||
/** Syntax-tree candidates when the backend has nothing (or the language isn't parsed there). */
|
||
export function syntaxCandidates(state: EditorState, sel: SimpleRange): SimpleRange[] {
|
||
const out: SimpleRange[] = [];
|
||
const word = state.wordAt(sel.from);
|
||
if (word) out.push({ from: word.from, to: word.to });
|
||
const tree = syntaxTree(state);
|
||
for (let node: ReturnType<typeof tree.resolveInner> | null = tree.resolveInner(sel.from, 1); node; node = node.parent) {
|
||
out.push({ from: node.from, to: node.to });
|
||
const text = state.sliceDoc(node.from, Math.min(node.to, node.from + 1)) + state.sliceDoc(Math.max(node.from, node.to - 1), node.to);
|
||
if (/^[([{"'`][)\]}"'`]$/.test(text) && node.to - node.from > 2) out.push({ from: node.from + 1, to: node.to - 1 });
|
||
}
|
||
const line = state.doc.lineAt(sel.from);
|
||
const endLine = state.doc.lineAt(sel.to);
|
||
const indent = /^\s*/.exec(line.text)![0].length;
|
||
out.push({ from: line.from + indent, to: endLine.to }, { from: line.from, to: endLine.to }, { from: 0, to: state.doc.length });
|
||
return out;
|
||
}
|
||
|
||
function rangesFromBackend(state: EditorState, ranges: Array<{ start: { line: number; col: number }; end: { line: number; col: number } }>): SimpleRange[] {
|
||
const off = (p: { line: number; col: number }) => {
|
||
if (p.line + 1 > state.doc.lines) return state.doc.length;
|
||
const l = state.doc.line(p.line + 1);
|
||
return Math.min(l.from + p.col, l.to);
|
||
};
|
||
return ranges.map((r) => ({ from: off(r.start), to: off(r.end) }));
|
||
}
|
||
|
||
export interface BackendChain {
|
||
/** `DocModel.changeCounter` when the chain was computed. */
|
||
counter: number;
|
||
/** Offset the backend was asked at. */
|
||
pos: number;
|
||
ranges: SimpleRange[];
|
||
}
|
||
|
||
/**
|
||
* The backend chain of a previous extend answers again while the document is
|
||
* unchanged and the selection still covers its position: every node around
|
||
* the selection is around that position too. Repeated ⌥↑ then needs no IPC.
|
||
*/
|
||
export function reusableChain(chain: BackendChain | undefined, counter: number, current: SimpleRange): SimpleRange[] | null {
|
||
return chain && chain.counter === counter && chain.pos >= current.from && chain.pos <= current.to ? chain.ranges : null;
|
||
}
|
||
|
||
const chains = new WeakMap<EditorSlot, BackendChain>();
|
||
|
||
/** Backend candidates around `current` (the cached chain for repeated extends of one range). */
|
||
async function backendCandidates(slot: EditorSlot, current: SimpleRange, cache: boolean): Promise<SimpleRange[]> {
|
||
const model = slot.model;
|
||
const cached = cache ? reusableChain(chains.get(slot), model.changeCounter, current) : null;
|
||
if (cached) return cached;
|
||
try {
|
||
await model.sync.whenIdle();
|
||
const counter = model.changeCounter;
|
||
const ranges = await model.manager.backend.selectionRanges(model.doc, current.from);
|
||
if (counter !== model.changeCounter || !slot.view) return [];
|
||
const candidates = rangesFromBackend(slot.view.state, ranges);
|
||
if (cache) chains.set(slot, { counter, pos: current.from, ranges: candidates });
|
||
return candidates;
|
||
} catch {
|
||
return []; // fall back to the syntax tree
|
||
}
|
||
}
|
||
|
||
async function extend(slot: EditorSlot) {
|
||
const view = slot.view;
|
||
if (!view) return;
|
||
const before = view.state.selection;
|
||
const currents = before.ranges.map((r) => ({ from: r.from, to: r.to }));
|
||
const candidates = await Promise.all(currents.map((c, i) => backendCandidates(slot, c, i === before.mainIndex)));
|
||
const v = slot.view;
|
||
if (!v || !v.state.selection.eq(before)) return;
|
||
const prev = stacks.get(slot) ?? [];
|
||
const results = currents.map((c, i) =>
|
||
extendSelection(prev[i] ?? emptyStack(), c, [...candidates[i]!, ...syntaxCandidates(v.state, c)]),
|
||
);
|
||
stacks.set(slot, results.map((r) => r.state));
|
||
if (!results.some((r) => r.next)) return;
|
||
const ranges = results.map((r, i) => (r.next ? EditorSelection.range(r.next.from, r.next.to) : before.ranges[i]!));
|
||
v.dispatch({ selection: EditorSelection.create(ranges, before.mainIndex), scrollIntoView: true, userEvent: "select.extend" });
|
||
}
|
||
|
||
/** Extend / shrink run in order: a quick ⌥↑⌥↑ extends twice although extending waits for the backend. */
|
||
const selectionQueue = new WeakMap<EditorSlot, Promise<void>>();
|
||
|
||
function queueSelection(slot: EditorSlot, step: (slot: EditorSlot) => void | Promise<void>) {
|
||
const next = (selectionQueue.get(slot) ?? Promise.resolve()).then(() => step(slot)).catch(() => {});
|
||
selectionQueue.set(slot, next);
|
||
return next;
|
||
}
|
||
|
||
function shrink(slot: EditorSlot) {
|
||
const view = slot.view;
|
||
if (!view) return;
|
||
const sel = view.state.selection;
|
||
const prev = stacks.get(slot) ?? [];
|
||
const results = sel.ranges.map((r, i) => shrinkSelection(prev[i] ?? emptyStack(), { from: r.from, to: r.to }));
|
||
stacks.set(slot, results.map((r) => r.state));
|
||
if (!results.some((r) => r.next)) return;
|
||
const ranges = results.map((r, i) => (r.next ? EditorSelection.range(r.next.from, r.next.to) : sel.ranges[i]!));
|
||
view.dispatch({ selection: EditorSelection.create(ranges, sel.mainIndex), scrollIntoView: true, userEvent: "select.shrink" });
|
||
}
|
||
|
||
// --- structural editing without a Lezer tree ------------------------------------------
|
||
|
||
const structureQueries = new WeakMap<DocModel, StructureQueries>();
|
||
|
||
function queriesFor(model: DocModel): StructureQueries {
|
||
let q = structureQueries.get(model);
|
||
if (!q) {
|
||
q = cachedStructureQueries({
|
||
stamp: () => model.changeCounter,
|
||
synced: () => model.sync.whenIdle(),
|
||
fetchRanges: (pos) => structure.structuralRanges(model.doc, pos),
|
||
fetchErrors: () => structure.syntaxErrors(model.doc),
|
||
});
|
||
structureQueries.set(model, q);
|
||
}
|
||
return q;
|
||
}
|
||
|
||
async function hasStructure(language: string): Promise<boolean> {
|
||
try {
|
||
return (await structure.support(language)).tree;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/** Structural edits wait for the backend: run them in order so a quick ⇧⌘↑⇧⌘↑ moves twice. */
|
||
const editQueue = new WeakMap<EditorSlot, Promise<void>>();
|
||
|
||
function queueEdit(slot: EditorSlot, step: () => Promise<unknown>) {
|
||
const next = (editQueue.get(slot) ?? Promise.resolve()).then(step).then(
|
||
() => {},
|
||
(e: unknown) => console.warn("structural edit failed", e),
|
||
);
|
||
editQueue.set(slot, next);
|
||
return next;
|
||
}
|
||
|
||
/** Move Statement: the Lezer tree where there is one, else the tree-sitter structure, else the line. */
|
||
function moveStatement(view: EditorView, slot: EditorSlot, up: boolean) {
|
||
const sel = view.state.selection;
|
||
if (hasLezerTree(view.state) || sel.ranges.length > 1 || !sel.main.empty) {
|
||
return up ? moveStatementUp(view) : moveStatementDown(view);
|
||
}
|
||
void queueEdit(slot, async () => {
|
||
const v = slot.view;
|
||
if (!v) return;
|
||
const language = slot.model.getMeta().language;
|
||
if ((await hasStructure(language)) && (await moveStatementStructural(v, up, queriesFor(slot.model))) !== "fallback") return;
|
||
if (slot.view === v) (up ? moveLineUp : moveLineDown)(v);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Complete Current Statement: tree-sitter knows what is missing where (and
|
||
* which multi-line statement the caret is in) for stream-mode languages, and
|
||
* places Java's `;` better than the line heuristics; the heuristics finish the line.
|
||
* With the completion lookup open, its selected item is inserted first (IDEA).
|
||
*/
|
||
function completeCurrentStatement(view: EditorView, slot: EditorSlot) {
|
||
if (completionStatus(view.state) === "active") acceptCompletion(view);
|
||
const language = slot.model.getMeta().language;
|
||
const sel = view.state.selection;
|
||
if ((hasLezerTree(view.state) && language !== "java") || sel.ranges.length > 1) return completeStatement(language)(view);
|
||
void queueEdit(slot, async () => {
|
||
const v = slot.view;
|
||
if (!v || v.state.readOnly) return;
|
||
if ((await hasStructure(language)) && (await completeStatementStructural(v, language, queriesFor(slot.model))) !== "fallback") return;
|
||
if (slot.view === v) completeStatement(language)(v);
|
||
});
|
||
}
|
||
|
||
// --- cyclic expand word / copy reference ------------------------------------------------
|
||
|
||
/** Text of the other open documents for Cyclic Expand Word (bounded). */
|
||
function otherEditorTexts(current: EditorSlot): string[] {
|
||
const out: string[] = [];
|
||
const seen = new Set([current.model.path]);
|
||
let budget = 4 * 1024 * 1024;
|
||
for (const slot of editorController().slots.values()) {
|
||
if (slot.disposed || seen.has(slot.model.path)) continue;
|
||
seen.add(slot.model.path);
|
||
const doc = slot.currentState().doc;
|
||
if (doc.length > budget) continue;
|
||
budget -= doc.length;
|
||
out.push(doc.toString());
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function expandWord(view: EditorView, slot: EditorSlot, direction: "backward" | "forward") {
|
||
const message = cyclicExpandWord(view, direction, () => otherEditorTexts(slot));
|
||
if (message) void executeAction("editor.showHint", { text: message });
|
||
}
|
||
|
||
function copyReference(view: EditorView, slot: EditorSlot) {
|
||
const state = view.state;
|
||
const head = state.selection.main.head;
|
||
const line = state.doc.lineAt(head);
|
||
const meta = slot.model.getMeta();
|
||
const outline = useOutline.getState()[slot.model.path] ?? [];
|
||
const reference = qualifiedReference({
|
||
language: meta.language,
|
||
path: slot.model.path,
|
||
root: useProject.getState().project?.root ?? null,
|
||
text: state.doc.toString(),
|
||
chain: symbolChain(outline, line.number - 1, head - line.from).map((i) => i.name),
|
||
line: line.number - 1,
|
||
});
|
||
recordClipboard(reference);
|
||
void navigator.clipboard?.writeText(reference).catch(() => {});
|
||
void executeAction("editor.showHint", { text: `Copied reference: ${reference}` });
|
||
}
|
||
|
||
/** ⌃G; when nothing more is found, a hint first and the search from the top on the next press (IDEA). */
|
||
function addNextOccurrence(view: EditorView) {
|
||
if (nextOccurrence(view) === "notFound") void executeAction("editor.showHint", { text: "Not found, press again to search from the top" });
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------
|
||
|
||
export function registerEditorActions(): () => void {
|
||
const ctrl = editorController();
|
||
const text = (id: string, title: string, run: (view: EditorView, slot: EditorSlot) => unknown, extra: Partial<Action> = {}): Action => ({
|
||
id,
|
||
title,
|
||
category: "Code",
|
||
when: TEXT_WHEN,
|
||
run: withView(run),
|
||
...extra,
|
||
});
|
||
const activePaneTab = () => {
|
||
const s = useTabs.getState();
|
||
const pane = s.panes[s.activePane];
|
||
return pane?.active ? { paneId: pane.id, tabId: pane.active } : null;
|
||
};
|
||
return registerActions([
|
||
text("code.commentLine", "Comment with Line Comment", (v) => commentLine(v)),
|
||
text("code.commentBlock", "Comment with Block Comment", (v) => toggleBlockComment(v)),
|
||
text("code.duplicateLine", "Duplicate Line or Selection", (v) => duplicateLineOrSelection(v)),
|
||
text("code.deleteLine", "Delete Line", (v) => deleteLine(v)),
|
||
text("code.moveLineUp", "Move Line Up", (v) => moveLineUp(v)),
|
||
text("code.moveLineDown", "Move Line Down", (v) => moveLineDown(v)),
|
||
text("code.moveStatementUp", "Move Statement Up", (v, slot) => moveStatement(v, slot, true)),
|
||
text("code.moveStatementDown", "Move Statement Down", (v, slot) => moveStatement(v, slot, false)),
|
||
text("code.extendSelection", "Extend Selection", (_v, slot) => void queueSelection(slot, extend), { category: "Edit" }),
|
||
text("code.shrinkSelection", "Shrink Selection", (_v, slot) => void queueSelection(slot, shrink), { category: "Edit" }),
|
||
text("code.fold", "Collapse", (v) => foldAtCaret(v), { category: "Folding", keywords: ["fold"] }),
|
||
text("code.unfold", "Expand", (v) => unfoldAtCaret(v), { category: "Folding", keywords: ["unfold"] }),
|
||
text("code.foldToggle", "Toggle Fold", (v) => toggleFold(v), { category: "Folding" }),
|
||
text("code.cyclicExpandWord", "Cyclic Expand Word", (v, slot) => expandWord(v, slot, "backward"), {
|
||
keywords: ["hippie", "complete word", "dabbrev"],
|
||
}),
|
||
text("code.cyclicExpandWordBackward", "Cyclic Expand Word (Backward)", (v, slot) => expandWord(v, slot, "forward"), {
|
||
keywords: ["hippie", "complete word", "dabbrev"],
|
||
}),
|
||
text("editor.copyReference", "Copy Reference", (v, slot) => copyReference(v, slot), {
|
||
category: "Edit",
|
||
keywords: ["fully qualified name", "fqn", "copy path"],
|
||
}),
|
||
text("code.pasteAsPlainText", "Paste as Plain Text", (v) => pasteAsPlainText(v), { category: "Edit", icon: "paste" }),
|
||
{
|
||
id: "editor.toggleSoftWrap",
|
||
title: "Soft-Wrap",
|
||
category: "View",
|
||
icon: "soft-wrap",
|
||
keywords: ["word wrap", "line wrap"],
|
||
when: "textEditorActive",
|
||
toggled: () => activeSlot()?.model.softWrapOn() ?? false,
|
||
run: () => activeSlot()?.model.toggleSoftWrap(),
|
||
},
|
||
text("code.foldAll", "Collapse All", (v) => collapseAll(v), { category: "Folding", icon: "collapse-all" }),
|
||
text("code.unfoldAll", "Expand All", (v) => expandAll(v), { category: "Folding", icon: "expand-all" }),
|
||
text("code.collapseRecursively", "Collapse Recursively", (v) => collapseRecursively(v), { category: "Folding", keywords: ["fold"] }),
|
||
text("code.expandRecursively", "Expand Recursively", (v) => expandRecursively(v), { category: "Folding", keywords: ["unfold"] }),
|
||
text("code.addCaretNextOccurrence", "Add Selection for Next Occurrence", (v) => addNextOccurrence(v), { category: "Edit" }),
|
||
text("code.selectAllOccurrences", "Select All Occurrences", (v) => selectAllOccurrences(v), { category: "Edit" }),
|
||
text("code.completeStatement", "Complete Current Statement", (v, slot) => completeCurrentStatement(v, slot)),
|
||
text("code.startNewLine", "Start New Line", (v) => startNewLine(v)),
|
||
text("code.startNewLineBefore", "Start New Line Before Current", (v) => startNewLineBefore(v)),
|
||
text("code.splitLine", "Split Line", (v) => splitLine(v)),
|
||
text("editor.scrollToCenter", "Scroll to Center", (v) => scrollToCenter(v), { category: "Editor Actions" }),
|
||
text("editor.codeBlockStart", "Move Caret to Code Block Start", (v) => cursorCodeBlockStart(v), { category: "Editor Actions" }),
|
||
text("editor.codeBlockEnd", "Move Caret to Code Block End", (v) => cursorCodeBlockEnd(v), { category: "Editor Actions" }),
|
||
text("editor.cloneCaretAbove", "Clone Caret Above", (v) => addCursorAbove(v), { category: "Editor Actions" }),
|
||
text("editor.cloneCaretBelow", "Clone Caret Below", (v) => addCursorBelow(v), { category: "Editor Actions" }),
|
||
text("editor.matchBrace", "Move Caret to Matching Brace", (v) => cursorMatchBrace(v), {
|
||
category: "Editor Actions",
|
||
// ⌃M is Show Commit Message History in the commit window.
|
||
keyWhen: "editorTextFocus",
|
||
}),
|
||
text("editor.toggleInsertState", "Toggle Insert/Overwrite", (v) => toggleOverwrite(v), {
|
||
category: "Editor Actions",
|
||
keywords: ["overwrite mode", "overtype"],
|
||
toggled: () => {
|
||
const view = activeSlot()?.view;
|
||
return view ? isOverwrite(view.state) : false;
|
||
},
|
||
}),
|
||
text("code.joinLines", "Join Lines", (v) => joinLines(v)),
|
||
text("code.toggleCase", "Toggle Case", (v) => toggleCase(v), { keywords: ["upper", "lower"] }),
|
||
text("code.addCaretsToLineEnds", "Add Carets to Ends of Selected Lines", (v) => addCaretsToLineEnds(v), { category: "Edit" }),
|
||
text("code.unselectOccurrence", "Unselect Occurrence", (v) => unselectOccurrence(v), { category: "Edit" }),
|
||
text("code.pasteFromHistory", "Paste from History…", (v) => pasteFromHistory(v), { category: "Edit", keywords: ["clipboard", "paste multiple"] }),
|
||
text("code.sortLines", "Sort Lines", (v) => sortLines(v), { category: "Edit" }),
|
||
text("code.reverseLines", "Reverse Lines", (v) => reverseLines(v), { category: "Edit" }),
|
||
text("code.unwrap", "Unwrap/Remove…", (_v, slot) => void unwrapAtCaret(slot), { keywords: ["unwrap", "remove if"] }),
|
||
text("code.foldSelection", "Fold Selection / Remove Region", (v) => foldSelection(v), { category: "Folding" }),
|
||
{
|
||
id: "editor.toggleColumnSelection",
|
||
title: "Column Selection Mode",
|
||
category: "Edit",
|
||
keywords: ["block selection", "rectangular selection"],
|
||
when: "textEditorActive",
|
||
toggled: () => {
|
||
const view = activeSlot()?.view;
|
||
return view ? isColumnMode(view.state) : false;
|
||
},
|
||
run: withView((v) => toggleColumnMode(v)),
|
||
},
|
||
{
|
||
id: "file.reloadFromDisk",
|
||
title: "Reload from Disk",
|
||
category: "File",
|
||
icon: "refresh",
|
||
when: "projectOpen",
|
||
keywords: ["synchronize", "refresh", "revert"],
|
||
run: ({ arg }) => {
|
||
const paths = typeof arg === "string" ? [arg] : Array.isArray(arg) ? arg.filter((p): p is string => typeof p === "string") : [];
|
||
const active = ctrl.activePath();
|
||
return ctrl.reloadFromDisk(paths.length ? paths : active ? [active] : []);
|
||
},
|
||
},
|
||
{
|
||
// IDEA's Synchronize: open documents without unsaved changes, the project tree and VCS status.
|
||
id: "file.reloadAllFromDisk",
|
||
title: "Reload All from Disk",
|
||
category: "File",
|
||
icon: "refresh",
|
||
when: "projectOpen",
|
||
keywords: ["synchronize", "refresh"],
|
||
run: async () => {
|
||
const meta = useDocMeta.getState();
|
||
await ctrl.reloadFromDisk(ctrl.openPaths().filter((p) => !meta[p]?.dirty));
|
||
void executeAction("project.refresh");
|
||
void executeAction("vcs.refresh");
|
||
},
|
||
},
|
||
{
|
||
id: "editor.resetFontSize",
|
||
title: "Reset Font Size in All Editors",
|
||
category: "View",
|
||
keywords: ["zoom", "font size", "mouse wheel"],
|
||
when: "projectOpen",
|
||
run: () => {
|
||
for (const ed of useEditorRegistry.getState().editors)
|
||
if (ed.view.state.field(fontZoom, false)) ed.view.dispatch({ effects: setFontZoom.of(0) });
|
||
},
|
||
},
|
||
{
|
||
id: "file.toggleReadOnly",
|
||
title: "Toggle Read-only Attribute",
|
||
category: "File",
|
||
keywords: ["make file writable", "make file read-only", "lock"],
|
||
when: "textEditorActive",
|
||
run: () => {
|
||
const slot = activeSlot();
|
||
if (slot) toggleReadOnly(slot.model);
|
||
},
|
||
},
|
||
{
|
||
id: "find.inFile",
|
||
title: "Find",
|
||
category: "Find",
|
||
icon: "search",
|
||
when: EDITOR_WHEN,
|
||
run: () => openFindBar(useTabs.getState().activePane, false),
|
||
},
|
||
{
|
||
id: "find.replaceInFile",
|
||
title: "Replace",
|
||
category: "Find",
|
||
icon: "replace",
|
||
when: EDITOR_WHEN,
|
||
run: () => openFindBar(useTabs.getState().activePane, true),
|
||
},
|
||
{
|
||
id: "find.next",
|
||
title: "Find Next Occurrence",
|
||
category: "Find",
|
||
icon: "arrow-down",
|
||
when: EDITOR_WHEN,
|
||
run: () => findAgain(useTabs.getState().activePane, true),
|
||
},
|
||
{
|
||
id: "find.previous",
|
||
title: "Find Previous Occurrence",
|
||
category: "Find",
|
||
icon: "arrow-up",
|
||
when: EDITOR_WHEN,
|
||
run: () => findAgain(useTabs.getState().activePane, false),
|
||
},
|
||
{
|
||
id: "navigate.gotoLine",
|
||
title: "Go to Line:Column…",
|
||
category: "Navigate",
|
||
when: EDITOR_WHEN,
|
||
run: () => useGotoLine.setState({ open: true }),
|
||
},
|
||
{
|
||
id: "navigate.back",
|
||
title: "Back",
|
||
category: "Navigate",
|
||
icon: "arrow-left",
|
||
run: () => ctrl.back(),
|
||
},
|
||
{
|
||
id: "navigate.forward",
|
||
title: "Forward",
|
||
category: "Navigate",
|
||
icon: "arrow-right",
|
||
run: () => ctrl.forward(),
|
||
},
|
||
{
|
||
id: "navigate.navigationBar",
|
||
title: "Jump to Navigation Bar",
|
||
category: "Navigate",
|
||
keywords: ["breadcrumbs"],
|
||
when: "projectOpen",
|
||
run: () => {
|
||
if (activeSlot()) return openNavBarPopup();
|
||
// No editor: the status bar breadcrumbs (StatusItems › Breadcrumbs) take focus.
|
||
const crumbs = document.querySelectorAll<HTMLButtonElement>('nav[aria-label="Breadcrumbs"] button');
|
||
crumbs[crumbs.length - 1]?.focus();
|
||
},
|
||
},
|
||
{
|
||
id: "navigate.lastEditLocation",
|
||
title: "Last Edit Location",
|
||
category: "Navigate",
|
||
when: "projectOpen",
|
||
run: () => void ctrl.lastEditLocation(),
|
||
},
|
||
{
|
||
id: "navigate.nextEditLocation",
|
||
title: "Next Edit Location",
|
||
category: "Navigate",
|
||
when: "projectOpen",
|
||
run: () => void ctrl.nextEditLocation(),
|
||
},
|
||
{
|
||
id: "editor.closeTab",
|
||
title: "Close Tab",
|
||
category: "Window",
|
||
icon: "close",
|
||
when: "(editorTabsOpen || editorSplit) && !toolWindowFocus",
|
||
run: () => ctrl.closeActiveTab(),
|
||
},
|
||
{
|
||
id: "editor.closeOtherTabs",
|
||
title: "Close Other Tabs",
|
||
category: "Window",
|
||
when: TABS_WHEN,
|
||
run: () => {
|
||
const t = activePaneTab();
|
||
if (t) ctrl.closeOthers(t.paneId, t.tabId);
|
||
},
|
||
},
|
||
{
|
||
id: "editor.closeAllTabs",
|
||
title: "Close All Tabs",
|
||
category: "Window",
|
||
when: "editorTabsOpen",
|
||
run: () => ctrl.closeEverything(),
|
||
},
|
||
{
|
||
id: "editor.pinTab",
|
||
title: "Pin / Unpin Tab",
|
||
category: "Window",
|
||
icon: "pin",
|
||
when: TABS_WHEN,
|
||
run: () => {
|
||
const t = activePaneTab();
|
||
if (t) ctrl.togglePin(t.paneId, t.tabId);
|
||
},
|
||
},
|
||
{
|
||
id: "editor.reopenClosedTab",
|
||
title: "Reopen Closed Tab",
|
||
category: "Window",
|
||
run: () => ctrl.reopenClosed(),
|
||
},
|
||
{ id: "editor.nextTab", title: "Select Next Tab", category: "Window", when: TABS_WHEN, run: () => ctrl.cycle(1) },
|
||
{ id: "editor.prevTab", title: "Select Previous Tab", category: "Window", when: TABS_WHEN, run: () => ctrl.cycle(-1) },
|
||
{
|
||
id: "editor.splitRight",
|
||
title: "Split Right",
|
||
category: "Window",
|
||
icon: "split",
|
||
when: "editorTabsOpen",
|
||
run: () => ctrl.split(useTabs.getState().activePane, "row"),
|
||
},
|
||
{
|
||
id: "editor.splitDown",
|
||
title: "Split Down",
|
||
category: "Window",
|
||
icon: "split-horizontal",
|
||
when: "editorTabsOpen",
|
||
run: () => ctrl.split(useTabs.getState().activePane, "column"),
|
||
},
|
||
{
|
||
id: "editor.moveToOppositeGroup",
|
||
title: "Move to Opposite Group",
|
||
category: "Window",
|
||
when: "editorSplit",
|
||
run: () => {
|
||
const t = activePaneTab();
|
||
if (t) ctrl.moveToOpposite(t.paneId, t.tabId);
|
||
},
|
||
},
|
||
{
|
||
id: "editor.unsplit",
|
||
title: "Unsplit",
|
||
category: "Window",
|
||
when: "editorSplit",
|
||
run: () => ctrl.unsplit(useTabs.getState().activePane),
|
||
},
|
||
{
|
||
id: "editor.copyPath",
|
||
title: "Copy Path",
|
||
category: "Edit",
|
||
icon: "copy",
|
||
when: "textEditorActive",
|
||
// ⇧⌘C / Ctrl+Shift+C stays with the terminal (copy) and other tool windows.
|
||
keyWhen: "editorFocus || !toolWindowFocus",
|
||
run: () => {
|
||
const p = ctrl.activePath();
|
||
if (p) void navigator.clipboard?.writeText(p);
|
||
},
|
||
},
|
||
{
|
||
id: "file.save",
|
||
title: "Save All",
|
||
category: "File",
|
||
keywords: ["save"],
|
||
run: () => ctrl.saveAll(),
|
||
},
|
||
]);
|
||
}
|