diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee deleted file mode 100644 index 68d0f7b09..000000000 --- a/spec/language-mode-spec.coffee +++ /dev/null @@ -1,506 +0,0 @@ -describe "LanguageMode", -> - [editor, buffer, languageMode] = [] - - afterEach -> - editor.destroy() - - describe "javascript", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".minIndentLevelForRowRange(startRow, endRow)", -> - it "returns the minimum indent level for the given row range", -> - expect(languageMode.minIndentLevelForRowRange(4, 7)).toBe 2 - expect(languageMode.minIndentLevelForRowRange(5, 7)).toBe 2 - expect(languageMode.minIndentLevelForRowRange(5, 6)).toBe 3 - expect(languageMode.minIndentLevelForRowRange(9, 11)).toBe 1 - expect(languageMode.minIndentLevelForRowRange(10, 10)).toBe 0 - - describe ".toggleLineCommentsForBufferRows(start, end)", -> - it "comments/uncomments lines in the given range", -> - languageMode.toggleLineCommentsForBufferRows(4, 7) - expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " // current = items.shift();" - expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " // }" - - languageMode.toggleLineCommentsForBufferRows(4, 5) - expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " current = items.shift();" - expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " // }" - - buffer.setText('\tvar i;') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "\t// var i;" - - buffer.setText('var i;') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "// var i;" - - buffer.setText(' var i;') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe " // var i;" - - buffer.setText(' ') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe " // " - - buffer.setText(' a\n \n b') - languageMode.toggleLineCommentsForBufferRows(0, 2) - expect(buffer.lineForRow(0)).toBe " // a" - expect(buffer.lineForRow(1)).toBe " // " - expect(buffer.lineForRow(2)).toBe " // b" - - buffer.setText(' \n // var i;') - languageMode.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe ' ' - expect(buffer.lineForRow(1)).toBe ' var i;' - - describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", -> - it "returns the start/end rows of the foldable region starting at the given row", -> - expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 12] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 9] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull() - expect(languageMode.rowRangeForCodeFoldAtBufferRow(4)).toEqual [4, 7] - - describe ".rowRangeForCommentAtBufferRow(bufferRow)", -> - it "returns the start/end rows of the foldable comment starting at the given row", -> - buffer.setText("//this is a multi line comment\n//another line") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 1] - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 1] - - buffer.setText("//this is a multi line comment\n//another line\n//and one more") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 2] - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 2] - - buffer.setText("//this is a multi line comment\n\n//with an empty line") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined() - expect(languageMode.rowRangeForCommentAtBufferRow(2)).toBeUndefined() - - buffer.setText("//this is a single line comment\n") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined() - - buffer.setText("//this is a single line comment") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() - - describe ".suggestedIndentForBufferRow", -> - it "bases indentation off of the previous non-blank line", -> - expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0 - expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3 - expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1 - - it "does not take invisibles into account", -> - editor.update({showInvisibles: true}) - expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0 - expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3 - expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1 - - describe "rowRangeForParagraphAtBufferRow", -> - describe "with code and comments", -> - beforeEach -> - buffer.setText ''' - var quicksort = function () { - /* Single line comment block */ - var sort = function(items) {}; - - /* - A multiline - comment is here - */ - var sort = function(items) {}; - - // A comment - // - // Multiple comment - // lines - var sort = function(items) {}; - // comment line after fn - - var nosort = function(items) { - return item; - } - - }; - ''' - - it "will limit paragraph range to comments", -> - range = languageMode.rowRangeForParagraphAtBufferRow(0) - expect(range).toEqual [[0, 0], [0, 29]] - - range = languageMode.rowRangeForParagraphAtBufferRow(10) - expect(range).toEqual [[10, 0], [10, 14]] - range = languageMode.rowRangeForParagraphAtBufferRow(11) - expect(range).toBeFalsy() - range = languageMode.rowRangeForParagraphAtBufferRow(12) - expect(range).toEqual [[12, 0], [13, 10]] - - range = languageMode.rowRangeForParagraphAtBufferRow(14) - expect(range).toEqual [[14, 0], [14, 32]] - - range = languageMode.rowRangeForParagraphAtBufferRow(15) - expect(range).toEqual [[15, 0], [15, 26]] - - range = languageMode.rowRangeForParagraphAtBufferRow(18) - expect(range).toEqual [[17, 0], [19, 3]] - - describe "coffeescript", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".toggleLineCommentsForBufferRows(start, end)", -> - it "comments/uncomments lines in the given range", -> - languageMode.toggleLineCommentsForBufferRows(4, 6) - expect(buffer.lineForRow(4)).toBe " # pivot = items.shift()" - expect(buffer.lineForRow(5)).toBe " # left = []" - expect(buffer.lineForRow(6)).toBe " # right = []" - - languageMode.toggleLineCommentsForBufferRows(4, 5) - expect(buffer.lineForRow(4)).toBe " pivot = items.shift()" - expect(buffer.lineForRow(5)).toBe " left = []" - expect(buffer.lineForRow(6)).toBe " # right = []" - - it "comments/uncomments lines when empty line", -> - languageMode.toggleLineCommentsForBufferRows(4, 7) - expect(buffer.lineForRow(4)).toBe " # pivot = items.shift()" - expect(buffer.lineForRow(5)).toBe " # left = []" - expect(buffer.lineForRow(6)).toBe " # right = []" - expect(buffer.lineForRow(7)).toBe " # " - - languageMode.toggleLineCommentsForBufferRows(4, 5) - expect(buffer.lineForRow(4)).toBe " pivot = items.shift()" - expect(buffer.lineForRow(5)).toBe " left = []" - expect(buffer.lineForRow(6)).toBe " # right = []" - expect(buffer.lineForRow(7)).toBe " # " - - describe "fold suggestion", -> - describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", -> - it "returns the start/end rows of the foldable region starting at the given row", -> - expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 20] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 17] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull() - expect(languageMode.rowRangeForCodeFoldAtBufferRow(19)).toEqual [19, 20] - - describe "css", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('css.css', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-css') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".toggleLineCommentsForBufferRows(start, end)", -> - it "comments/uncomments lines in the given range", -> - languageMode.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe "/*body {" - expect(buffer.lineForRow(1)).toBe " font-size: 1234px;*/" - expect(buffer.lineForRow(2)).toBe " width: 110%;" - expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;" - - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(0)).toBe "/*body {" - expect(buffer.lineForRow(1)).toBe " font-size: 1234px;*/" - expect(buffer.lineForRow(2)).toBe " /*width: 110%;*/" - expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;" - - languageMode.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe "body {" - expect(buffer.lineForRow(1)).toBe " font-size: 1234px;" - expect(buffer.lineForRow(2)).toBe " /*width: 110%;*/" - expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;" - - it "uncomments lines with leading whitespace", -> - buffer.setTextInRange([[2, 0], [2, Infinity]], " /*width: 110%;*/") - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe " width: 110%;" - - it "uncomments lines with trailing whitespace", -> - buffer.setTextInRange([[2, 0], [2, Infinity]], "/*width: 110%;*/ ") - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe "width: 110%; " - - it "uncomments lines with leading and trailing whitespace", -> - buffer.setTextInRange([[2, 0], [2, Infinity]], " /*width: 110%;*/ ") - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe " width: 110%; " - - describe "less", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.less', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-less') - - waitsForPromise -> - atom.packages.activatePackage('language-css') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe "when commenting lines", -> - it "only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`", -> - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "// @color: #4D926F;" - - describe "xml", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.xml', autoIndent: false).then (o) -> - editor = o - editor.setText("") - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-xml') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe "when uncommenting lines", -> - it "removes the leading whitespace from the comment end pattern match", -> - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "test" - - describe "folding", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - it "maintains cursor buffer position when a folding/unfolding", -> - editor.setCursorBufferPosition([5, 5]) - languageMode.foldAll() - expect(editor.getCursorBufferPosition()).toEqual([5, 5]) - - describe ".unfoldAll()", -> - it "unfolds every folded line", -> - initialScreenLineCount = editor.getScreenLineCount() - languageMode.foldBufferRow(0) - languageMode.foldBufferRow(1) - expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount - languageMode.unfoldAll() - expect(editor.getScreenLineCount()).toBe initialScreenLineCount - - describe ".foldAll()", -> - it "folds every foldable line", -> - languageMode.foldAll() - - [fold1, fold2, fold3] = languageMode.unfoldAll() - expect([fold1.start.row, fold1.end.row]).toEqual [0, 12] - expect([fold2.start.row, fold2.end.row]).toEqual [1, 9] - expect([fold3.start.row, fold3.end.row]).toEqual [4, 7] - - describe ".foldBufferRow(bufferRow)", -> - describe "when bufferRow can be folded", -> - it "creates a fold based on the syntactic region starting at the given row", -> - languageMode.foldBufferRow(1) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [1, 9] - - describe "when bufferRow can't be folded", -> - it "searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)", -> - languageMode.foldBufferRow(8) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [1, 9] - - describe "when the bufferRow is already folded", -> - it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", -> - languageMode.foldBufferRow(2) - expect(editor.isFoldedAtBufferRow(0)).toBe(false) - expect(editor.isFoldedAtBufferRow(1)).toBe(true) - - languageMode.foldBufferRow(1) - expect(editor.isFoldedAtBufferRow(0)).toBe(true) - - describe "when the bufferRow is in a multi-line comment", -> - it "searches upward and downward for surrounding comment lines and folds them as a single fold", -> - buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment") - languageMode.foldBufferRow(1) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [1, 3] - - describe "when the bufferRow is a single-line comment", -> - it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", -> - buffer.insert([1, 0], " //this is a single line comment\n") - languageMode.foldBufferRow(1) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [0, 13] - - describe ".foldAllAtIndentLevel(indentLevel)", -> - it "folds blocks of text at the given indentation level", -> - languageMode.foldAllAtIndentLevel(0) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter - expect(editor.getLastScreenRow()).toBe 0 - - languageMode.foldAllAtIndentLevel(1) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" - expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + editor.displayLayer.foldCharacter - expect(editor.getLastScreenRow()).toBe 4 - - languageMode.foldAllAtIndentLevel(2) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" - expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" - expect(editor.lineTextForScreenRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.getLastScreenRow()).toBe 9 - - describe "folding with comments", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample-with-comments.js', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".unfoldAll()", -> - it "unfolds every folded line", -> - initialScreenLineCount = editor.getScreenLineCount() - languageMode.foldBufferRow(0) - languageMode.foldBufferRow(5) - expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount - languageMode.unfoldAll() - expect(editor.getScreenLineCount()).toBe initialScreenLineCount - - describe ".foldAll()", -> - it "folds every foldable line", -> - languageMode.foldAll() - - folds = languageMode.unfoldAll() - expect(folds.length).toBe 8 - expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30] - expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4] - expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27] - expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8] - expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16] - expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20] - expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22] - expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25] - - describe ".foldAllAtIndentLevel()", -> - it "folds every foldable range at a given indentLevel", -> - languageMode.foldAllAtIndentLevel(2) - - folds = languageMode.unfoldAll() - expect(folds.length).toBe 5 - expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8] - expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16] - expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20] - expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22] - expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25] - - it "does not fold anything but the indentLevel", -> - languageMode.foldAllAtIndentLevel(0) - - folds = languageMode.unfoldAll() - expect(folds.length).toBe 1 - expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30] - - describe ".isFoldableAtBufferRow(bufferRow)", -> - it "returns true if the line starts a multi-line comment", -> - expect(languageMode.isFoldableAtBufferRow(1)).toBe true - expect(languageMode.isFoldableAtBufferRow(6)).toBe true - expect(languageMode.isFoldableAtBufferRow(8)).toBe false - expect(languageMode.isFoldableAtBufferRow(11)).toBe true - expect(languageMode.isFoldableAtBufferRow(15)).toBe false - expect(languageMode.isFoldableAtBufferRow(17)).toBe true - expect(languageMode.isFoldableAtBufferRow(21)).toBe true - expect(languageMode.isFoldableAtBufferRow(24)).toBe true - expect(languageMode.isFoldableAtBufferRow(28)).toBe false - - it "returns true for lines that end with a comment and are followed by an indented line", -> - expect(languageMode.isFoldableAtBufferRow(5)).toBe true - - it "does not return true for a line in the middle of a comment that's followed by an indented line", -> - expect(languageMode.isFoldableAtBufferRow(7)).toBe false - editor.buffer.insert([8, 0], ' ') - expect(languageMode.isFoldableAtBufferRow(7)).toBe false - - describe "css", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('css.css', autoIndent: true).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-source') - atom.packages.activatePackage('language-css') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe "suggestedIndentForBufferRow", -> - it "does not return negative values (regression)", -> - editor.setText('.test {\npadding: 0;\n}') - expect(editor.suggestedIndentForBufferRow(2)).toBe 0 diff --git a/spec/language-mode-spec.js b/spec/language-mode-spec.js new file mode 100644 index 000000000..34f341bfc --- /dev/null +++ b/spec/language-mode-spec.js @@ -0,0 +1,503 @@ +const dedent = require('dedent') +const {Point, Range} = require('text-buffer') +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') + +describe('LanguageMode', () => { + let editor + + afterEach(() => { + editor.destroy() + }) + + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.toggleLineCommentsForBufferRows(start, end)', () => { + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + + editor.setText('\tvar i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;') + + editor.setText('var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// var i;') + + editor.setText(' var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // var i;') + + editor.setText(' ') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // ') + + editor.setText(' a\n \n b') + editor.toggleLineCommentsForBufferRows(0, 2) + expect(editor.lineTextForBufferRow(0)).toBe(' // a') + expect(editor.lineTextForBufferRow(1)).toBe(' // ') + expect(editor.lineTextForBufferRow(2)).toBe(' // b') + + editor.setText(' \n // var i;') + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe(' ') + expect(editor.lineTextForBufferRow(1)).toBe(' var i;') + }) + }) + + describe('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { + it('returns the start/end rows of the foldable region starting at the given row', () => { + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]]) + }) + }) + + describe('.suggestedIndentForBufferRow', () => { + it('bases indentation off of the previous non-blank line', () => { + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + + it('does not take invisibles into account', () => { + editor.update({showInvisibles: true}) + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + }) + + describe('rowRangeForParagraphAtBufferRow', () => { + describe('with code and comments', () => { + beforeEach(() => + editor.setText(dedent ` + var quicksort = function () { + /* Single line comment block */ + var sort = function(items) {}; + + /* + A multiline + comment is here + */ + var sort = function(items) {}; + + // A comment + // + // Multiple comment + // lines + var sort = function(items) {}; + // comment line after fn + + var nosort = function(items) { + item; + } + + }; + `) + ) + + it('will limit paragraph range to comments', () => { + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(0)).toEqual([[0, 0], [0, 29]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(1)).toEqual([[1, 0], [1, 33]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(2)).toEqual([[2, 0], [2, 32]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(3)).toBeFalsy() + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(4)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(5)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(6)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(7)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(8)).toEqual([[8, 0], [8, 32]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(9)).toBeFalsy() + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(10)).toEqual([[10, 0], [13, 10]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(11)).toEqual([[10, 0], [13, 10]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(12)).toEqual([[10, 0], [13, 10]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(14)).toEqual([[14, 0], [14, 32]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(15)).toEqual([[15, 0], [15, 26]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(18)).toEqual([[17, 0], [19, 3]]) + }) + }) + }) + }) + + describe('coffeescript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) + await atom.packages.activatePackage('language-coffee-script') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.toggleLineCommentsForBufferRows(start, end)', () => { + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 6) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + }) + + it('comments/uncomments lines when empty line', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') + }) + }) + + describe('fold suggestion', () => { + describe('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { + it('returns the start/end rows of the foldable region starting at the given row', () => { + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]]) + }) + }) + }) + }) + + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: false}) + await atom.packages.activatePackage('language-css') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.toggleLineCommentsForBufferRows(start, end)', () => { + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe('/*body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(0)).toBe('/*body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') + expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe('body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;') + expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + }) + + it('uncomments lines with leading whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') + }) + + it('uncomments lines with trailing whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe('width: 110%; ') + }) + + it('uncomments lines with leading and trailing whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ') + }) + }) + }) + + describe('less', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.less', {autoIndent: false}) + await atom.packages.activatePackage('language-less') + await atom.packages.activatePackage('language-css') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('when commenting lines', () => { + it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') + }) + }) + }) + + describe('xml', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.xml', {autoIndent: false}) + editor.setText('') + await atom.packages.activatePackage('language-xml') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('when uncommenting lines', () => { + it('removes the leading whitespace from the comment end pattern match', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('test') + }) + }) + }) + + describe('folding', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + it('maintains cursor buffer position when a folding/unfolding', () => { + editor.setCursorBufferPosition([5, 5]) + editor.foldAll() + expect(editor.getCursorBufferPosition()).toEqual([5, 5]) + }) + + describe('.unfoldAll()', () => { + it('unfolds every folded line', () => { + const initialScreenLineCount = editor.getScreenLineCount() + editor.foldBufferRow(0) + editor.foldBufferRow(1) + expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + editor.unfoldAll() + expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) + }) + }) + + describe('.foldAll()', () => { + it('folds every foldable line', () => { + editor.foldAll() + + const [fold1, fold2, fold3] = editor.unfoldAll() + expect([fold1.start.row, fold1.end.row]).toEqual([0, 12]) + expect([fold2.start.row, fold2.end.row]).toEqual([1, 9]) + expect([fold3.start.row, fold3.end.row]).toEqual([4, 7]) + }) + }) + + describe('.foldBufferRow(bufferRow)', () => { + describe('when bufferRow can be folded', () => { + it('creates a fold based on the syntactic region starting at the given row', () => { + editor.foldBufferRow(1) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([1, 9]) + }) + }) + + describe("when bufferRow can't be folded", () => { + it('searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)', () => { + editor.foldBufferRow(8) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([1, 9]) + }) + }) + + describe('when the bufferRow is already folded', () => { + it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => { + editor.foldBufferRow(2) + expect(editor.isFoldedAtBufferRow(0)).toBe(false) + expect(editor.isFoldedAtBufferRow(1)).toBe(true) + + editor.foldBufferRow(1) + expect(editor.isFoldedAtBufferRow(0)).toBe(true) + }) + }) + + describe('when the bufferRow is in a multi-line comment', () => { + it('searches upward and downward for surrounding comment lines and folds them as a single fold', () => { + editor.buffer.insert([1, 0], ' //this is a comment\n // and\n //more docs\n\n//second comment') + editor.foldBufferRow(1) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([1, 3]) + }) + }) + + describe('when the bufferRow is a single-line comment', () => { + it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => { + editor.buffer.insert([1, 0], ' //this is a single line comment\n') + editor.foldBufferRow(1) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([0, 13]) + }) + }) + }) + + describe('.foldAllAtIndentLevel(indentLevel)', () => { + it('folds blocks of text at the given indentation level', () => { + editor.foldAllAtIndentLevel(0) + expect(editor.lineTextForScreenRow(0)).toBe(`var quicksort = function () {${editor.displayLayer.foldCharacter}`) + expect(editor.getLastScreenRow()).toBe(0) + + editor.foldAllAtIndentLevel(1) + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') + expect(editor.lineTextForScreenRow(1)).toBe(` var sort = function(items) {${editor.displayLayer.foldCharacter}`) + expect(editor.getLastScreenRow()).toBe(4) + + editor.foldAllAtIndentLevel(2) + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') + expect(editor.lineTextForScreenRow(1)).toBe(' var sort = function(items) {') + expect(editor.lineTextForScreenRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.getLastScreenRow()).toBe(9) + }) + }) + }) + + describe('folding with comments', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.unfoldAll()', () => { + it('unfolds every folded line', () => { + const initialScreenLineCount = editor.getScreenLineCount() + editor.foldBufferRow(0) + editor.foldBufferRow(5) + expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + editor.unfoldAll() + expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) + }) + }) + + describe('.foldAll()', () => { + it('folds every foldable line', () => { + editor.foldAll() + + const folds = editor.unfoldAll() + expect(folds.length).toBe(8) + expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) + expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4]) + expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27]) + expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8]) + expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16]) + expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20]) + expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22]) + expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25]) + }) + }) + + describe('.foldAllAtIndentLevel()', () => { + it('folds every foldable range at a given indentLevel', () => { + editor.foldAllAtIndentLevel(2) + + const folds = editor.unfoldAll() + expect(folds.length).toBe(5) + expect([folds[0].start.row, folds[0].end.row]).toEqual([6, 8]) + expect([folds[1].start.row, folds[1].end.row]).toEqual([11, 16]) + expect([folds[2].start.row, folds[2].end.row]).toEqual([17, 20]) + expect([folds[3].start.row, folds[3].end.row]).toEqual([21, 22]) + expect([folds[4].start.row, folds[4].end.row]).toEqual([24, 25]) + }) + + it('does not fold anything but the indentLevel', () => { + editor.foldAllAtIndentLevel(0) + + const folds = editor.unfoldAll() + expect(folds.length).toBe(1) + expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) + }) + }) + + describe('.isFoldableAtBufferRow(bufferRow)', () => { + it('returns true if the line starts a multi-line comment', () => { + expect(editor.isFoldableAtBufferRow(1)).toBe(true) + expect(editor.isFoldableAtBufferRow(6)).toBe(true) + expect(editor.isFoldableAtBufferRow(8)).toBe(false) + expect(editor.isFoldableAtBufferRow(11)).toBe(true) + expect(editor.isFoldableAtBufferRow(15)).toBe(false) + expect(editor.isFoldableAtBufferRow(17)).toBe(true) + expect(editor.isFoldableAtBufferRow(21)).toBe(true) + expect(editor.isFoldableAtBufferRow(24)).toBe(true) + expect(editor.isFoldableAtBufferRow(28)).toBe(false) + }) + + it('returns true for lines that end with a comment and are followed by an indented line', () => { + expect(editor.isFoldableAtBufferRow(5)).toBe(true) + }) + + it("does not return true for a line in the middle of a comment that's followed by an indented line", () => { + expect(editor.isFoldableAtBufferRow(7)).toBe(false) + editor.buffer.insert([8, 0], ' ') + expect(editor.isFoldableAtBufferRow(7)).toBe(false) + }) + }) + }) + + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: true}) + await atom.packages.activatePackage('language-source') + await atom.packages.activatePackage('language-css') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('suggestedIndentForBufferRow', () => { + it('does not return negative values (regression)', () => { + editor.setText('.test {\npadding: 0;\n}') + expect(editor.suggestedIndentForBufferRow(2)).toBe(0) + }) + }) + }) +}) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 3fd40cdad..82764c438 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3343,9 +3343,9 @@ describe('TextEditorComponent', () => { await component.getNextUpdatePromise() expect(editor.isFoldedAtScreenRow(5)).toBe(true) - target = element.querySelectorAll('.line-number')[6].querySelector('.icon-right') - component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 5)}) - expect(editor.isFoldedAtScreenRow(5)).toBe(false) + target = element.querySelectorAll('.line-number')[4].querySelector('.icon-right') + component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 4)}) + expect(editor.isFoldedAtScreenRow(4)).toBe(false) }) it('autoscrolls when dragging near the top or bottom of the gutter', async () => { diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index a8e01f798..134a1a0b1 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -1,8 +1,9 @@ const NullGrammar = require('../src/null-grammar') const TokenizedBuffer = require('../src/tokenized-buffer') const TextBuffer = require('text-buffer') -const {Point} = TextBuffer +const {Point, Range} = TextBuffer const _ = require('underscore-plus') +const dedent = require('dedent') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') describe('TokenizedBuffer', () => { @@ -12,7 +13,6 @@ describe('TokenizedBuffer', () => { // enable async tokenization TokenizedBuffer.prototype.chunkSize = 5 jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') - await atom.packages.activatePackage('language-javascript') }) @@ -528,78 +528,6 @@ describe('TokenizedBuffer', () => { }) }) // } - describe('.isFoldableAtRow(row)', () => { - beforeEach(() => { - buffer = atom.project.bufferForPathSync('sample.js') - buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') - buffer.insert([0, 0], '// multi-line\n// comment\n// block\n') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - }) - - it('includes the first line of multi-line comments', () => { - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent - expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false) - - buffer.insert([0, Infinity], '\n') - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false) - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) - }) // because of indent - - it('includes non-comment lines that precede an increase in indentation', () => { - buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable - - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.insert([7, 0], ' ') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.insert([7, 0], ' \n x\n') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.insert([9, 0], ' ') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - }) - }) - describe('.tokenizedLineForRow(row)', () => { it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => { buffer = atom.project.bufferForPathSync('sample.js') @@ -750,4 +678,239 @@ describe('TokenizedBuffer', () => { }) }) }) + + describe('.isFoldableAtRow(row)', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') + buffer.insert([0, 0], '// multi-line\n// comment\n// block\n') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + }) + + it('includes the first line of multi-line comments', () => { + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent + expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false) + + buffer.insert([0, Infinity], '\n') + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + }) // because of indent + + it('includes non-comment lines that precede an increase in indentation', () => { + buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable + + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' \n x\n') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([9, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + }) + }) + + describe('.getFoldableRangesAtIndentLevel', () => { + it('returns the ranges that can be folded at the given indent level', () => { + buffer = new TextBuffer(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) { + f() + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + + tokenizedBuffer = new TokenizedBuffer({buffer}) + + expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2))).toBe(dedent ` + if (a) {⋯ + } + i() + if (j) {⋯ + } + `) + + expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2))).toBe(dedent ` + if (a) { + b(); + if (c) {⋯ + } + h() + } + i() + if (j) { + k() + } + `) + + expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2))).toBe(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) {⋯ + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + }) + }) + + describe('.getFoldableRanges', () => { + it('returns the ranges that can be folded', () => { + buffer = new TextBuffer(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) { + f() + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + + tokenizedBuffer = new TokenizedBuffer({buffer}) + + expect(tokenizedBuffer.getFoldableRanges(2).map(r => r.toString())).toEqual([ + ...tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2), + ...tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2), + ...tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2), + ].sort((a, b) => (a.start.row - b.start.row) || (a.end.row - b.end.row)).map(r => r.toString())) + }) + }) + + describe('.getFoldableRangeContainingPoint', () => { + it('returns the range for the smallest fold that contains the given range', () => { + buffer = new TextBuffer(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) { + f() + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + + tokenizedBuffer = new TokenizedBuffer({buffer}) + + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 5), 2)).toBeNull() + + let range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 10), 2) + expect(simulateFold([range])).toBe(dedent ` + if (a) {⋯ + } + i() + if (j) { + k() + } + `) + + range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity), 2) + expect(simulateFold([range])).toBe(dedent ` + if (a) {⋯ + } + i() + if (j) { + k() + } + `) + + range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, 20), 2) + expect(simulateFold([range])).toBe(dedent ` + if (a) { + b(); + if (c) {⋯ + } + h() + } + i() + if (j) { + k() + } + `) + }) + }) + + function simulateFold (ranges) { + buffer.transact(() => { + for (const range of ranges.reverse()) { + buffer.setTextInRange(range, '⋯') + } + }) + let text = buffer.getText() + buffer.undo() + return text + } }) diff --git a/src/language-mode.coffee b/src/language-mode.coffee index 1839f1c59..953d328b2 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -1,9 +1,11 @@ -{Range} = require 'text-buffer' +{Range, Point} = require 'text-buffer' _ = require 'underscore-plus' {OnigRegExp} = require 'oniguruma' ScopeDescriptor = require './scope-descriptor' NullGrammar = require './null-grammar' +NON_WHITESPACE_REGEX = /\S/ + module.exports = class LanguageMode # Sets up a `LanguageMode` for the given {TextEditor}. @@ -90,148 +92,28 @@ class LanguageMode buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString) return - # Folds all the foldable lines in the buffer. - foldAll: -> - @unfoldAll() - foldedRowRanges = {} - for currentRow in [0..@buffer.getLastRow()] by 1 - rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] - continue unless startRow? - continue if foldedRowRanges[rowRange] - - @editor.foldBufferRowRange(startRow, endRow) - foldedRowRanges[rowRange] = true - return - - # Unfolds all the foldable lines in the buffer. - unfoldAll: -> - @editor.displayLayer.destroyAllFolds() - - # Fold all comment and code blocks at a given indentLevel - # - # indentLevel - A {Number} indicating indentLevel; 0 based. - foldAllAtIndentLevel: (indentLevel) -> - @unfoldAll() - foldedRowRanges = {} - for currentRow in [0..@buffer.getLastRow()] by 1 - rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] - continue unless startRow? - continue if foldedRowRanges[rowRange] - - # assumption: startRow will always be the min indent level for the entire range - if @editor.indentationForBufferRow(startRow) is indentLevel - @editor.foldBufferRowRange(startRow, endRow) - foldedRowRanges[rowRange] = true - return - - # Given a buffer row, creates a fold at it. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns the new {Fold}. - foldBufferRow: (bufferRow) -> - for currentRow in [bufferRow..0] by -1 - [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] - continue unless startRow? and startRow <= bufferRow <= endRow - unless @editor.isFoldedAtBufferRow(startRow) - return @editor.foldBufferRowRange(startRow, endRow) - - # Find the row range for a fold at a given bufferRow. Will handle comments - # and code. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns an {Array} of the [startRow, endRow]. Returns null if no range. - rowRangeForFoldAtBufferRow: (bufferRow) -> - rowRange = @rowRangeForCommentAtBufferRow(bufferRow) - rowRange ?= @rowRangeForCodeFoldAtBufferRow(bufferRow) - rowRange - - rowRangeForCommentAtBufferRow: (bufferRow) -> - return unless @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() - - startRow = bufferRow - endRow = bufferRow - - if bufferRow > 0 - for currentRow in [bufferRow-1..0] by -1 - break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment() - startRow = currentRow - - if bufferRow < @buffer.getLastRow() - for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1 - break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment() - endRow = currentRow - - return [startRow, endRow] if startRow isnt endRow - - rowRangeForCodeFoldAtBufferRow: (bufferRow) -> - return null unless @isFoldableAtBufferRow(bufferRow) - - startIndentLevel = @editor.indentationForBufferRow(bufferRow) - scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0]) - for row in [(bufferRow + 1)..@editor.getLastBufferRow()] by 1 - continue if @editor.isBufferRowBlank(row) - indentation = @editor.indentationForBufferRow(row) - if indentation <= startIndentLevel - includeRowInFold = indentation is startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row)) - foldEndRow = row if includeRowInFold - break - - foldEndRow = row - - [bufferRow, foldEndRow] - - isFoldableAtBufferRow: (bufferRow) -> - @editor.tokenizedBuffer.isFoldableAtRow(bufferRow) - - # Returns a {Boolean} indicating whether the line at the given buffer - # row is a comment. - isLineCommentedAtBufferRow: (bufferRow) -> - return false unless 0 <= bufferRow <= @editor.getLastBufferRow() - @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() ? false - # Find a row range for a 'paragraph' around specified bufferRow. A paragraph # is a block of text bounded by and empty line or a block of text that is not # the same type (comments next to source code). rowRangeForParagraphAtBufferRow: (bufferRow) -> - scope = @editor.scopeDescriptorForBufferPosition([bufferRow, 0]) - commentStrings = @editor.getCommentStrings(scope) - commentStartRegex = null - if commentStrings?.commentStartString? and not commentStrings.commentEndString? - commentStartRegexString = _.escapeRegExp(commentStrings.commentStartString).replace(/(\s+)$/, '(?:$1)?') - commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") + return unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(bufferRow)) - filterCommentStart = (line) -> - if commentStartRegex? - matches = commentStartRegex.searchSync(line) - line = line.substring(matches[0].end) if matches?.length - line - - return unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(bufferRow))) - - if @isLineCommentedAtBufferRow(bufferRow) - isOriginalRowComment = true - range = @rowRangeForCommentAtBufferRow(bufferRow) - [firstRow, lastRow] = range or [bufferRow, bufferRow] - else - isOriginalRowComment = false - [firstRow, lastRow] = [0, @editor.getLastBufferRow()-1] + isCommented = @editor.tokenizedBuffer.isRowCommented(bufferRow) startRow = bufferRow - while startRow > firstRow - break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment - break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1))) + while startRow > 0 + break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(startRow - 1)) + break if @editor.tokenizedBuffer.isRowCommented(startRow - 1) isnt isCommented startRow-- endRow = bufferRow - lastRow = @editor.getLastBufferRow() - while endRow < lastRow - break if @isLineCommentedAtBufferRow(endRow + 1) isnt isOriginalRowComment - break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(endRow + 1))) + rowCount = @editor.getLineCount() + while endRow < rowCount + break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(endRow + 1)) + break if @editor.tokenizedBuffer.isRowCommented(endRow + 1) isnt isCommented endRow++ - new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length]) + new Range(new Point(startRow, 0), new Point(endRow, @editor.buffer.lineLengthForRow(endRow))) # Given a buffer row, this returns a suggested indentation level. # @@ -345,6 +227,3 @@ class LanguageMode decreaseNextIndentRegexForScopeDescriptor: (scopeDescriptor) -> @cacheRegex(@editor.getDecreaseNextIndentPattern(scopeDescriptor)) - - foldEndRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getFoldEndPattern(scopeDescriptor)) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a84f6f631..117589750 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3311,13 +3311,14 @@ class TextEditor extends Model # indentation level up to the nearest following row with a lower indentation # level. foldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @foldBufferRow(bufferRow) + {row} = @getCursorBufferPosition() + range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) + @displayLayer.foldBufferRange(range) # Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @unfoldBufferRow(bufferRow) + position = @getCursorBufferPosition() + @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) # Essential: Fold the given row in buffer coordinates based on its indentation # level. @@ -3327,13 +3328,26 @@ class TextEditor extends Model # # * `bufferRow` A {Number}. foldBufferRow: (bufferRow) -> - @languageMode.foldBufferRow(bufferRow) + position = Point(bufferRow, Infinity) + loop + foldableRange = @tokenizedBuffer.getFoldableRangeContainingPoint(position, @getTabLength()) + if foldableRange + existingFolds = @displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start)) + if existingFolds.length is 0 + @displayLayer.foldBufferRange(foldableRange) + else + firstExistingFoldRange = @displayLayer.bufferRangeForFold(existingFolds[0]) + if firstExistingFoldRange.start.isLessThan(position) + position = Point(firstExistingFoldRange.start.row, 0) + continue + return # Essential: Unfold all folds containing the given row in buffer coordinates. # # * `bufferRow` A {Number} unfoldBufferRow: (bufferRow) -> - @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))) + position = Point(bufferRow, Infinity) + @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) # Extended: For each selection, fold the rows it intersects. foldSelectedLines: -> @@ -3342,18 +3356,25 @@ class TextEditor extends Model # Extended: Fold all foldable lines. foldAll: -> - @languageMode.foldAll() + @displayLayer.destroyAllFolds() + for range in @tokenizedBuffer.getFoldableRanges(@getTabLength()) + @displayLayer.foldBufferRange(range) + return # Extended: Unfold all existing folds. unfoldAll: -> - @languageMode.unfoldAll() + result = @displayLayer.destroyAllFolds() @scrollToCursorPosition() + result # Extended: Fold all foldable lines at the given indent level. # # * `level` A {Number}. foldAllAtIndentLevel: (level) -> - @languageMode.foldAllAtIndentLevel(level) + @displayLayer.destroyAllFolds() + for range in @tokenizedBuffer.getFoldableRangesAtIndentLevel(level, @getTabLength()) + @displayLayer.foldBufferRange(range) + return # Extended: Determine whether the given row in buffer coordinates is foldable. # @@ -3547,6 +3568,7 @@ class TextEditor extends Model # for specific syntactic scopes. See the `ScopedSettingsDelegate` in # `text-editor-registry.js` for an example implementation. setScopedSettingsDelegate: (@scopedSettingsDelegate) -> + @tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate # Experimental: Retrieve the {Object} that provides the editor with settings # for specific syntactic scopes. diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index fbb9de77f..fd5691740 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -6,8 +6,11 @@ const TokenIterator = require('./token-iterator') const ScopeDescriptor = require('./scope-descriptor') const TokenizedBufferIterator = require('./tokenized-buffer-iterator') const NullGrammar = require('./null-grammar') +const {OnigRegExp} = require('oniguruma') const {toFirstMateScopeId} = require('./first-mate-helpers') +const NON_WHITESPACE_REGEX = /\S/ + let nextId = 0 const prefixedScopes = new Map() @@ -26,6 +29,7 @@ class TokenizedBuffer { this.emitter = new Emitter() this.disposables = new CompositeDisposable() this.tokenIterator = new TokenIterator(this) + this.regexesByPattern = {} this.alive = true this.id = params.id != null ? params.id : nextId++ @@ -265,33 +269,7 @@ class TokenizedBuffer { } isFoldableAtRow (row) { - return this.isFoldableCodeAtRow(row) || this.isFoldableCommentAtRow(row) - } - - // Returns a {Boolean} indicating whether the given buffer row starts - // a a foldable row range due to the code's indentation patterns. - isFoldableCodeAtRow (row) { - if (row >= 0 && row <= this.buffer.getLastRow()) { - const nextRow = this.buffer.nextNonBlankRow(row) - const tokenizedLine = this.tokenizedLines[row] - if (this.buffer.isRowBlank(row) || (tokenizedLine && tokenizedLine.isComment()) || nextRow == null) { - return false - } else { - return this.indentLevelForRow(nextRow) > this.indentLevelForRow(row) - } - } else { - return false - } - } - - isFoldableCommentAtRow (row) { - const previousRow = row - 1 - const nextRow = row + 1 - return ( - (!this.tokenizedLines[previousRow] || !this.tokenizedLines[previousRow].isComment()) && - (this.tokenizedLines[row] && this.tokenizedLines[row].isComment()) && - (this.tokenizedLines[nextRow] && this.tokenizedLines[nextRow].isComment()) - ) + return this.endRowForFoldAtRow(row, 1) != null } buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) { @@ -554,6 +532,116 @@ class TokenizedBuffer { return new Range(new Point(position.row, startColumn), new Point(position.row, endColumn)) } + isRowCommented (row) { + return this.tokenizedLines[row] && this.tokenizedLines[row].isComment() + } + + getFoldableRangeContainingPoint (point, tabLength) { + if (point.column >= this.buffer.lineLengthForRow(point.row)) { + const endRow = this.endRowForFoldAtRow(point.row, tabLength) + if (endRow != null) { + return Range(Point(point.row, Infinity), Point(endRow, Infinity)) + } + } + + for (let row = point.row - 1; row >= 0; row--) { + const endRow = this.endRowForFoldAtRow(row, tabLength) + if (endRow != null && endRow > point.row) { + return Range(Point(row, Infinity), Point(endRow, Infinity)) + } + } + return null + } + + getFoldableRangesAtIndentLevel (indentLevel, tabLength) { + const result = [] + let row = 0 + const lineCount = this.buffer.getLineCount() + while (row < lineCount) { + if (this.indentLevelForLine(this.buffer.lineForRow(row), tabLength) === indentLevel) { + const endRow = this.endRowForFoldAtRow(row, tabLength) + if (endRow != null) { + result.push(Range(Point(row, Infinity), Point(endRow, Infinity))) + row = endRow + 1 + continue + } + } + row++ + } + return result + } + + getFoldableRanges (tabLength) { + const result = [] + let row = 0 + const lineCount = this.buffer.getLineCount() + while (row < lineCount) { + const endRow = this.endRowForFoldAtRow(row, tabLength) + if (endRow != null) { + result.push(Range(Point(row, Infinity), Point(endRow, Infinity))) + } + row++ + } + return result + } + + endRowForFoldAtRow (row, tabLength) { + if (this.isRowCommented(row)) { + return this.endRowForCommentFoldAtRow(row) + } else { + return this.endRowForCodeFoldAtRow(row, tabLength) + } + } + + endRowForCommentFoldAtRow (row) { + if (this.isRowCommented(row - 1)) return + + let endRow + for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) { + if (!this.isRowCommented(nextRow)) break + endRow = nextRow + } + + return endRow + } + + endRowForCodeFoldAtRow (row, tabLength) { + let foldEndRow + const line = this.buffer.lineForRow(row) + if (!NON_WHITESPACE_REGEX.test(line)) return + const startIndentLevel = this.indentLevelForLine(line, tabLength) + const scopeDescriptor = this.scopeDescriptorForPosition([row, 0]) + const foldEndRegex = this.foldEndRegexForScopeDescriptor(scopeDescriptor) + for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) { + const line = this.buffer.lineForRow(nextRow) + if (!NON_WHITESPACE_REGEX.test(line)) continue + const indentation = this.indentLevelForLine(line, tabLength) + if (indentation < startIndentLevel) { + break + } else if (indentation === startIndentLevel) { + if (foldEndRegex && foldEndRegex.searchSync(line)) foldEndRow = nextRow + break + } + foldEndRow = nextRow + } + return foldEndRow + } + + foldEndRegexForScopeDescriptor (scopes) { + if (this.scopedSettingsDelegate) { + return this.regexForPattern(this.scopedSettingsDelegate.getFoldEndPattern(scopes)) + } + } + + regexForPattern (pattern) { + if (pattern) { + if (!this.regexesByPattern[pattern]) { + this.regexesByPattern[pattern] = new OnigRegExp(pattern) + } + return this.regexesByPattern[pattern] + } + } + // Gets the row number of the last line. // // Returns a {Number}.