| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- import { expect } from 'chai'
- import { EditorState, Text } from '@codemirror/state'
- import { LaTeXLanguage } from '../../../../../../frontend/js/features/source-editor/languages/latex/latex-language'
- import {
- ensureSyntaxTree,
- foldNodeProp,
- LanguageSupport,
- } from '@codemirror/language'
- import { EditorView } from '@codemirror/view'
- const latex = new LanguageSupport(LaTeXLanguage)
- const makeView = (lines: string[]): EditorView => {
- const text = Text.of(lines)
- const view = new EditorView({
- state: EditorState.create({
- doc: text,
- extensions: [latex],
- }),
- })
- return view
- }
- type Fold = { from: number; to: number }
- const _getFolds = (view: EditorView) => {
- const ranges: Fold[] = []
- const tree = ensureSyntaxTree(view.state, view.state.doc.length)
- if (!tree) {
- throw new Error("Couldn't get Syntax Tree")
- }
- tree.iterate({
- enter: nodeRef => {
- const prop = nodeRef.type.prop(foldNodeProp)
- if (prop) {
- const hasFold = prop(nodeRef.node, view.state)
- if (hasFold) {
- ranges.push({ from: hasFold.from, to: hasFold.to })
- }
- }
- },
- })
- return ranges
- }
- describe('CodeMirror LaTeX-folding', function () {
- describe('With empty document', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = ['']
- view = makeView(content)
- })
- it('should not produce any folds', function () {
- const folds = _getFolds(view)
- expect(folds).to.be.empty
- })
- })
- describe('Sectioning command folding', function () {
- describe('with no foldable sections', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = ['hello', 'test']
- view = makeView(content)
- })
- it('should not produce any folds', function () {
- const folds = _getFolds(view)
- expect(folds).to.be.empty
- })
- })
- describe('with one foldable section', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = ['hello', '\\section{one}', 'a', 'b', 'c']
- view = makeView(content)
- })
- it('should produce one fold', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(1)
- })
- it('should fold from the section line to last line', function () {
- const folds = _getFolds(view)
- const fold = folds[0]
- expect(view.state.doc.lineAt(fold.from).number).to.equal(2)
- expect(view.state.doc.lineAt(fold.to).number).to.equal(
- view.state.doc.lines
- )
- })
- })
- describe('with two foldable sections', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = [
- 'hello',
- '\\section{one}',
- 'a',
- 'b',
- '\\section{two}',
- 'c',
- 'd',
- ]
- view = makeView(content)
- })
- it('should produce two folds', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(2)
- expect(view.state.doc.lineAt(folds[0].from).number).to.equal(2)
- expect(view.state.doc.lineAt(folds[0].to).number).to.equal(4)
- expect(view.state.doc.lineAt(folds[1].from).number).to.equal(5)
- expect(view.state.doc.lineAt(folds[1].to).number).to.equal(
- view.state.doc.lines
- )
- })
- })
- describe('with a comment line before an indented sectioning command', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- // Reproduces a bug where a comment line followed by an indented
- // `\subsection` should not pull the next sectioning command's line
- // into the previous fold.
- content = [
- '\\section{1}',
- ' Content',
- ' \\subsection{1a}',
- ' Content',
- ' %',
- ' \\subsection{1b}',
- ' Content',
- ]
- view = makeView(content)
- })
- it('should fold subsection 1a up to the comment line, leaving subsection 1b on its own line', function () {
- const folds = _getFolds(view)
- const subsection1aFold = folds.find(
- fold => view.state.doc.lineAt(fold.from).number === 3
- )
- expect(subsection1aFold).not.to.be.undefined
- // The fold for subsection 1a should end on (or before) line 5 so that
- // line 6 (subsection 1b) can be folded independently.
- expect(
- view.state.doc.lineAt(subsection1aFold!.to).number
- ).to.be.at.most(5)
- })
- it('should still produce a separate fold for subsection 1b', function () {
- const folds = _getFolds(view)
- const subsection1bFold = folds.find(
- fold => view.state.doc.lineAt(fold.from).number === 6
- )
- expect(subsection1bFold).not.to.be.undefined
- })
- })
- describe('with an indented sectioning command after a blank line', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- // The folding algorithm should not consider whitespace on the line
- // before an indented sectioning command as part of the previous
- // section's fold.
- content = ['\\section{1}', 'Content', '', ' \\section{2}', 'Content']
- view = makeView(content)
- })
- it('should not include the indented sectioning command line in the previous fold', function () {
- const folds = _getFolds(view)
- const firstFold = folds.find(
- fold => view.state.doc.lineAt(fold.from).number === 1
- )
- expect(firstFold).not.to.be.undefined
- expect(view.state.doc.lineAt(firstFold!.to).number).to.be.at.most(3)
- })
- })
- describe('with a complex mix of comments, blank lines and indented sectioning commands', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- // Combines all the tricky cases:
- // - trailing `%` comment lines before indented and non-indented
- // sectioning commands
- // - blank lines (with and without trailing whitespace) before indented
- // sectioning commands
- // - multiple levels of (sub)sectioning at varying indentation
- content = [
- '\\section{1}',
- ' Content',
- ' \\subsection{1a}',
- ' Content a',
- ' %',
- ' \\subsubsection{1a1a}',
- ' Hello',
- ' %',
- '\\subsection{1b}',
- 'Content b',
- '',
- ' \\subsection{1c}',
- ' Content c',
- '',
- ' \\subsubsection{1c1a}',
- ' World',
- ' ',
- ' \\subsubsection{1c1b}',
- ' New ',
- ' ',
- ]
- view = makeView(content)
- })
- it('should produce a separate fold for each sectioning command', function () {
- const folds = _getFolds(view)
- const fromLines = folds
- .map(fold => view.state.doc.lineAt(fold.from).number)
- .sort((a, b) => a - b)
- // One fold per sectioning command line
- expect(fromLines).to.deep.equal([1, 3, 6, 9, 12, 15, 18])
- })
- it('should not pull a sibling sectioning command line into the previous fold', function () {
- const folds = _getFolds(view)
- const foldByFromLine = new Map<number, Fold>()
- for (const fold of folds) {
- foldByFromLine.set(view.state.doc.lineAt(fold.from).number, fold)
- }
- // Each subsection 1a / 1b / 1c sibling fold must end before the next
- // subsection's line so the next subsection can be folded independently.
- const siblingPairs: Array<[number, number]> = [
- [3, 9], // \subsection{1a} fold must end before line 9 (\subsection{1b})
- [9, 12], // \subsection{1b} fold must end before line 12 (\subsection{1c})
- [15, 18], // \subsubsection{1c1a} fold must end before line 18 (\subsubsection{1c1b})
- ]
- for (const [fromLine, nextFromLine] of siblingPairs) {
- const fold = foldByFromLine.get(fromLine)
- expect(fold, `fold starting on line ${fromLine}`).not.to.be.undefined
- expect(
- view.state.doc.lineAt(fold!.to).number,
- `fold starting on line ${fromLine} should end before line ${nextFromLine}`
- ).to.be.lessThan(nextFromLine)
- }
- })
- })
- describe('with realistic nesting', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = [
- 'hello',
- '\\chapter{1}',
- ' a',
- ' \\section{1.1}',
- ' a',
- ' \\subsection{1.1.1}',
- ' a',
- ' \\section{1.2}',
- ' a',
- ' \\subsection{1.2.1}',
- ' a',
- '\\chapter{2}',
- ' a',
- ' \\section{2.1}',
- ' a',
- ' \\section{2.2}',
- ' a',
- ]
- view = makeView(content)
- })
- it('should produce many folds', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(8)
- const foldDescriptions = folds.map(fold => {
- const fromLine = view.state.doc.lineAt(fold.from).number
- const toLine = view.state.doc.lineAt(fold.to).number
- return { fromLine, toLine }
- })
- expect(foldDescriptions).to.deep.equal([
- { fromLine: 2, toLine: 11 },
- { fromLine: 4, toLine: 7 },
- { fromLine: 6, toLine: 7 },
- { fromLine: 8, toLine: 11 },
- { fromLine: 10, toLine: 11 },
- { fromLine: 12, toLine: 17 },
- { fromLine: 14, toLine: 15 },
- { fromLine: 16, toLine: 17 },
- ])
- })
- })
- })
- describe('Environment folding', function () {
- describe('with single environment', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = ['\\begin{foo}', 'content', '\\end{foo}']
- view = makeView(content)
- })
- it('should fold the environment', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(1)
- expect(folds).to.deep.equal([{ from: 11, to: 20 }])
- })
- })
- describe('with nested environment', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = [
- '\\begin{foo}',
- '\\begin{bar}',
- 'content',
- '\\end{bar}',
- '\\end{foo}',
- ]
- view = makeView(content)
- })
- it('should fold the environment', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(2)
- expect(folds).to.deep.equal([
- { from: 11, to: 42 },
- { from: 23, to: 32 },
- ])
- })
- })
- })
- describe('Comment folding', function () {
- describe('with a single set of comments', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = ['Hello', '% {', 'this is folded', '% }', 'End']
- view = makeView(content)
- })
- it('should fold the region marked by comments', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(1)
- expect(folds).to.deep.equal([{ from: 9, to: 27 }])
- })
- })
- describe('with several sets of comments', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = [
- 'Hello',
- '% {',
- 'this is folded',
- '% }',
- '',
- '% {',
- 'and this also',
- '% }',
- 'End',
- ]
- view = makeView(content)
- })
- it('should fold both regions marked by comments', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(2)
- expect(folds).to.deep.equal([
- { from: 9, to: 27 },
- { from: 33, to: 50 },
- ])
- })
- })
- describe('with nested sets of comments', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = [
- 'Hello',
- '% {',
- 'one',
- '% {',
- 'two',
- '% {',
- 'three',
- '% }',
- 'two',
- '% }',
- 'one',
- '% }',
- 'End',
- ]
- view = makeView(content)
- })
- it('should fold all the regions marked by comments, with nesting', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(3)
- expect(folds).to.deep.equal([
- { from: 9, to: 50 },
- { from: 17, to: 42 },
- { from: 25, to: 34 },
- ])
- })
- })
- describe('with fold comment spanning entire document', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = ['% {', 'Hello', '% }']
- view = makeView(content)
- })
- it('should fold', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(1)
- expect(folds).to.deep.equal([{ from: 3, to: 12 }])
- })
- })
- describe('with fold comment at start of document', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = ['% {', 'Hello', '% }', 'Test']
- view = makeView(content)
- })
- it('should fold', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(1)
- expect(folds).to.deep.equal([{ from: 3, to: 12 }])
- })
- })
- describe('with fold comment at end of document', function () {
- let view: EditorView, content: string[]
- beforeEach(function () {
- content = ['Test', '% {', 'Hello', '% }']
- view = makeView(content)
- })
- it('should fold', function () {
- const folds = _getFolds(view)
- expect(folds.length).to.equal(1)
- expect(folds).to.deep.equal([{ from: 8, to: 17 }])
- })
- })
- })
- })
|