Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/app-core/src/lib/cm-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,29 @@ describe('toggleWrap', () => {
expect(view.state.selection.main.head).toBe(3) // between == and ==
})

it('moves past the closing marker when toggling off an active empty-selection wrap', () => {
const view = mount('**bold** text', 6, 6) // cursor before closing **
toggleWrap(view, '**')
expect(view.state.doc.toString()).toBe('**bold** text')
expect(view.state.selection.main.empty).toBe(true)
expect(view.state.selection.main.head).toBe(8)
})

it('removes an untouched empty pair when pressing the shortcut again', () => {
const view = mount('****', 2, 2) // between the opening and closing **
toggleWrap(view, '**')
expect(view.state.doc.toString()).toBe('')
expect(view.state.selection.main.empty).toBe(true)
expect(view.state.selection.main.head).toBe(0)
})

it('does not treat a cursor before a later opening marker as active formatting', () => {
const view = mount('**done** **next**', 9, 9) // cursor before the second opening **
toggleWrap(view, '**')
expect(view.state.doc.toString()).toBe('**done** ******next**')
expect(view.state.selection.main.head).toBe(11)
})

it('works for the other markers', () => {
for (const [marker, expected] of [
['~~', 'a ~~b~~ c'],
Expand Down
42 changes: 42 additions & 0 deletions packages/app-core/src/lib/cm-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@
import { EditorSelection } from '@codemirror/state'
import type { EditorView } from '@codemirror/view'

function isInsideUnclosedMarker(text: string, marker: string): boolean {
let count = 0
let index = 0
while (index < text.length) {
const found = text.indexOf(marker, index)
if (found === -1) break
if (
marker !== '*' ||
(text[found - 1] !== '*' && text[found + marker.length] !== '*')
) {
count++
}
index = found + marker.length
}
return count % 2 === 1
}

/**
* Toggle a symmetric inline marker around each selection range: wrap when it
* isn't wrapped, unwrap when the markers already sit just outside (or just
Expand All @@ -18,6 +35,31 @@ export function toggleWrap(view: EditorView, marker: string): boolean {
view.state.changeByRange((range) => {
const { from, to } = range
if (from === to) {
const before = view.state.sliceDoc(Math.max(0, from - m.length), from)
const after = view.state.sliceDoc(from, Math.min(view.state.doc.length, from + m.length))

if (after === m) {
if (before === m) {
// Empty pair: pressing the shortcut again removes the markers.
return {
changes: { from: from - m.length, to: from + m.length, insert: '' },
range: EditorSelection.cursor(from - m.length)
}
}

const line = view.state.doc.lineAt(from)
const lineBefore = view.state.sliceDoc(line.from, from)
if (isInsideUnclosedMarker(lineBefore, m)) {
// Cursor is just before the closing marker from a previously inserted
// pair. Treat the shortcut as leaving/toggling off formatting instead
// of inserting another marker pair inside it.
return {
changes: [],
range: EditorSelection.cursor(from + m.length)
}
}
}

// No selection: insert the pair and drop the cursor between them.
return {
changes: { from, insert: m + m },
Expand Down