diff --git a/packages/codemirror/src/extensions/custom_tab.ts b/packages/codemirror/src/extensions/custom_tab.ts
index 9ba602010..2b93d7678 100644
--- a/packages/codemirror/src/extensions/custom_tab.ts
+++ b/packages/codemirror/src/extensions/custom_tab.ts
@@ -2,20 +2,41 @@ import { indentLess, indentMore } from "@codemirror/commands";
import { EditorSelection, type ChangeSpec } from "@codemirror/state";
import type { KeyBinding } from "@codemirror/view";
+/**
+ * Custom key binding for indentation:
+ *
+ * - Tab while at the beginning of a line will indent the line.
+ * - Tab while not at the beginning of a line will insert a tab character.
+ * - Tab while not at the beginning of a line while text is selected will replace the txt with a tab character.
+ * - Shift+Tab will always unindent.
+ */
const smartIndentWithTab: KeyBinding[] = [
{
key: "Tab",
run({ state, dispatch }) {
const { selection } = state;
-
- // Handle selection indenting normally
- if (selection.ranges.some(range => !range.empty)) {
- return indentMore({ state, dispatch });
- }
-
const changes = [];
const newSelections = [];
+ // Step 1: Handle non-empty selections → replace with tab
+ if (selection.ranges.some(range => !range.empty)) {
+ for (let range of selection.ranges) {
+ changes.push({ from: range.from, to: range.to, insert: "\t" });
+ newSelections.push(EditorSelection.cursor(range.from + 1));
+ }
+
+ dispatch(
+ state.update({
+ changes,
+ selection: EditorSelection.create(newSelections),
+ scrollIntoView: true,
+ userEvent: "input"
+ })
+ );
+ return true;
+ }
+
+ // Step 2: Handle empty selections
for (let range of selection.ranges) {
const line = state.doc.lineAt(range.head);
const beforeCursor = state.doc.sliceString(line.from, range.head);
@@ -24,7 +45,7 @@ const smartIndentWithTab: KeyBinding[] = [
// Only whitespace before cursor → indent line
return indentMore({ state, dispatch });
} else {
- // Insert a tab character at cursor
+ // Insert tab character at cursor
changes.push({ from: range.head, to: range.head, insert: "\t" });
newSelections.push(EditorSelection.cursor(range.head + 1));
}