Skip to content

Commit

Permalink
Handle tab-indentation better
Browse files Browse the repository at this point in the history
FIX: Make `insertNewlineContinueMarkup` and `deleteMarkupBackward`
use tabs for indentation when appropriate.

Closes codemirror/dev#1243
  • Loading branch information
marijnh committed Sep 1, 2023
1 parent 6c7daa5 commit 758fea8
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 16 deletions.
38 changes: 27 additions & 11 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {StateCommand, Text, EditorSelection, ChangeSpec} from "@codemirror/state"
import {syntaxTree} from "@codemirror/language"
import {StateCommand, Text, EditorState, EditorSelection, ChangeSpec, countColumn} from "@codemirror/state"
import {syntaxTree, indentUnit} from "@codemirror/language"
import {SyntaxNode, Tree} from "@lezer/common"
import {markdownLanguage} from "./markdown"

Expand Down Expand Up @@ -43,15 +43,15 @@ function getContext(node: SyntaxNode, doc: Text) {
let line = doc.lineAt(node.from), startPos = node.from - line.from
if (node.name == "FencedCode") {
context.push(new Context(node, startPos, startPos, "", "", "", null))
} else if (node.name == "Blockquote" && (match = /^[ \t]*>( ?)/.exec(line.text.slice(startPos)))) {
} else if (node.name == "Blockquote" && (match = /^ *>( ?)/.exec(line.text.slice(startPos)))) {
context.push(new Context(node, startPos, startPos + match[0].length, "", match[1], ">", null))
} else if (node.name == "ListItem" && node.parent!.name == "OrderedList" &&
(match = /^([ \t]*)\d+([.)])([ \t]*)/.exec(line.text.slice(startPos)))) {
(match = /^( *)\d+([.)])( *)/.exec(line.text.slice(startPos)))) {
let after = match[3], len = match[0].length
if (after.length >= 4) { after = after.slice(0, after.length - 4); len -= 4 }
context.push(new Context(node.parent!, startPos, startPos + len, match[1], after, match[2], node))
} else if (node.name == "ListItem" && node.parent!.name == "BulletList" &&
(match = /^([ \t]*)([-+*])([ \t]{1,4}\[[ xX]\])?([ \t]+)/.exec(line.text.slice(startPos)))) {
(match = /^( *)([-+*])( {1,4}\[[ xX]\])?( +)/.exec(line.text.slice(startPos)))) {
let after = match[4], len = match[0].length
if (after.length > 4) { after = after.slice(0, after.length - 4); len -= 4 }
let type = match[2]
Expand Down Expand Up @@ -83,6 +83,18 @@ function renumberList(after: SyntaxNode, doc: Text, changes: ChangeSpec[], offse
}
}

function normalizeIndent(content: string, state: EditorState) {
let blank = /^[ \t]*/.exec(content)![0].length
if (!blank || state.facet(indentUnit) != "\t") return content
let col = countColumn(content, 4, blank)
let space = ""
for (let i = col; i > 0;) {
if (i >= 4) { space += "\t"; i -= 4 }
else { space += " "; i-- }
}
return space + content.slice(blank)
}

/// This command, when invoked in Markdown context with cursor
/// selection(s), will create a new line with the markup for
/// blockquotes and lists that were active on the old line. If the
Expand Down Expand Up @@ -124,9 +136,9 @@ export const insertNewlineContinueMarkup: StateCommand = ({state, dispatch}) =>
} else { // Move this line down
let insert = ""
for (let i = 0, e = context.length - 2; i <= e; i++) {
insert += context[i].blank(i < e ? context[i + 1].from - insert.length : null, i < e)
insert += context[i].blank(i < e ? countColumn(line.text, 4, context[i + 1].from) - insert.length : null, i < e)
}
insert += state.lineBreak
insert = normalizeIndent(insert + state.lineBreak, state)
return {range: EditorSelection.cursor(pos + insert.length), changes: {from: line.from, insert}}
}
}
Expand All @@ -149,12 +161,12 @@ export const insertNewlineContinueMarkup: StateCommand = ({state, dispatch}) =>
if (!continued || /^[\s\d.)\-+*>]*/.exec(line.text)![0].length >= inner.to) {
for (let i = 0, e = context.length - 1; i <= e; i++) {
insert += i == e && !continued ? context[i].marker(doc, 1)
: context[i].blank(i < e ? context[i + 1].from - insert.length : null)
: context[i].blank(i < e ? countColumn(line.text, 4, context[i + 1].from) - insert.length : null)
}
}
let from = pos
while (from > line.from && /\s/.test(line.text.charAt(from - line.from - 1))) from--
insert = state.lineBreak + insert
insert = state.lineBreak + normalizeIndent(insert, state)
changes.push({from, to: pos, insert})
return {range: EditorSelection.cursor(from + insert.length), changes}
})
Expand Down Expand Up @@ -216,8 +228,12 @@ export const deleteMarkupBackward: StateCommand = ({state, dispatch}) => {
(!inner.item || line.from <= inner.item.from || !/\S/.test(line.text.slice(0, inner.to)))) {
let start = line.from + inner.from
// Replace a list item marker with blank space
if (inner.item && inner.node.from < inner.item.from && /\S/.test(line.text.slice(inner.from, inner.to)))
return {range, changes: {from: start, to: line.from + inner.to, insert: inner.blank(inner.to - inner.from)}}
if (inner.item && inner.node.from < inner.item.from && /\S/.test(line.text.slice(inner.from, inner.to))) {
let insert = inner.blank(countColumn(line.text, 4, inner.to) - countColumn(line.text, 4, inner.from))
if (start == line.from) insert = normalizeIndent(insert, state)
return {range: EditorSelection.cursor(start + insert.length),
changes: {from: start, to: line.from + inner.to, insert}}
}
// Delete one level of indentation
if (start < pos)
return {range: EditorSelection.cursor(start), changes: {from: start, to: pos}}
Expand Down
24 changes: 19 additions & 5 deletions test/test-commands.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {EditorState, EditorSelection, StateCommand} from "@codemirror/state"
import {EditorState, EditorSelection, StateCommand, Extension} from "@codemirror/state"
import {markdown, deleteMarkupBackward, insertNewlineContinueMarkup} from "@codemirror/lang-markdown"
import {indentUnit} from "@codemirror/language"
import ist from "ist"

function mkState(doc: string) {
function mkState(doc: string, extension?: Extension) {
let cursors = []
for (let pos = 0;;) {
pos = doc.indexOf("|", pos)
Expand All @@ -13,7 +14,7 @@ function mkState(doc: string) {
return EditorState.create({
doc,
selection: cursors.length ? EditorSelection.create(cursors) : undefined,
extensions: [markdown().language, EditorState.allowMultipleSelections.of(true)]
extensions: [markdown().language, EditorState.allowMultipleSelections.of(true), extension || []]
})
}

Expand All @@ -26,13 +27,17 @@ function stateStr(state: EditorState) {
return doc
}

const tabs: Extension = [EditorState.tabSize.of(4), indentUnit.of("\t")]

function cmd(state: EditorState, command: StateCommand) {
command({state, dispatch(tr) { state = tr.state }})
return state
}

describe("insertNewlineContinueMarkup", () => {
function test(from: string, to: string) { ist(stateStr(cmd(mkState(from), insertNewlineContinueMarkup)), to) }
function test(from: string, to: string, ext?: Extension) {
ist(stateStr(cmd(mkState(from, ext), insertNewlineContinueMarkup)), to)
}

it("doesn't continue anything at the top level", () =>
test("one|", "one|"))
Expand Down Expand Up @@ -144,10 +149,16 @@ describe("insertNewlineContinueMarkup", () => {
test("- [ ] item 1\n - [ ] item 1.1\n - [ ] item 1.1.1|",
"- [ ] item 1\n - [ ] item 1.1\n - [ ] item 1.1.1\n - [ ] |")
})

it("handles tab-indentation", () => {
test(" - one\n\t- two|", " - one\n\t- two\n\t- |", tabs)
})
})

describe("deleteMarkupBackward", () => {
function test(from: string, to: string) { ist(stateStr(cmd(mkState(from), deleteMarkupBackward)), to) }
function test(from: string, to: string, ext?: Extension) {
ist(stateStr(cmd(mkState(from, ext), deleteMarkupBackward)), to)
}

it("does nothing in regular text", () =>
test("one|", "one|"))
Expand Down Expand Up @@ -193,4 +204,7 @@ describe("deleteMarkupBackward", () => {

it("doesn't delete normal text in continued list items", () =>
test("- \na |b", "- \na |b"))

it("normalizes whitespace on deleting", () =>
test(" - one\n - |", " - one\n\t|", tabs))
})

0 comments on commit 758fea8

Please sign in to comment.