However, in order to match the content it’s for, I need spaces instead of tabs, so I added indentUnits.of(" ") and EditorState.tabSize.of(4) statements, but pressing tab in the editor still writes a \t instead of the four spaces I told it to.
Looking at the insertTab command source code, it feels like there’s a bug here?
/// Insert a tab character at the cursor or, if something is selected,
/// use [`indentMore`](#commands.indentMore) to indent the entire
/// selection.
export const insertTab: StateCommand = ({state, dispatch}) => {
if (state.selection.ranges.some(r => !r.empty)) return indentMore({state, dispatch})
dispatch(state.update(state.replaceSelection("\t"), {scrollIntoView: true, userEvent: "input"}))
return true
}
Unless I’m reading this wrong, this code ignores whatever was specified via indentUnits and instead of uses a hardcoded tab code?
(and if this is indeed a bug, I don’t know if that means that indentLess needs a fix, too)
Is there a way to tell indentMore to just insert the spaces at the cursor, when in the middle of a line rather than in leading whitespace? (when there is no selection)
Fair enough. In case it’s useful for other folks who happen across this because of google searching or whatnot, I ended up with this:
const INDENT_STRING = ` `;
const TAB_NOTICE_TEXT =
`Using tab for code indentation. To tab out of the editor, press escape first.`;
let bypassTabs = false;
extensions.push(
indentUnit.of(INDENT_STRING),
EditorState.tabSize.of(INDENT_STRING.length),
keymap.of([
{
key: `Escape`,
run: () => {
bypassTabs = true;
new Notice(
`Escaping the editor: pressing tab will now focus on the next focusable element on the page.`
);
},
},
{
key: `Tab`,
preventDefault: true,
run: (view) => {
if (bypassTabs) return (bypassTabs = false);
createOneTimeNotice(TAB_NOTICE_TEXT, 5000);
// Multi line selection = indent
const { doc, selection } = view.state;
const { ranges } = selection;
const aLine = doc.lineAt(ranges.at(0).from);
const hLine = doc.lineAt(ranges.at(-1).to);
const multiline = aLine.number !== hLine.number;
if (multiline) return indentMore(view);
// single line: do we indent, or insert/replace spaces?
const pos = selection.main.head;
const { from, to } = ranges[0];
// scoped helper function for single line "add spaces somewhere":
const indent = (from, to = from) => {
view.dispatch({
changes: { from, to, insert: INDENT_STRING },
selection: { anchor: from + INDENT_STRING.length },
});
return true;
};
// text selection = replace with spaces
if (from !== to) return indent(from, to);
// Anything else = insert spaces
return indent(pos);
},
},
{
key: `Shift-Tab`,
preventDefault: true,
run: (view) => {
if (bypassTabs) return (bypassTabs = false);
createOneTimeNotice(TAB_NOTICE_TEXT, 5000);
return indentLess(view);
},
},
])
With the note that this has an “escape” for users who rely on tab to navigate pages. Pressing esc acts as a one-time escape so you can tab or shift-tab out. When you shift-tab or tab back in, you’ll be trapped again (until the next esc).
I started from your solution, but I think the number of spaces should vary based on the column number each cursor is at. So I rewrote it to the following:
import { indentMore, indentLess } from '@codemirror/commands'
import { getIndentUnit } from '@codemirror/language'
import { EditorSelection } from '@codemirror/state'
/** @import { Command, KeyBinding } from '@codemirror/view' */
/** @type {Command} */
function indentMoreOrInsertTab(view) {
const { doc, selection } = view.state
const { ranges } = selection
if (ranges.length == 0) {
// cannot continue if having no selections
return false
}
/** @type {Map<number, number>} */
const colNoMap = new Map()
const hasMultilineSelection = ranges.some(range => {
const line = doc.lineAt(range.from)
colNoMap.set(range.from, range.from - line.from)
return (line.number !== doc.lineAt(range.to).number)
})
if (hasMultilineSelection) {
return indentMore(view)
}
// else the mapped will contain info about all ranges
const indentUnit = getIndentUnit(view.state)
// TODO: now assuming it's true
// const useSoftTab = true
const tx = view.state.changeByRange(range => {
const { from, to } = range
const col = /** @type {number} */(colNoMap.get(from))
const count = indentUnit - (col % indentUnit)
const changes = { from: from, to, insert: ' '.repeat(count) }
return {
changes,
range: EditorSelection.cursor(from + count),
}
})
view.dispatch(tx)
return true
}
/** @type {KeyBinding} */
export const myIndentWithTab = {
key: 'Tab',
run: indentMoreOrInsertTab,
shift: indentLess,
}