import ist from "ist" import { SelectionRange, EditorState, EditorSelection, Extension, StateCommand } from "@codemirror/next/state" import { Text } from "@codemirror/next/text" import { toggleLineComment, getLinesInRange, CommentTokens, toggleBlockComment } from "@codemirror/next/comment" import { html } from "@codemirror/next/lang-html" describe("comment", () => { it("get lines across range", () => { // 0 1 2 3 // 0123456 7890123 4567890 1234567 8901234 5 let doc = Text.of("Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n\n".split("\n")) const t = (from: number, to: number, expectedLinesNo: number[]) => { let lines = getLinesInRange(doc, new SelectionRange(from, to)) ist(lines.map(l => l.start).join(","), expectedLinesNo.join(",")) } t(0, 0, [0]) t(7, 7, [7]) t(16, 16, [14]) t(0, 35, [0, 7, 14, 21, 28, 35]) t(0, 6, [0]) t(4, 8, [0, 7]) t(3, 17, [0, 7, 14]) }) const defaultConfig: CommentTokens = {line: "//", block: {open: "/*", close: "*/"}} /// Creates a new `EditorState` using `doc` as the document text. /// The selection ranges in the returned state can be specified /// within the `doc` argument: /// The character `|` is used a marker to indicate both the /// start and the end of a `SelectionRange`, *e.g.*, /// /// ```typescript /// s("line 1\nlin|e 2\nline 3") /// ``` function s(doc: string, config: CommentTokens = defaultConfig, extensions: readonly Extension[] = []): EditorState { let markers = [], pos while ((pos = doc.indexOf("|", 0)) >= 0) { markers.push(pos) doc = doc.slice(0, pos) + doc.slice(pos + 1) } const ranges: SelectionRange[] = [] if (markers.length == 1) { ranges.push(new SelectionRange(markers[0])) } else if (markers.length % 2 != 0) { throw "Markers for multiple selections need to be even."; } else { for (let i = 0; i < markers.length; i += 2) ranges.push(new SelectionRange(markers[i], markers[i + 1])) if (ranges.length == 0) ranges.push(new SelectionRange(0)) } return EditorState.create({ doc, selection: EditorSelection.create(ranges), extensions: [EditorState.allowMultipleSelections.of(true), EditorState.addLanguageData.of({commentTokens: config})].concat(extensions) }) } function same(actualState: EditorState, expectedState: EditorState) { ist(actualState.doc.toString(), expectedState.doc.toString()) ist(JSON.stringify(actualState.selection), JSON.stringify(expectedState.selection)) } function checkToggleChain(toggle: StateCommand, config: CommentTokens, docs: string[]) { let state = s(docs[0], config) for (let i = 1; i <= docs.length; i++) { toggle({state, dispatch(tr) { state = tr.apply() }}) same(state, s(docs[i == docs.length ? docs.length - 2 : i], config)) } } // Runs all tests for the given line-comment token, `k`. function runLineCommentTests(k: string) { function check(...docs: string[]) { checkToggleChain(toggleLineComment, {line: k}, docs) } describe(`Line comments ('${k}')`, () => { it("toggles in an empty single selection", () => { check(`\nline 1\n ${k} ${k} ${k} ${k}line| 2\nline 3\n`, `\nline 1\n ${k} ${k} ${k}line| 2\nline 3\n`, `\nline 1\n ${k} ${k}line| 2\nline 3\n`, `\nline 1\n ${k}line| 2\nline 3\n`, `\nline 1\n line| 2\nline 3\n`, `\nline 1\n ${k} line| 2\nline 3\n`) check(`\nline 1\n ${k}line 2|\nline 3\n`, `\nline 1\n line 2|\nline 3\n`, `\nline 1\n ${k} line 2|\nline 3\n`) check(`\nline 1\n| ${k}line 2\nline 3\n`, `\nline 1\n| line 2\nline 3\n`, `\nline 1\n| ${k} line 2\nline 3\n`) check(`\nline 1\n|${k}\nline 3\n`, `\nline 1\n|\nline 3\n`, `\nline 1\n|${k} \nline 3\n`) check(`\nline 1\n line 2\nline 3\n|${k}`, `\nline 1\n line 2\nline 3\n|`, `\nline 1\n line 2\nline 3\n|${k} `) }) it("toggles comments in a single line when the cursor is at the beginning", () => { check(`line 1\n |line 2\nline 3\n`, `line 1\n |${k} line 2\nline 3\n`) }) it("toggles comments in a single line selection", () => { check(`line 1\n ${k}li|ne |2\nline 3\n`, `line 1\n li|ne |2\nline 3\n`, `line 1\n ${k} li|ne |2\nline 3\n`) }) it("toggles comments in a multi-line selection", () => { check(`\n ${k}lin|e 1\n ${k} line 2\n ${k} line |3\n`, `\n lin|e 1\n line 2\n line |3\n`, `\n ${k} lin|e 1\n ${k} line 2\n ${k} line |3\n`) check(`\n ${k}lin|e 1\n ${k} line 2\n line 3\n ${k} li|ne 4\n`, `\n ${k} ${k}lin|e 1\n ${k} ${k} line 2\n ${k} line 3\n ${k} ${k} li|ne 4\n`) check(`\n ${k} lin|e 1\n\n ${k} line |3\n`, `\n lin|e 1\n\n line |3\n`) check(`\n ${k} lin|e 1\n \n ${k} line |3\n`, `\n lin|e 1\n \n line |3\n`) check(`\n|\n ${k} line 2\n | \n`, `\n|\n line 2\n | \n`) check(`\n|\n\n | \n`, `\n|\n\n | \n`) }) it("toggles comments in a multi-line multi-range selection", () => { check(`\n lin|e 1\n line |2\n line 3\n l|ine 4\n line| 5\n`, `\n ${k} lin|e 1\n ${k} line |2\n line 3\n ${k} l|ine 4\n ${k} line| 5\n`) }) }) } /// Runs all tests for the given block-comment tokens. function runBlockCommentTests(o: string, c: string) { describe(`Block comments ('${o} ${c}')`, () => { function check(...docs: string[]) { checkToggleChain(toggleBlockComment, {block: {open: o, close: c}}, docs) } it("toggles block comment in multi-line selection", () => { check(`\n lin|e 1\n line 2\n line 3\n line |4\n line 5\n`, `\n lin${o} |e 1\n line 2\n line 3\n line | ${c}4\n line 5\n`) }) it("toggles block comment in multi-line multi-range selection", () => { check(`\n lin|e 1\n line |2\n l|ine 3\n line 4\n line |5\n`, `\n lin${o} |e 1\n line | ${c}2\n l${o} |ine 3\n line 4\n line | ${c}5\n`) }) it("can toggle comments inside the selection", () => { check(`|${o} one\ntwo ${c}| three`, `|one\ntwo| three`, `${o} |one\ntwo| ${c} three`) }) }) } runLineCommentTests("//") runLineCommentTests("#") runBlockCommentTests("/*", "*/") runBlockCommentTests("") it("toggles line comment in multi-language doc", () => { let state = s(` `, undefined, [html()]) toggleLineComment({state, dispatch(tr) { state = tr.apply() }}) same(state, s(` `)) }) })