дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Root cause: the editor had no font weight settings, and the WebView rendering of JetBrains Mono looks lighter than IDEA (#32). Fix (IDEA's Settings › Editor › Font › Typography Settings): configurable Main font weight and Bold font weight (defaults Regular 400 / Bold 700) via --fw-code / --fw-code-bold; colour-scheme bold/regular overrides still win; settings preview updates live. Replaces the Light-only 450/550 hard-coding. Siblings covered: sticky lines, diff/merge, run console (ANSI bold), Find in Files, completion/parameter info/docs, database editor, debugger value popup, matched braces, terminal with "Use editor font". Verified: scoped vitest, ide-core tests, in-app on macOS (settings UI, editor weight follows the setting end to end). Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
476 рядки
20 KiB
TypeScript
476 рядки
20 KiB
TypeScript
/**
|
||
* Editor behaviour driven by Settings › Editor (General, Appearance, Smart
|
||
* Keys, Code Editing, Code Style). Everything here is rebuilt into the
|
||
* `prefs` compartment when settings change.
|
||
*/
|
||
import { EditorSelection, EditorState, Prec, RangeSetBuilder, StateEffect, StateField, type Extension, type Transaction, type TransactionSpec } from "@codemirror/state";
|
||
import {
|
||
Decoration,
|
||
EditorView,
|
||
MatchDecorator,
|
||
ViewPlugin,
|
||
drawSelection,
|
||
keymap,
|
||
scrollPastEnd,
|
||
type DecorationSet,
|
||
type ViewUpdate,
|
||
} from "@codemirror/view";
|
||
import { highlightTree } from "@lezer/highlight";
|
||
import { getIndentUnit, getIndentation, indentRange, indentString, indentUnit, syntaxTree } from "@codemirror/language";
|
||
import {
|
||
cursorLineStart,
|
||
deleteCharBackwardStrict,
|
||
insertNewlineKeepIndent,
|
||
selectLineStart,
|
||
} from "@codemirror/commands";
|
||
import type { EditorExtras } from "@/platform/editorSettings";
|
||
import { openideaHighlightStyle } from "./highlight";
|
||
import {
|
||
cursorHumpLeft,
|
||
cursorHumpRight,
|
||
deleteHumpBackward,
|
||
deleteHumpForward,
|
||
deleteWordBackward,
|
||
selectHumpLeft,
|
||
selectHumpRight,
|
||
} from "./editing";
|
||
import { enterInLineComment, lineCommentStyle, type LineCommentStyle } from "./comments";
|
||
import { skipClosingQuote, smartBackspace, smartBackspaceApplies, tabOut } from "./smartKeys";
|
||
|
||
// --- soft wraps ---------------------------------------------------------------
|
||
|
||
/** `*.md; *.txt` style patterns matched against the file name (`*` alone matches everything). */
|
||
export function matchesFilePatterns(patterns: string, path: string): boolean {
|
||
const name = path.slice(path.lastIndexOf("/") + 1);
|
||
return patterns
|
||
.split(/[;,]/)
|
||
.map((p) => p.trim())
|
||
.filter(Boolean)
|
||
.some((p) => {
|
||
const re = new RegExp(`^${p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`, "i");
|
||
return re.test(name);
|
||
});
|
||
}
|
||
|
||
export function softWrapActive(softWrap: boolean | undefined, prefs: EditorExtras, path: string): boolean {
|
||
return !!softWrap && matchesFilePatterns(prefs.general.softWrapFiles || "*", path);
|
||
}
|
||
|
||
const wrapIndentCache = new Map<number, Decoration>();
|
||
function wrapIndentDeco(cols: number) {
|
||
let d = wrapIndentCache.get(cols);
|
||
if (!d) {
|
||
d = Decoration.line({ attributes: { style: `padding-left: calc(6px + ${cols}ch); text-indent: -${cols}ch` } });
|
||
wrapIndentCache.set(cols, d);
|
||
}
|
||
return d;
|
||
}
|
||
|
||
/** "Use original line's indent for wrapped parts": a hanging indent per visible line. */
|
||
const wrapIndent = ViewPlugin.fromClass(
|
||
class {
|
||
decorations: DecorationSet;
|
||
constructor(view: EditorView) {
|
||
this.decorations = this.build(view);
|
||
}
|
||
update(u: ViewUpdate) {
|
||
if (u.docChanged || u.viewportChanged) this.decorations = this.build(u.view);
|
||
}
|
||
build(view: EditorView): DecorationSet {
|
||
const b = new RangeSetBuilder<Decoration>();
|
||
const tab = view.state.tabSize;
|
||
for (const { from, to } of view.visibleRanges) {
|
||
for (let pos = from; pos <= to; ) {
|
||
const line = view.state.doc.lineAt(pos);
|
||
let cols = 0;
|
||
for (let i = 0; i < line.length; i++) {
|
||
const ch = line.text.charCodeAt(i);
|
||
if (ch === 32) cols++;
|
||
else if (ch === 9) cols += tab - (cols % tab);
|
||
else break;
|
||
}
|
||
if (cols > 0 && cols < line.length + tab) b.add(line.from, line.from, wrapIndentDeco(cols));
|
||
pos = line.to + 1;
|
||
}
|
||
}
|
||
return b.finish();
|
||
}
|
||
},
|
||
{ decorations: (v) => v.decorations },
|
||
);
|
||
|
||
// --- whitespace ---------------------------------------------------------------
|
||
|
||
const spaceMark = Decoration.mark({ class: "cm-highlightSpace" });
|
||
const tabMark = Decoration.mark({ class: "cm-highlightTab" });
|
||
|
||
/** Show whitespaces with IDEA's Leading / Inner / Trailing filters. */
|
||
export function whitespaceMarks(o: { leading: boolean; inner: boolean; trailing: boolean }): Extension {
|
||
const decorator = new MatchDecorator({
|
||
regexp: /[ \t]/g,
|
||
decorate: (add, from, to, match, view) => {
|
||
const line = view.state.doc.lineAt(from);
|
||
const col = from - line.from;
|
||
const lead = line.text.length - line.text.trimStart().length;
|
||
const trail = line.text.trimEnd().length;
|
||
const kind = col < lead ? "leading" : col >= trail ? "trailing" : "inner";
|
||
// Whitespace-only lines count as leading.
|
||
if (lead === line.length ? !o.leading && !o.trailing : !o[kind]) return;
|
||
add(from, to, match[0] === "\t" ? tabMark : spaceMark);
|
||
},
|
||
});
|
||
return ViewPlugin.fromClass(
|
||
class {
|
||
decorations: DecorationSet;
|
||
constructor(view: EditorView) {
|
||
this.decorations = decorator.createDeco(view);
|
||
}
|
||
update(u: ViewUpdate) {
|
||
this.decorations = decorator.updateDeco(u, this.decorations);
|
||
}
|
||
},
|
||
{ decorations: (v) => v.decorations },
|
||
);
|
||
}
|
||
|
||
// --- right margin / visual guides --------------------------------------------
|
||
|
||
/** Vertical lines at the hard-wrap column and the visual guides (`ch` columns of the mono font). */
|
||
export function marginGuides(columns: readonly number[]): Extension {
|
||
const cols = [...new Set(columns.filter((c) => c > 0 && c < 1000))].sort((a, b) => a - b);
|
||
if (!cols.length) return [];
|
||
const image = cols
|
||
.map(
|
||
(c) =>
|
||
`linear-gradient(to right, transparent calc(6px + ${c}ch), var(--border-right-margin, var(--border)) calc(6px + ${c}ch), var(--border-right-margin, var(--border)) calc(7px + ${c}ch), transparent calc(7px + ${c}ch))`,
|
||
)
|
||
.join(", ");
|
||
return EditorView.contentAttributes.of({ style: `background-image: ${image}` });
|
||
}
|
||
|
||
// --- method separators --------------------------------------------------------
|
||
|
||
/** Lines (0-based) that start a function / method, from the document outline. */
|
||
export const setMethodLines = StateEffect.define<number[]>();
|
||
|
||
const separatorDeco = Decoration.line({ class: "cm-openidea-methodSeparator" });
|
||
|
||
const methodLines = StateField.define<number[]>({
|
||
create: () => [],
|
||
update(lines, tr) {
|
||
for (const e of tr.effects) if (e.is(setMethodLines)) return e.value;
|
||
if (!tr.docChanged || !lines.length) return lines;
|
||
const oldDoc = tr.startState.doc;
|
||
return lines
|
||
.filter((l) => l + 1 <= oldDoc.lines)
|
||
.map((l) => tr.state.doc.lineAt(tr.changes.mapPos(oldDoc.line(l + 1).from, 1)).number - 1);
|
||
},
|
||
provide: (f) =>
|
||
EditorView.decorations.compute([f], (state) => {
|
||
const b = new RangeSetBuilder<Decoration>();
|
||
for (const l of [...new Set(state.field(f))].sort((a, c) => a - c))
|
||
if (l > 0 && l + 1 <= state.doc.lines) {
|
||
const line = state.doc.line(l + 1);
|
||
b.add(line.from, line.from, separatorDeco);
|
||
}
|
||
return b.finish();
|
||
}),
|
||
});
|
||
|
||
/** Kept in every editor so outline refreshes can always deliver separators. */
|
||
export const methodLinesField = methodLines;
|
||
|
||
// --- paste --------------------------------------------------------------------
|
||
|
||
function leadingColumns(text: string, tabSize: number): number {
|
||
let col = 0;
|
||
for (const ch of text) {
|
||
if (ch === " ") col++;
|
||
else if (ch === "\t") col += tabSize - (col % tabSize);
|
||
else break;
|
||
}
|
||
return col;
|
||
}
|
||
|
||
/** Reformat on paste › Indent Block: re-indents a pasted block to the caret's indentation. */
|
||
export function indentPastedText(text: string, targetCol: number, unit: string, tabSize: number, unitCols = tabSize): string {
|
||
const lines = text.split("\n");
|
||
if (lines.length < 2) return text;
|
||
const rest = lines.slice(1).filter((l) => l.trim());
|
||
if (!rest.length) return text;
|
||
// A first line copied with its indentation defines the block's base; one copied from
|
||
// mid-line does not, so the following lines do.
|
||
const firstIndented = /^[ \t]/.test(lines[0]!);
|
||
let base = Math.min(...(firstIndented ? [lines[0]!, ...rest] : rest).map((l) => leadingColumns(l, tabSize)));
|
||
// A block opener copied from mid-line ("if x:", "fn f() {") sat one level above its body.
|
||
if (!firstIndented && base > 0 && /[:{([]\s*$/.test(lines[0]!)) base = Math.max(0, base - unitCols);
|
||
const delta = targetCol - base;
|
||
const pad = (n: number) => (unit === "\t" ? "\t".repeat(Math.floor(n / tabSize)) + " ".repeat(n % tabSize) : " ".repeat(n));
|
||
return lines
|
||
.map((l, i) => {
|
||
if (i === 0) return l.trimStart();
|
||
if (!l.trim()) return "";
|
||
return pad(Math.max(0, leadingColumns(l, tabSize) + delta)) + l.trimStart();
|
||
})
|
||
.join("\n");
|
||
}
|
||
|
||
/**
|
||
* Reformat on paste. `indentBlock` shifts the pasted block to the paste
|
||
* column; `indentEachLine` (IDEA's default) then gives each pasted line the
|
||
* language's indentation, where the language can compute one. A paste
|
||
* after nothing but whitespace starts at the line's proper indentation.
|
||
*/
|
||
export function pasteReindentFilter(mode: "indentBlock" | "indentEachLine") {
|
||
return EditorState.transactionFilter.of((tr: Transaction) => {
|
||
if (!tr.isUserEvent("input.paste") || !tr.docChanged) return tr;
|
||
const start = tr.startState;
|
||
const changes: Array<{ from: number; to: number; text: string }> = [];
|
||
tr.changes.iterChanges((from, to, _fb, _tb, inserted) => changes.push({ from, to, text: inserted.toString() }));
|
||
if (changes.length !== 1) return tr;
|
||
let { from } = changes[0]!;
|
||
const { to, text } = changes[0]!;
|
||
if (!text.trim()) return tr;
|
||
const line = start.doc.lineAt(from);
|
||
const before = start.doc.sliceString(line.from, from);
|
||
const atIndent = /^\s*$/.test(before);
|
||
const tabSize = start.tabSize;
|
||
const unitCols = getIndentUnit(start) || tabSize;
|
||
const useTabs = start.facet(indentUnit)[0] === "\t";
|
||
let targetCol = leadingColumns(atIndent ? before : line.text, tabSize);
|
||
let prefix = "";
|
||
if (atIndent) {
|
||
const proper = getIndentation(start, line.from);
|
||
if (proper !== null && proper !== targetCol) {
|
||
targetCol = proper;
|
||
prefix = indentString(start, proper);
|
||
from = line.from;
|
||
}
|
||
}
|
||
const block = indentPastedText(text, targetCol, useTabs ? "\t" : " ", tabSize, unitCols);
|
||
const out = prefix + (prefix ? block.trimStart() : block);
|
||
const first: TransactionSpec = {
|
||
changes: { from, to, insert: out },
|
||
selection: EditorSelection.cursor(from + out.length),
|
||
userEvent: "input.paste",
|
||
scrollIntoView: true,
|
||
};
|
||
if (mode !== "indentEachLine") return out === text && from === changes[0]!.from ? tr : first;
|
||
// Lines whose start lies in the pasted text, plus the paste line when it was pasted at its indentation.
|
||
const pasted = start.update({ ...first, filter: false }).state;
|
||
const end = from + out.length;
|
||
const firstLine = pasted.doc.lineAt(from);
|
||
const lineStart = atIndent ? firstLine.from : firstLine.to + 1;
|
||
const lastLine = pasted.doc.lineAt(end);
|
||
const lineEnd = lastLine.from === end && end > from ? end - 1 : end;
|
||
if (lineStart > lineEnd) return first;
|
||
const reindent = indentRange(pasted, lineStart, lineEnd);
|
||
if (reindent.empty) return first;
|
||
return [first, { changes: reindent, selection: EditorSelection.cursor(reindent.mapPos(end, 1)), sequential: true }];
|
||
});
|
||
}
|
||
|
||
// --- rich-text copy -----------------------------------------------------------
|
||
|
||
const escapeHtml = (s: string) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
|
||
/** HTML of `[from, to)` with the editor's syntax colours inlined (Copy as rich text). */
|
||
export function richTextHtml(view: EditorView, from: number, to: number): string {
|
||
const state = view.state;
|
||
const styleOf = new Map<string, string>();
|
||
const probe = document.createElement("span");
|
||
view.contentDOM.appendChild(probe);
|
||
// Bold = heavier than the editor's main weight (Typography Settings).
|
||
const regular = Number(getComputedStyle(view.contentDOM).fontWeight) || 400;
|
||
const resolve = (cls: string) => {
|
||
let s = styleOf.get(cls);
|
||
if (s === undefined) {
|
||
probe.className = cls;
|
||
const cs = getComputedStyle(probe);
|
||
s = `color:${cs.color};${cs.fontStyle !== "normal" ? `font-style:${cs.fontStyle};` : ""}${Number(cs.fontWeight) > regular ? "font-weight:bold;" : ""}`;
|
||
styleOf.set(cls, s);
|
||
}
|
||
return s;
|
||
};
|
||
let html = "";
|
||
let pos = from;
|
||
try {
|
||
highlightTree(
|
||
syntaxTree(state),
|
||
openideaHighlightStyle,
|
||
(a, b, cls) => {
|
||
const s = Math.max(a, from);
|
||
const e = Math.min(b, to);
|
||
if (e <= s) return;
|
||
if (s > pos) html += escapeHtml(state.sliceDoc(pos, s));
|
||
html += `<span style="${resolve(cls)}">${escapeHtml(state.sliceDoc(s, e))}</span>`;
|
||
pos = e;
|
||
},
|
||
from,
|
||
to,
|
||
);
|
||
} finally {
|
||
probe.remove();
|
||
}
|
||
if (pos < to) html += escapeHtml(state.sliceDoc(pos, to));
|
||
const cs = getComputedStyle(view.contentDOM);
|
||
const bg = getComputedStyle(view.dom).backgroundColor;
|
||
return `<pre style="font-family:${cs.fontFamily.replace(/"/g, "'")};font-size:${cs.fontSize};color:${cs.color};background:${bg}">${html}</pre>`;
|
||
}
|
||
|
||
const richCopy = EditorView.domEventHandlers({
|
||
copy(e, view) {
|
||
const sel = view.state.selection;
|
||
if (sel.ranges.length !== 1 || sel.main.empty || !e.clipboardData) return false;
|
||
const { from, to } = sel.main;
|
||
try {
|
||
const html = richTextHtml(view, from, to);
|
||
e.clipboardData.clearData();
|
||
e.clipboardData.setData("text/plain", view.state.sliceDoc(from, to));
|
||
e.clipboardData.setData("text/html", html);
|
||
e.preventDefault();
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
},
|
||
});
|
||
|
||
// --- caret / scrolling / mouse ----------------------------------------------
|
||
|
||
/** Font zoom of one editor (⌘ + wheel), in px relative to the configured size. */
|
||
export const setFontZoom = StateEffect.define<number>();
|
||
export const fontZoom = StateField.define<number>({
|
||
create: () => 0,
|
||
update: (v, tr) => tr.effects.reduce((acc, e) => (e.is(setFontZoom) ? e.value : acc), v),
|
||
provide: (f) =>
|
||
Prec.lowest(
|
||
EditorView.editorAttributes.compute([f], (s) => {
|
||
const z = s.field(f);
|
||
const attrs: Record<string, string> = {};
|
||
if (z)
|
||
attrs.style = `--openidea-editor-font-size: calc(var(--openidea-editor-base-size, var(--fs-code)) + ${z}px); --openidea-editor-line-height: calc(var(--openidea-editor-base-line, var(--lh-code)) + ${Math.round(z * 1.6)}px)`;
|
||
return attrs;
|
||
}),
|
||
),
|
||
});
|
||
|
||
const wheelZoom = EditorView.domEventHandlers({
|
||
wheel(e, view) {
|
||
if (!e.metaKey && !e.ctrlKey) return false;
|
||
e.preventDefault();
|
||
const z = view.state.field(fontZoom, false) ?? 0;
|
||
const next = Math.max(-6, Math.min(36, z + (e.deltaY < 0 ? 1 : -1)));
|
||
if (next !== z) view.dispatch({ effects: setFontZoom.of(next) });
|
||
return true;
|
||
},
|
||
});
|
||
|
||
const noTextDrag = EditorView.domEventHandlers({
|
||
dragstart(e, view) {
|
||
if (!view.contentDOM.contains(e.target as Node)) return false;
|
||
e.preventDefault();
|
||
return true;
|
||
},
|
||
});
|
||
|
||
/** "Prefer scrolling editor canvas to keep caret line centered". */
|
||
const centerCaret = EditorState.transactionExtender.of((tr) => {
|
||
if (!tr.selection || tr.docChanged || tr.isUserEvent("select.pointer") || !tr.isUserEvent("select")) return null;
|
||
return { effects: EditorView.scrollIntoView(tr.newSelection.main.head, { y: "center" }) };
|
||
});
|
||
|
||
// --- smart keys ----------------------------------------------------------------
|
||
|
||
const OPENERS: Record<string, true> = { "(": true, "[": true, "{": true, '"': true, "'": true, "`": true };
|
||
|
||
/** Typing a bracket / quote over a selection replaces it instead of wrapping it. */
|
||
const noSurround = EditorView.inputHandler.of((view, from, to, text) => {
|
||
if (!OPENERS[text] || from === to || view.state.selection.ranges.length !== 1) return false;
|
||
view.dispatch({ changes: { from, to, insert: text }, selection: EditorSelection.cursor(from + 1), userEvent: "input.type" });
|
||
return true;
|
||
});
|
||
|
||
/** Gutter icon groups of Settings › Editor › General › Gutter Icons (stable gutter classes). */
|
||
export const GUTTER_ICON_GROUPS = [
|
||
{ id: "run", gutterClass: "cm-openidea-gutter-run" },
|
||
{ id: "bookmarks", gutterClass: "cm-openidea-bookmarks" },
|
||
] as const;
|
||
|
||
export interface PrefsOptions {
|
||
prefs: EditorExtras;
|
||
path: string;
|
||
softWrap: boolean | undefined;
|
||
/** Soft wraps toggled for the file, overriding `softWrap` and the file patterns. */
|
||
wrapOverride?: boolean | null;
|
||
showWhitespace: boolean | undefined;
|
||
/** Hard wrap column for this file's language. */
|
||
hardWrapAt: number;
|
||
readOnly: boolean;
|
||
/** Distraction Free Mode: no right margin / visual guides beside the centred column. */
|
||
distractionFree?: boolean;
|
||
/** Language id of the document (per-language smart-key behaviour). */
|
||
language?: string;
|
||
/** Code Style › Comment Code of the document's language. */
|
||
commentStyle?: LineCommentStyle;
|
||
}
|
||
|
||
export function caretExtension(prefs: EditorExtras): Extension {
|
||
const a = prefs.appearance;
|
||
return [
|
||
drawSelection({ cursorBlinkRate: a.caretBlinking ? Math.max(100, Math.min(5000, a.caretBlinkPeriod)) * 2 : 0 }),
|
||
a.blockCaret ? EditorView.editorAttributes.of({ class: "cm-openidea-blockCaret" }) : [],
|
||
];
|
||
}
|
||
|
||
export function prefsExtension(o: PrefsOptions): Extension {
|
||
const { prefs } = o;
|
||
const g = prefs.general;
|
||
const k = prefs.smartKeys;
|
||
const out: Extension[] = [];
|
||
if (g.scrollPastEnd) out.push(scrollPastEnd());
|
||
if ((o.wrapOverride ?? softWrapActive(o.softWrap, prefs, o.path)) && g.softWrapIndent) out.push(wrapIndent);
|
||
if (o.showWhitespace) {
|
||
const a = prefs.appearance;
|
||
out.push(whitespaceMarks({ leading: a.whitespaceLeading, inner: a.whitespaceInner, trailing: a.whitespaceTrailing }));
|
||
}
|
||
if (prefs.appearance.hardWrapGuides && !o.distractionFree) out.push(marginGuides([o.hardWrapAt, ...prefs.codeStyle.visualGuides]));
|
||
if (prefs.appearance.methodSeparators) out.push(EditorView.editorAttributes.of({ class: "cm-openidea-methodSeparators" }));
|
||
const hidden = GUTTER_ICON_GROUPS.filter((grp) => !prefs.showGutterIcons || prefs.gutterIconsDisabled.includes(grp.id));
|
||
if (hidden.length) out.push(EditorView.editorAttributes.of({ class: hidden.map((grp) => `cm-openidea-hideGutter-${grp.id}`).join(" ") }));
|
||
if (g.wheelZoom) out.push(wheelZoom);
|
||
if (!g.dragAndDrop) out.push(noTextDrag);
|
||
if (g.caretScroll === "center") out.push(centerCaret);
|
||
if (g.richTextCopy) out.push(richCopy);
|
||
if (!o.readOnly && (k.reformatOnPaste === "indentBlock" || k.reformatOnPaste === "indentEachLine")) out.push(pasteReindentFilter(k.reformatOnPaste));
|
||
|
||
// Pairing: closeBrackets() reads the brackets list from language data; ours comes first.
|
||
const brackets = [...(k.pairBrackets ? ["(", "[", "{"] : []), ...(k.pairQuotes ? ["'", '"'] : [])];
|
||
out.push(Prec.highest(EditorState.languageData.of(() => [{ closeBrackets: { brackets } }])));
|
||
if (!k.surroundSelection) out.push(Prec.highest(noSurround));
|
||
if (k.pairQuotes) out.push(Prec.highest(skipClosingQuote));
|
||
if (k.tabOutOfPair) out.push(tabOut);
|
||
if (o.commentStyle) out.push(lineCommentStyle.of(o.commentStyle));
|
||
|
||
const keys = [];
|
||
if (k.camelHumps)
|
||
keys.push(
|
||
{ key: "Alt-ArrowLeft", run: cursorHumpLeft, shift: selectHumpLeft, preventDefault: true },
|
||
{ key: "Alt-ArrowRight", run: cursorHumpRight, shift: selectHumpRight, preventDefault: true },
|
||
{ key: "Alt-Delete", run: deleteHumpForward },
|
||
);
|
||
// Delete to Word Start: the whitespace before the caret and the word before it.
|
||
keys.push({ key: "Mod-Backspace", mac: "Alt-Backspace", run: k.camelHumps ? deleteHumpBackward : deleteWordBackward });
|
||
if (!k.smartHome)
|
||
keys.push(
|
||
{ key: "Home", run: cursorLineStart, shift: selectLineStart, preventDefault: true },
|
||
{ mac: "Mod-ArrowLeft", run: cursorLineStart, shift: selectLineStart, preventDefault: true },
|
||
);
|
||
if (!o.readOnly) keys.push({ key: "Enter", run: enterInLineComment });
|
||
if (!k.smartIndent) keys.push({ key: "Enter", run: insertNewlineKeepIndent });
|
||
if (k.backspaceUnindent === "none") keys.push({ key: "Backspace", run: deleteCharBackwardStrict });
|
||
else if (smartBackspaceApplies(k.backspaceUnindent, o.language)) keys.push({ key: "Backspace", run: smartBackspace });
|
||
if (keys.length) out.push(Prec.high(keymap.of(keys)));
|
||
return out;
|
||
}
|