diff --git a/Atom.xcodeproj/project.pbxproj b/Atom.xcodeproj/project.pbxproj index b95de6be8..afc739ad0 100644 --- a/Atom.xcodeproj/project.pbxproj +++ b/Atom.xcodeproj/project.pbxproj @@ -396,6 +396,7 @@ 048A2FC5154870DC0051715C /* PathWatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PathWatcher.h; sourceTree = ""; }; 048A2FC6154870DC0051715C /* PathWatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PathWatcher.m; sourceTree = ""; }; 04E1DDDC152A0941001A9D07 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = frameworks/Sparkle.framework; sourceTree = ""; }; + 04F21A2615644AC10083F6D4 /* ResourceConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ResourceConfig.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -791,9 +792,10 @@ children = ( 0487D15E14FEE7880045E5E3 /* Resources */, 0487C93414FED5FB0045E5E3 /* ClientWindow.xib */, - 0487C93514FED5FB0045E5E3 /* Info.plist */, 0487C93614FED5FB0045E5E3 /* MainMenu.xib */, + 0487C93514FED5FB0045E5E3 /* Info.plist */, 0415A1FD1524EC8A0075C91C /* atom.icns */, + 04F21A2615644AC10083F6D4 /* ResourceConfig.xcconfig */, ); name = "Supporting Files"; sourceTree = ""; @@ -946,7 +948,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "rake compile-coffeescripts\n"; + shellScript = "rake copy-files-to-bundle\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -1054,7 +1056,7 @@ GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", - "LOAD_RESOURCES_FROM_DIR=\"\\\"$PROJECT_DIR\\\"\"", + "LOAD_RESOURCES_FROM_DIR=\"\\\"$LOAD_RESOURCES_FROM_DIR\\\"\"", "\"ENABLE_REMOTING=1\"", "\"ENABLE_P2P_APIS=1\"", "\"ENABLE_CONFIGURATION_POLICY\"", @@ -1117,6 +1119,7 @@ }; 0487C93114FED5370045E5E3 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 04F21A2615644AC10083F6D4 /* ResourceConfig.xcconfig */; buildSettings = { FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Atom.xcodeproj/xcshareddata/xcschemes/atom-debug.xcscheme b/Atom.xcodeproj/xcshareddata/xcschemes/atom-debug.xcscheme new file mode 100644 index 000000000..b7620e646 --- /dev/null +++ b/Atom.xcodeproj/xcshareddata/xcschemes/atom-debug.xcscheme @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Atom.xcodeproj/xcshareddata/xcschemes/atom-release.xcscheme b/Atom.xcodeproj/xcshareddata/xcschemes/atom-release.xcscheme new file mode 100644 index 000000000..9740ad880 --- /dev/null +++ b/Atom.xcodeproj/xcshareddata/xcschemes/atom-release.xcscheme @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Atom.xcodeproj/xcshareddata/xcschemes/libcef_dll_wrapper.xcscheme b/Atom.xcodeproj/xcshareddata/xcschemes/libcef_dll_wrapper.xcscheme new file mode 100644 index 000000000..a6ed5e475 --- /dev/null +++ b/Atom.xcodeproj/xcshareddata/xcschemes/libcef_dll_wrapper.xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Atom/ResourceConfig.xcconfig b/Atom/ResourceConfig.xcconfig new file mode 100644 index 000000000..e9cc63cdd --- /dev/null +++ b/Atom/ResourceConfig.xcconfig @@ -0,0 +1 @@ +LOAD_RESOURCES_FROM_DIR=$PROJECT_DIR diff --git a/Atom/src/Atom.mm b/Atom/src/Atom.mm index 1733e242c..7b563e796 100755 --- a/Atom/src/Atom.mm +++ b/Atom/src/Atom.mm @@ -112,7 +112,8 @@ CefRefPtr atom = CefV8Value::CreateObject(NULL, NULL); global->SetValue("atom", atom, V8_PROPERTY_ATTRIBUTE_NONE); - + + #ifdef LOAD_RESOURCES_FROM_DIR char path[] = LOAD_RESOURCES_FROM_DIR; #else diff --git a/Rakefile b/Rakefile index c6ddeeb17..2cbb0d9a0 100644 --- a/Rakefile +++ b/Rakefile @@ -7,7 +7,7 @@ BUILD_DIR = '/tmp/atom-build' desc "Build Atom via `xcodebuild`" task :build => :"verify-prerequisites" do - output = `xcodebuild -configuration Release SYMROOT=#{BUILD_DIR}` + output = `xcodebuild -scheme atom-release SYMROOT=#{BUILD_DIR}` if $?.exitstatus != 0 $stderr.puts "Error #{$?.exitstatus}:\n#{output}" exit($?.exitstatus) @@ -48,8 +48,8 @@ task :benchmark do Rake::Task["run"].invoke end -desc "Compile CoffeeScripts" -task :"compile-coffeescripts" => :"verify-prerequisites" do +desc "Copy files to bundle and compile CoffeeScripts" +task :"copy-files-to-bundle" => :"verify-prerequisites" do project_dir = ENV['PROJECT_DIR'] || '.' built_dir = ENV['BUILT_PRODUCTS_DIR'] || '.' contents_dir = ENV['CONTENTS_FOLDER_PATH'].to_s @@ -61,8 +61,11 @@ task :"compile-coffeescripts" => :"verify-prerequisites" do cp_r dir, File.join(dest, dir) end - puts contents_dir - sh "coffee -c #{dest}/src #{dest}/vendor #{dest}/spec" + if ENV['LOAD_RESOURCES_FROM_DIR'] + sh "coffee -c #{dest}/src/stdlib/require.coffee" + else + sh "coffee -c #{dest}/src #{dest}/vendor #{dest}/spec" + end end desc "Change webkit frameworks to use @rpath as install name" @@ -83,7 +86,7 @@ end desc "Remove any 'fit' or 'fdescribe' focus directives from the specs" task :nof do - system %{find . -name *spec.coffee | xargs sed -E -i "" "s/f(it|describe) +(['\\"])/\\1 \\2/g"} + system %{find . -name *spec.coffee | xargs sed -E -i "" "s/f+(it|describe) +(['\\"])/\\1 \\2/g"} end task :"verify-prerequisites" do diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 06078e855..9c1fc04ba 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -27,7 +27,7 @@ describe "editor.", -> editor.setBuffer new Buffer(require.resolve('fixtures/medium.coffee')) describe "at-begining.", -> - benchmark "insert-delete", -> + fbenchmark "insert-delete", -> editor.insertText('x') editor.backspace() @@ -53,8 +53,6 @@ describe "editor.", -> benchmark "moving-to-eof.", 1, -> editor.moveCursorToBottom() - waitsFor (scrollComplete) -> - editor.scrollView.on 'scroll', scrollComplete describe "on-first-line.", -> benchmark "inserting-newline", 5, -> diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 7d587d859..7d3ccc3a2 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -278,6 +278,53 @@ describe "Editor", -> editor.loadPreviousEditSession() expect(editor.buffer.path).toBe "2" + describe ".scrollTop(n)", -> + beforeEach -> + editor.attachToDom(heightInLines: 5) + expect(editor.verticalScrollbar.scrollTop()).toBe 0 + expect(editor.visibleLines.css('-webkit-tranform')).toBeNull() + expect(editor.gutter.lineNumbers.css('-webkit-tranform')).toBeNull() + + describe "when called with a scroll top argument", -> + it "sets the scrollTop of the vertical scrollbar and sets a transform on the line numbers and lines", -> + editor.scrollTop(100) + expect(editor.verticalScrollbar.scrollTop()).toBe 100 + expect(editor.visibleLines.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -100)' + expect(editor.gutter.lineNumbers.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -100)' + + editor.scrollTop(150) + expect(editor.verticalScrollbar.scrollTop()).toBe 150 + expect(editor.visibleLines.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -150)' + expect(editor.gutter.lineNumbers.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -150)' + + it "does not allow negative scrollTops to be assigned", -> + editor.scrollTop(-100) + expect(editor.scrollTop()).toBe 0 + + it "doesn't do anything if the scrollTop hasn't changed", -> + editor.scrollTop(100) + spyOn(editor.verticalScrollbar, 'scrollTop') + spyOn(editor.visibleLines, 'css') + spyOn(editor.gutter.lineNumbers, 'css') + + editor.scrollTop(100) + expect(editor.verticalScrollbar.scrollTop).not.toHaveBeenCalled() + expect(editor.visibleLines.css).not.toHaveBeenCalled() + expect(editor.gutter.lineNumbers.css).not.toHaveBeenCalled() + + describe "when the 'adjustVerticalScrollbar' option is false (defaults to true)", -> + it "doesn't adjust the scrollTop of the vertical scrollbar", -> + editor.scrollTop(100, adjustVerticalScrollbar: false) + expect(editor.verticalScrollbar.scrollTop()).toBe 0 + expect(editor.visibleLines.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -100)' + expect(editor.gutter.lineNumbers.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -100)' + + describe "when called with no argument", -> + it "returns the last assigned value or 0 if none has been assigned", -> + expect(editor.scrollTop()).toBe 0 + editor.scrollTop(50) + expect(editor.scrollTop()).toBe 50 + describe "editor-open event", -> it 'only triggers an editor-open event when it is first added to the DOM', -> openHandler = jasmine.createSpy('openHandler') @@ -334,6 +381,7 @@ describe "Editor", -> describe "when soft-wrap is enabled", -> beforeEach -> + setEditorHeightInLines(editor, 20) setEditorWidthInChars(editor, 50) editor.setSoftWrap(true) expect(editor.renderer.maxLineLength).toBe 50 @@ -504,6 +552,90 @@ describe "Editor", -> expect(editor.visibleLines.find('.line:eq(0)').text()).toBe editor.buffer.lineForRow(1) expect(editor.visibleLines.find('.line:eq(5)').text()).toBe editor.buffer.lineForRow(6) + describe "when lines are added", -> + beforeEach -> + editor.attachToDom() + setEditorHeightInLines(editor, 5) + spyOn(editor, "scrollTo") + + describe "when the change the precedes the first rendered row", -> + it "inserts and removes rendered lines to account for upstream change", -> + editor.scrollBottom(editor.scrollView.prop('scrollHeight')) + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(8) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + buffer.change([[1,0], [3,0]], "1\n2\n3\n") + expect(editor.visibleLines.find(".line").length).toBe 5 + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(8) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + describe "when the change straddles the first rendered row", -> + it "doesn't render rows that were not previously rendered", -> + editor.scrollBottom(editor.scrollView.prop('scrollHeight')) + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(8) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + buffer.change([[2,0], [7,0]], "2\n3\n4\n5\n6\n7\n8\n9\n") + expect(editor.visibleLines.find(".line").length).toBe 5 + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(8) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + describe "when the change the straddles the last rendered row", -> + it "doesn't render rows that were not previously rendered", -> + buffer.change([[2,0], [7,0]], "2\n3\n4\n5\n6\n7\n8\n") + expect(editor.visibleLines.find(".line").length).toBe 5 + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(0) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(4) + + describe "when the change the follows the last rendered row", -> + it "does not change the rendered lines", -> + buffer.change([[12,0], [12,0]], "12\n13\n14\n") + expect(editor.visibleLines.find(".line").length).toBe 5 + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(0) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(4) + + describe "when lines are removed", -> + beforeEach -> + editor.attachToDom() + setEditorHeightInLines(editor, 5) + spyOn(editor, "scrollTo") + + describe "when the change the precedes the first rendered row", -> + it "removes rendered lines to account for upstream change", -> + editor.scrollBottom(editor.scrollView.prop('scrollHeight')) + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(8) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + buffer.change([[1,0], [2,0]], "") + expect(editor.visibleLines.find(".line").length).toBe 4 + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(8) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(11) + + describe "when the change straddles the first rendered row", -> + it "renders the correct rows", -> + editor.scrollBottom(editor.scrollView.prop('scrollHeight')) + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(8) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + buffer.change([[7,0], [11,0]], "1\n2\n") + expect(editor.visibleLines.find(".line").length).toBe 3 + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(8) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(10) + + describe "when the change the straddles the last rendered row", -> + it "renders the correct rows", -> + buffer.change([[2,0], [7,0]], "") + expect(editor.visibleLines.find(".line").length).toBe 5 + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(0) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(4) + + describe "when the change the follows the last rendered row", -> + it "does not change the rendered lines", -> + buffer.change([[12,0], [12,0]], "") + expect(editor.visibleLines.find(".line").length).toBe 5 + expect(editor.visibleLines.find(".line:first").text()).toBe buffer.lineForRow(0) + expect(editor.visibleLines.find(".line:last").text()).toBe buffer.lineForRow(4) + describe "gutter rendering", -> beforeEach -> editor.attachToDom(heightInLines: 5.5) @@ -519,6 +651,21 @@ describe "Editor", -> expect(editor.gutter.find('.line-number:first').text()).toBe "2" expect(editor.gutter.find('.line-number:last').text()).toBe "7" + describe "width", -> + it "sets the width based on last line number", -> + expect(editor.gutter.lineNumbers.outerWidth()).toBe editor.charWidth * 2 + + it "updates the width when total number of lines gains a digit", -> + oneHundredLines = [0..100].join("\n") + editor.insertText(oneHundredLines) + expect(editor.gutter.lineNumbers.outerWidth()).toBe editor.charWidth * 3 + + describe "when the insertion of lines causes the editor to scroll", -> + it "renders line numbers correctly", -> + oneHundredLines = [0..100].join("\n") + editor.insertText(oneHundredLines) + expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 6 + describe "when wrapping is on", -> it "renders a • instead of line number for wrapped portions of lines", -> editor.setMaxLineLength(50) @@ -528,18 +675,18 @@ describe "Editor", -> expect(editor.gutter.find('.line-number:eq(5)').text()).toBe '5' describe "when there are folds", -> - it "skips line numbers", -> - editor.createFold([[3, 10], [5, 1]]) + it "skips line numbers covered by the fold and updates them when the fold changes", -> + editor.createFold(3, 5) expect(editor.gutter.find('.line-number:eq(3)').text()).toBe '4' expect(editor.gutter.find('.line-number:eq(4)').text()).toBe '7' - describe "when there is a fold on the last screen line of a wrapped line", -> - it "renders line numbers correctly when the fold is destroyed (regression)", -> - setEditorHeightInLines(editor, 20) - editor.setMaxLineLength(50) - fold = editor.createFold([[3, 52], [3, 56]]) - fold.destroy() - expect(editor.gutter.find('.line-number:last').text()).toBe '13' + buffer.insert([4,0], "\n\n") + expect(editor.gutter.find('.line-number:eq(3)').text()).toBe '4' + expect(editor.gutter.find('.line-number:eq(4)').text()).toBe '9' + + buffer.delete([[3,0], [6,0]]) + expect(editor.gutter.find('.line-number:eq(3)').text()).toBe '4' + expect(editor.gutter.find('.line-number:eq(4)').text()).toBe '6' describe "when the scrollView is scrolled to the right", -> it "adds a drop shadow to the gutter", -> @@ -567,53 +714,6 @@ describe "Editor", -> expect(editor.gutter.find('.line-number:first').text()).toBe "4" expect(editor.gutter.find('.line-number:last').text()).toBe "9" - describe ".scrollTop(n)", -> - beforeEach -> - editor.attachToDom(heightInLines: 5) - expect(editor.verticalScrollbar.scrollTop()).toBe 0 - expect(editor.visibleLines.css('-webkit-tranform')).toBeNull() - expect(editor.gutter.lineNumbers.css('-webkit-tranform')).toBeNull() - - describe "when called with a scroll top argument", -> - it "sets the scrollTop of the vertical scrollbar and sets a transform on the line numbers and lines", -> - editor.scrollTop(100) - expect(editor.verticalScrollbar.scrollTop()).toBe 100 - expect(editor.visibleLines.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -100)' - expect(editor.gutter.lineNumbers.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -100)' - - editor.scrollTop(150) - expect(editor.verticalScrollbar.scrollTop()).toBe 150 - expect(editor.visibleLines.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -150)' - expect(editor.gutter.lineNumbers.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -150)' - - it "does not allow negative scrollTops to be assigned", -> - editor.scrollTop(-100) - expect(editor.scrollTop()).toBe 0 - - it "doesn't do anything if the scrollTop hasn't changed", -> - editor.scrollTop(100) - spyOn(editor.verticalScrollbar, 'scrollTop') - spyOn(editor.visibleLines, 'css') - spyOn(editor.gutter.lineNumbers, 'css') - - editor.scrollTop(100) - expect(editor.verticalScrollbar.scrollTop).not.toHaveBeenCalled() - expect(editor.visibleLines.css).not.toHaveBeenCalled() - expect(editor.gutter.lineNumbers.css).not.toHaveBeenCalled() - - describe "when the 'adjustVerticalScrollbar' option is false (defaults to true)", -> - it "doesn't adjust the scrollTop of the vertical scrollbar", -> - editor.scrollTop(100, adjustVerticalScrollbar: false) - expect(editor.verticalScrollbar.scrollTop()).toBe 0 - expect(editor.visibleLines.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -100)' - expect(editor.gutter.lineNumbers.css('-webkit-transform')).toBe 'matrix(1, 0, 0, 1, 0, -100)' - - describe "when called with no argument", -> - it "returns the last assigned value or 0 if none has been assigned", -> - expect(editor.scrollTop()).toBe 0 - editor.scrollTop(50) - expect(editor.scrollTop()).toBe 50 - describe "font size", -> it "sets the initial font size based on the value assigned to the root view", -> rootView.setFontSize(20) @@ -897,46 +997,48 @@ describe "Editor", -> beforeEach -> setEditorWidthInChars(editor, 50) editor.setSoftWrap(true) - editor.createFold(new Range([3, 3], [3, 7])) + editor.createFold(2, 3) describe "when it is a single click", -> it "re-positions the cursor from the clicked screen position to the corresponding buffer position", -> expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [4, 7]) - expect(editor.getCursorBufferPosition()).toEqual(row: 3, column: 58) + editor.visibleLines.trigger mousedownEvent(editor: editor, point: [9, 0]) + expect(editor.getCursorBufferPosition()).toEqual(row: 8, column: 11) describe "when it is a double click", -> it "selects the word under the cursor", -> expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [4, 3], originalEvent: {detail: 1}) + editor.visibleLines.trigger mousedownEvent(editor: editor, point: [9, 0], originalEvent: {detail: 1}) editor.visibleLines.trigger 'mouseup' - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [4, 3], originalEvent: {detail: 2}) - expect(editor.getSelectedText()).toBe "right" + editor.visibleLines.trigger mousedownEvent(editor: editor, point: [9, 0], originalEvent: {detail: 2}) + expect(editor.getSelectedText()).toBe "sort" describe "when it is clicked more then twice (triple, quadruple, etc...)", -> it "selects the line under the cursor", -> expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) # Triple click - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [4, 3], originalEvent: {detail: 1}) + point = [9, 3] + editor.visibleLines.trigger mousedownEvent(editor: editor, point: point, originalEvent: {detail: 1}) editor.visibleLines.trigger 'mouseup' - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [4, 3], originalEvent: {detail: 2}) + editor.visibleLines.trigger mousedownEvent(editor: editor, point: point, originalEvent: {detail: 2}) editor.visibleLines.trigger 'mouseup' - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [4, 3], originalEvent: {detail: 3}) + editor.visibleLines.trigger mousedownEvent(editor: editor, point: point, originalEvent: {detail: 3}) editor.visibleLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe " var pivot = items.shift(), current, left = [], right = [];" + expect(editor.getSelectedText()).toBe " return sort(left).concat(pivot).concat(sort(right));" # Quad click - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [8, 3], originalEvent: {detail: 1}) + point = [12, 3] + editor.visibleLines.trigger mousedownEvent(editor: editor, point: point, originalEvent: {detail: 1}) editor.visibleLines.trigger 'mouseup' - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [8, 3], originalEvent: {detail: 2}) + editor.visibleLines.trigger mousedownEvent(editor: editor, point: point, originalEvent: {detail: 2}) editor.visibleLines.trigger 'mouseup' - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [8, 3], originalEvent: {detail: 3}) + editor.visibleLines.trigger mousedownEvent(editor: editor, point: point, originalEvent: {detail: 3}) editor.visibleLines.trigger 'mouseup' - editor.visibleLines.trigger mousedownEvent(editor: editor, point: [8, 3], originalEvent: {detail: 4}) + editor.visibleLines.trigger mousedownEvent(editor: editor, point: point, originalEvent: {detail: 4}) editor.visibleLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe " current < pivot ? left.push(current) : right.push(current);" + expect(editor.getSelectedText()).toBe " return sort(Array.apply(this, arguments));" describe "when soft-wrap is disabled", -> describe "when it is a single click", -> @@ -1176,13 +1278,6 @@ describe "Editor", -> editor.insertText("\n") expect(editor.buffer.lineForRow(2)).toEqual(" ") - describe "when a newline is inserted following a fold placeholder", -> - it "indents cursor based on the indentation of previous buffer line", -> - editor.createFold([[1, 10], [1, 30]]) - editor.setCursorBufferPosition([1, 30]) - editor.insertText("\n") - expect(editor.buffer.lineForRow(2)).toEqual(" ") - describe "when text beginning with a newline is inserted", -> it "indents cursor based on the indentation of previous buffer line", -> editor.setCursorBufferPosition([4, 29]) @@ -2332,47 +2427,83 @@ describe "Editor", -> describe "folding", -> beforeEach -> + editor.setBuffer(new Buffer(require.resolve('fixtures/two-hundred.txt'))) editor.attachToDom() describe "when a fold-selection event is triggered", -> - it "folds the selected text and moves the cursor to just after the placeholder, then treats the placeholder as a single character", -> + it "folds the lines covered by the selection into a single line with a fold class", -> editor.getSelection().setBufferRange(new Range([4, 29], [7, 4])) editor.trigger 'fold-selection' - expect(editor.visibleLines.find('.line:eq(4)').find('.fold-placeholder')).toExist() - expect(editor.visibleLines.find('.line:eq(5)').text()).toBe ' return sort(left).concat(pivot).concat(sort(right));' + expect(editor.visibleLines.find('.line:eq(4)')).toHaveClass('fold') + expect(editor.visibleLines.find('.line:eq(5)').text()).toBe '8' expect(editor.getSelection().isEmpty()).toBeTruthy() - expect(editor.getCursorScreenPosition()).toEqual [4, 32] + expect(editor.getCursorScreenPosition()).toEqual [5, 0] - editor.setCursorBufferPosition([9, 4]) - expect(editor.getCursorScreenPosition()).toEqual [6, 4] - - editor.insertText('x') - expect(editor.getCursorScreenPosition()).toEqual [6, 5] - expect(editor.getCursorBufferPosition()).toEqual [9, 5] - - editor.setCursorScreenPosition([4, 30]) - expect(editor.getCursorScreenPosition()).toEqual [4, 29] - editor.moveCursorRight() - expect(editor.getCursorScreenPosition()).toEqual [4, 32] - - describe "when a fold placeholder is clicked", -> + describe "when a fold placeholder line is clicked", -> it "removes the associated fold and places the cursor at its beginning", -> - editor.getSelection().setBufferRange(new Range([4, 29], [7, 4])) + editor.getSelection().setBufferRange(new Range([3, 0], [9, 0])) editor.trigger 'fold-selection' - editor.find('.fold-placeholder .ellipsis').mousedown() + editor.find('.fold.line').mousedown() - expect(editor.find('.fold-placeholder')).not.toExist() - expect(editor.visibleLines.find('.line:eq(5)').text()).toBe ' current = items.shift();' + expect(editor.find('.fold')).not.toExist() + expect(editor.visibleLines.find('.line:eq(4)').text()).toMatch /4-+/ + expect(editor.visibleLines.find('.line:eq(5)').text()).toMatch /5/ - expect(editor.getCursorBufferPosition()).toEqual [4, 29] + expect(editor.getCursorBufferPosition()).toEqual [3, 0] - describe "when there is nothing on a line except a fold placeholder", -> - it "follows the placeholder with a non-breaking space to ensure the line has the proper height", -> - editor.createFold([[1, 0], [1, 30]]) - expect(editor.visibleLines.find('.line:eq(1)').html()).toMatch / $/ + describe "when the unfold event is triggered when the cursor is on a fold placeholder line", -> + it "removes the associated fold and places the cursor at its beginning", -> + editor.getSelection().setBufferRange(new Range([3, 0], [9, 0])) + editor.trigger 'fold-selection' + + editor.setCursorBufferPosition([3,0]) + editor.trigger 'unfold' + + expect(editor.find('.fold')).not.toExist() + expect(editor.visibleLines.find('.line:eq(4)').text()).toMatch /4-+/ + expect(editor.visibleLines.find('.line:eq(5)').text()).toMatch /5/ + + expect(editor.getCursorBufferPosition()).toEqual [3, 0] + + describe "when a selection starts/stops intersecting a fold", -> + it "adds/removes the 'selected' class to the fold's line element and hides the cursor if it is on the fold line", -> + editor.createFold(2, 4) + + editor.setSelectionBufferRange([[1, 0], [2, 0]], reverse: true) + expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.selected') + + editor.setSelectionBufferRange([[1, 0], [1, 1]]) + expect(editor.lineElementForScreenRow(2)).not.toMatchSelector('.fold.selected') + + editor.setSelectionBufferRange([[1, 0], [5, 0]]) + expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.selected') + + editor.setCursorScreenPosition([3,0]) + expect(editor.lineElementForScreenRow(2)).not.toMatchSelector('.fold.selected') + + editor.setCursorScreenPosition([2,0]) + expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.selected') + expect(editor.find('.cursor').css('display')).toBe 'none' + + editor.setCursorScreenPosition([3,0]) + expect(editor.find('.cursor').css('display')).toBe 'block' + + describe "when a selected fold is scrolled into view (and the fold line was not previously rendered)", -> + it "renders the fold's line element with the 'selected' class", -> + setEditorHeightInLines(editor, 5) + + editor.createFold(2, 4) + editor.setSelectionBufferRange([[1, 0], [5, 0]]) + expect(editor.visibleLines.find('.fold.selected')).toExist() + + editor.scrollToBottom() + expect(editor.visibleLines.find('.fold.selected')).not.toExist() + + editor.scrollTop(0) + expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.selected') describe "editor-path-change event", -> it "emits event when buffer's path is changed", -> diff --git a/spec/app/line-map-spec.coffee b/spec/app/line-map-spec.coffee index 52e8bf3ad..9307e8144 100644 --- a/spec/app/line-map-spec.coffee +++ b/spec/app/line-map-spec.coffee @@ -34,7 +34,6 @@ describe "LineMap", -> expect(map.lineForScreenRow(0)).toEqual line1 expect(map.lineForScreenRow(1)).toEqual line0a.concat(line0b) - describe ".spliceAtBufferRow(bufferRow, rowCount, lineFragments)", -> describe "when called with a row count of 0", -> it "inserts the given line fragments before the specified buffer row", -> diff --git a/spec/app/renderer-spec.coffee b/spec/app/renderer-spec.coffee index 5feb36849..ef887fb6d 100644 --- a/spec/app/renderer-spec.coffee +++ b/spec/app/renderer-spec.coffee @@ -10,6 +10,13 @@ describe "Renderer", -> changeHandler = jasmine.createSpy 'changeHandler' renderer.on 'change', changeHandler + describe "when the buffer changes", -> + it "renders line numbers correctly", -> + originalLineCount = renderer.lineCount() + oneHundredLines = [0..100].join("\n") + buffer.insert([0,0], oneHundredLines) + expect(renderer.lineCount()).toBe 100 + originalLineCount + describe "soft wrapping", -> beforeEach -> renderer.setMaxLineLength(50) @@ -43,98 +50,6 @@ describe "Renderer", -> expect(renderer.lineForRow(3).text).toBe ' var pivot = items.shift(), current, left = [], ' expect(renderer.lineForRow(4).text).toBe 'right = [];' - describe "when a fold is created on the last screen line of a wrapped buffer line", -> - it "inserts the placeholder in the correct location and fires a change event", -> - fold = renderer.createFold([[3, 52], [3, 56]]) - expect(renderer.lineForRow(3).text).toBe ' var pivot = items.shift(), current, left = [], ' - expect(renderer.lineForRow(4).text).toBe 'r... = [];' - expect(renderer.lineForRow(5).text).toBe ' while(items.length > 0) {' - - expect(changeHandler).toHaveBeenCalled() - [[event]]= changeHandler.argsForCall - expect(event.oldRange).toEqual([[3, 0], [4, 11]]) - expect(event.newRange).toEqual([[3, 0], [4, 10]]) - - changeHandler.reset() - fold.destroy() - - expect(renderer.lineForRow(3).text).toBe ' var pivot = items.shift(), current, left = [], ' - expect(renderer.lineForRow(4).text).toBe 'right = [];' - expect(renderer.lineForRow(5).text).toBe ' while(items.length > 0) {' - - expect(changeHandler).toHaveBeenCalled() - [[event]]= changeHandler.argsForCall - expect(event.oldRange).toEqual([[3, 0], [4, 10]]) - expect(event.newRange).toEqual([[3, 0], [4, 11]]) - - describe "when a fold is created on the penultimate screen line of a wrapped buffer line", -> - beforeEach -> - renderer.setMaxLineLength(36) - changeHandler.reset() - - it "inserts the placeholder in the correct location and fires a change event", -> - fold = renderer.createFold([[6, 29], [6, 33]]) - expect(renderer.lineForRow(8).text).toBe " current < pivot ? " - expect(renderer.lineForRow(9).text).toBe "left....(current) : " - expect(renderer.lineForRow(10).text).toBe "right.push(current);" - - expect(changeHandler).toHaveBeenCalled() - [[event]]= changeHandler.argsForCall - expect(event.oldRange).toEqual([[8, 0], [10, 20]]) - expect(event.newRange).toEqual([[8, 0], [10, 20]]) - - changeHandler.reset() - fold.destroy() - - expect(changeHandler).toHaveBeenCalled() - [[event]]= changeHandler.argsForCall - expect(event.oldRange).toEqual([[8, 0], [10, 20]]) - expect(event.newRange).toEqual([[8, 0], [10, 20]]) - - describe "when a fold ends on the penultimate screen line of a wrapped buffer line", -> - beforeEach -> - renderer.setMaxLineLength(36) - changeHandler.reset() - - it "inserts the placeholder in the correct location and fires a change event", -> - fold = renderer.createFold([[5, 0], [6, 29]]) - expect(renderer.lineForRow(6).text).toBe " while(items.length > 0) {" - expect(renderer.lineForRow(7).text).toBe "...push(current) : " - expect(renderer.lineForRow(8).text).toBe "right.push(current);" - - expect(changeHandler).toHaveBeenCalled() - [[event]]= changeHandler.argsForCall - expect(event.oldRange).toEqual([[7, 0], [10, 20]]) - expect(event.newRange).toEqual([[7, 0], [8, 20]]) - - changeHandler.reset() - fold.destroy() - - expect(changeHandler).toHaveBeenCalled() - [[event]]= changeHandler.argsForCall - expect(event.oldRange).toEqual([[7, 0], [8, 20]]) - expect(event.newRange).toEqual([[7, 0], [10, 20]]) - - describe "when there is a fold placeholder straddling the max length boundary", -> - it "wraps the line before the fold placeholder", -> - renderer.createFold([[3, 49], [6, 1]]) - - expect(renderer.lineForRow(3).text).toBe ' var pivot = items.shift(), current, left = []' - expect(renderer.lineForRow(4).text).toBe '... current < pivot ? left.push(current) : ' - expect(renderer.lineForRow(5).text).toBe 'right.push(current);' - expect(renderer.lineForRow(6).text).toBe ' }' - - renderer.createFold([[6, 56], [8, 15]]) - expect(renderer.lineForRow(6).text).toBe 'right.push(...(left).concat(pivot).concat(sort(rig' - expect(renderer.lineForRow(7).text).toBe 'ht));' - expect(renderer.lineForRow(8).text).toBe ' };' - - describe "when there is a fold placeholder ending at the max length boundary", -> - it "wraps the line after the fold placeholder", -> - renderer.createFold([[3, 47], [3, 51]]) - expect(renderer.lineForRow(3).text).toBe ' var pivot = items.shift(), current, left = ...' - expect(renderer.lineForRow(4).text).toBe 'right = [];' - describe "when the buffer changes", -> describe "when buffer lines are updated", -> describe "when the update makes a soft-wrapped line shorter than the max line length", -> @@ -147,6 +62,7 @@ describe "Renderer", -> [[event]]= changeHandler.argsForCall expect(event.oldRange).toEqual([[7, 0], [8, 20]]) expect(event.newRange).toEqual([[7, 0], [7, 47]]) + expect(event.lineNumbersChanged).toBeTruthy() describe "when the update causes a line to softwrap an additional time", -> it "rewraps the line and emits a change event", -> @@ -160,6 +76,7 @@ describe "Renderer", -> [[event]] = changeHandler.argsForCall expect(event.oldRange).toEqual([[7, 0], [8, 20]]) expect(event.newRange).toEqual([[7, 0], [9, 20]]) + expect(event.lineNumbersChanged).toBeTruthy() describe "when buffer lines are inserted", -> it "inserts / updates wrapped lines and emits a change event", -> @@ -173,6 +90,7 @@ describe "Renderer", -> [event] = changeHandler.argsForCall[0] expect(event.oldRange).toEqual([[7, 0], [8, 20]]) expect(event.newRange).toEqual([[7, 0], [10, 20]]) + expect(event.lineNumbersChanged).toBeTruthy() describe "when buffer lines are removed", -> it "removes lines and emits a change event", -> @@ -186,6 +104,7 @@ describe "Renderer", -> [event] = changeHandler.argsForCall[0] expect(event.oldRange).toEqual([[3, 0], [11, 45]]) expect(event.newRange).toEqual([[3, 0], [5, 45]]) + expect(event.lineNumbersChanged).toBeTruthy() describe "position translation", -> it "translates positions accounting for wrapped lines", -> @@ -220,335 +139,325 @@ describe "Renderer", -> [event] = changeHandler.argsForCall[0] expect(event.oldRange).toEqual([[0, 0], [15, 2]]) expect(event.newRange).toEqual([[0, 0], [18, 2]]) + expect(event.lineNumbersChanged).toBeTruthy() describe "folding", -> + beforeEach -> + buffer = new Buffer(require.resolve 'fixtures/two-hundred.txt') + renderer = new Renderer(buffer, {tabText}) + renderer.on 'change', changeHandler + describe "when folds are created and destroyed", -> describe "when a fold spans multiple lines", -> - it "replaces the lines spanned by the fold with a single line containing a placeholder", -> - previousLine4Text = renderer.lineForRow(4).text - previousLine5Text = renderer.lineForRow(5).text + it "replaces the lines spanned by the fold with a placeholder that references the fold object", -> + fold = renderer.createFold(4, 7) - fold = renderer.createFold([[4, 29], [7, 4]]) - - expect(renderer.lineForRow(4).text).toBe ' while(items.length > 0) {...}' - expect(renderer.lineForRow(5).text).toBe ' return sort(left).concat(pivot).concat(sort(right));' + [line4, line5] = renderer.linesForRows(4, 5) + expect(line4.fold).toBe fold + expect(line4.text).toMatch /^4-+/ + expect(line4.bufferDelta).toEqual [4, 0] + expect(line4.screenDelta).toEqual [1, 0] + expect(line5.text).toBe '8' expect(changeHandler).toHaveBeenCalled() [event] = changeHandler.argsForCall[0] - expect(event.oldRange).toEqual [[4, 0], [7, 5]] - expect(event.newRange).toEqual [[4, 0], [4, 33]] + expect(event.oldRange).toEqual [[4, 0], [7, 1]] + expect(event.newRange).toEqual [[4, 0], [4, 101]] + expect(event.lineNumbersChanged).toBeTruthy() changeHandler.reset() fold.destroy() - expect(renderer.lineForRow(4).text).toBe previousLine4Text - expect(renderer.lineForRow(5).text).toBe previousLine5Text + [line4, line5] = renderer.linesForRows(4, 5) + expect(line4.fold).toBeUndefined() + expect(line4.text).toMatch /^4-+/ + expect(line4.bufferDelta).toEqual [1, 0] + expect(line4.screenDelta).toEqual [1, 0] + expect(line5.text).toBe '5' expect(changeHandler).toHaveBeenCalled() [[event]] = changeHandler.argsForCall - expect(event.oldRange).toEqual [[4, 0], [4, 33]] - expect(event.newRange).toEqual [[4, 0], [7, 5]] + expect(event.oldRange).toEqual [[4, 0], [4, 101]] + expect(event.newRange).toEqual [[4, 0], [7, 1]] + expect(event.lineNumbersChanged).toBeTruthy() describe "when a fold spans a single line", -> - it "renders a placeholder for the folded region, but does not skip any lines", -> - fold = renderer.createFold([[2, 8], [2, 25]]) + it "renders a fold placeholder for the folded line but does not skip any lines", -> + fold = renderer.createFold(4, 4) - [line2, line3] = renderer.linesForRows(2, 3) - expect(line2.text).toBe ' if (...) return items;' - expect(line3.text).toBe ' var pivot = items.shift(), current, left = [], right = [];' + [line4, line5] = renderer.linesForRows(4, 5) + expect(line4.fold).toBe fold + expect(line4.text).toMatch /^4-+/ + expect(line4.bufferDelta).toEqual [1, 0] + expect(line4.screenDelta).toEqual [1, 0] + expect(line5.text).toBe '5' expect(changeHandler).toHaveBeenCalled() [[event]] = changeHandler.argsForCall - expect(event.oldRange).toEqual [[2, 0], [2, 40]] - expect(event.newRange).toEqual [[2, 0], [2, 26]] + expect(event.oldRange).toEqual [[4, 0], [4, 101]] + expect(event.newRange).toEqual [[4, 0], [4, 101]] + + # Line numbers don't actually change, but it's not worth the complexity to have this + # be false for single line folds since they are so rare + expect(event.lineNumbersChanged).toBeTruthy() changeHandler.reset() fold.destroy() - [line2, line3] = renderer.linesForRows(2, 3) - expect(line2.text).toBe ' if (items.length <= 1) return items;' - expect(line3.text).toBe ' var pivot = items.shift(), current, left = [], right = [];' + [line4, line5] = renderer.linesForRows(4, 5) + expect(line4.fold).toBeUndefined() + expect(line4.text).toMatch /^4-+/ + expect(line4.bufferDelta).toEqual [1, 0] + expect(line4.screenDelta).toEqual [1, 0] + expect(line5.text).toBe '5' expect(changeHandler).toHaveBeenCalled() [[event]] = changeHandler.argsForCall - expect(event.newRange).toEqual [[2, 0], [2, 40]] - expect(event.oldRange).toEqual [[2, 0], [2, 26]] + expect(event.oldRange).toEqual [[4, 0], [4, 101]] + expect(event.newRange).toEqual [[4, 0], [4, 101]] + expect(event.lineNumbersChanged).toBeTruthy() changeHandler.reset() describe "when a fold is nested within another fold", -> - it "only renders the placeholder for the inner fold when the outer fold is destroyed", -> - outerFold = renderer.createFold([[4, 29], [8, 36]]) - innerFold = renderer.createFold([[8, 5], [8, 10]]) + it "does not render the placeholder for the inner fold until the outer fold is destroyed", -> + innerFold = renderer.createFold(6, 7) + outerFold = renderer.createFold(4, 8) [line4, line5] = renderer.linesForRows(4, 5) - expect(line4.text).toBe ' while(items.length > 0) {...concat(sort(right));' - expect(line5.text).toBe ' };' + expect(line4.fold).toBe outerFold + expect(line4.text).toMatch /4-+/ + expect(line4.bufferDelta).toEqual [5, 0] + expect(line4.screenDelta).toEqual [1, 0] + expect(line5.text).toMatch /9-+/ outerFold.destroy() - [line4, line5] = renderer.linesForRows(4, 5) - expect(line4.text).toBe ' while(items.length > 0) {' - expect(line5.text).toBe ' current = items.shift();' - expect(renderer.lineForRow(8).text).toBe ' r... sort(left).concat(pivot).concat(sort(right));' + [line4, line5, line6, line7] = renderer.linesForRows(4, 7) + expect(line4.fold).toBeUndefined() + expect(line4.text).toMatch /^4-+/ + expect(line4.bufferDelta).toEqual [1, 0] + expect(line4.screenDelta).toEqual [1, 0] + expect(line5.text).toBe '5' + expect(line6.fold).toBe innerFold + expect(line6.text).toBe '6' + expect(line6.bufferDelta).toEqual [2, 0] + expect(line6.screenDelta).toEqual [1, 0] + expect(line7.text).toBe '8' it "allows the outer fold to start at the same location as the inner fold", -> - renderer.createFold([[4, 29], [7, 4]]) - renderer.createFold([[4, 29], [9, 2]]) - expect(renderer.lineForRow(4).text).toBe " while(items.length > 0) {...};" + innerFold = renderer.createFold(4, 6) + outerFold = renderer.createFold(4, 8) - describe "when a fold begins on the line on which another fold ends", -> - describe "when the second fold is created before the first fold", -> - it "renders a placeholder for both folds on the first line of the first fold", -> - fold1 = renderer.createFold([[7, 5], [8, 36]]) - fold2 = renderer.createFold([[4, 29], [7, 4]]) - - [line4, line5] = renderer.linesForRows(4, 5) - expect(line4.text).toBe ' while(items.length > 0) {...}...concat(sort(right));' - expect(line5.text).toBe ' };' - - expect(changeHandler.callCount).toBe 2 - [[event1], [event2]] = changeHandler.argsForCall - expect(event1.oldRange).toEqual [[7, 0], [8, 56]] - expect(event1.newRange).toEqual [[7, 0], [7, 28]] - expect(event2.oldRange).toEqual [[4, 0], [7, 28]] - expect(event2.newRange).toEqual [[4, 0], [4, 56]] - changeHandler.reset() - - fold1.destroy() - [line4, line5] = renderer.linesForRows(4, 5) - expect(line4.text).toBe ' while(items.length > 0) {...}' - expect(line5.text).toBe ' return sort(left).concat(pivot).concat(sort(right));' - - expect(changeHandler).toHaveBeenCalled() - [event] = changeHandler.argsForCall[0] - expect(event.oldRange).toEqual [[4, 0], [4, 56]] - expect(event.newRange).toEqual [[4, 0], [5, 56]] - changeHandler.reset() - - fold2.destroy() - [line4, line5] = renderer.linesForRows(4, 5) - expect(line4.text).toBe ' while(items.length > 0) {' - expect(line5.text).toBe ' current = items.shift();' - - expect(changeHandler).toHaveBeenCalled() - [event] = changeHandler.argsForCall[0] - expect(event.oldRange).toEqual [[4, 0], [4, 33]] - expect(event.newRange).toEqual [[4, 0], [7, 5]] - - describe "when the second fold is created after the first fold", -> - it "renders a placeholder for both folds on the first line of the first fold", -> - fold1 = renderer.createFold([[4, 29], [7, 4]]) - fold2 = renderer.createFold([[7, 5], [8, 36]]) - [line4, line5] = renderer.linesForRows(4, 5) - expect(line4.text).toBe ' while(items.length > 0) {...}...concat(sort(right));' - expect(line5.text).toBe ' };' - - expect(changeHandler.callCount).toBe 2 - [[event1], [event2]] = changeHandler.argsForCall - expect(event1.oldRange).toEqual [[4, 0], [7, 5]] - expect(event1.newRange).toEqual [[4, 0], [4, 33]] - expect(event2.oldRange).toEqual [[4, 0], [5, 56]] - expect(event2.newRange).toEqual [[4, 0], [4, 56]] - changeHandler.reset() - - fold1.destroy() - [line4, line5] = renderer.linesForRows(4, 5) - [line7] = renderer.linesForRows(7, 7) - expect(line4.text).toBe ' while(items.length > 0) {' - expect(line5.text).toBe ' current = items.shift();' - expect(line7.text).toBe ' }...concat(sort(right));' - - expect(changeHandler).toHaveBeenCalled() - [event] = changeHandler.argsForCall[0] - expect(event.oldRange).toEqual [[4, 0], [4, 56]] - expect(event.newRange).toEqual [[4, 0], [7, 28]] - changeHandler.reset() - - fold2.destroy() - [line4, line5] = renderer.linesForRows(4, 5) - expect(line4.text).toBe ' while(items.length > 0) {' - expect(line5.text).toBe ' current = items.shift();' - - expect(changeHandler).toHaveBeenCalled() - [event] = changeHandler.argsForCall[0] - expect(event.oldRange).toEqual [[7, 0], [7, 28]] - expect(event.newRange).toEqual [[7, 0], [8, 56]] - - describe "when a fold starts at the beginning of a line", -> - it "renders a placeholder at the beginning of the line", -> - renderer.createFold([[4, 0], [7, 4]]) - expect(renderer.lineForRow(4).text).toBe '...}' - - describe "when a fold ends at the beginning of a line", -> - it "renders a placeholder at the beginning of the line", -> - renderer.createFold([[4, 29], [7, 0]]) - expect(renderer.lineForRow(4).text).toBe ' while(items.length > 0) {... }' - - describe "when a fold starts on the first line of the buffer", -> - it "renders the first line correctly when the fold is destroyed", -> - fold = renderer.createFold([[0, 14], [0, 27]]) - fold.destroy() - expect(renderer.lineForRow(0).text).toBe 'var quicksort = function () {' - - describe "when a fold causes a wrapped line to become shorter than the max line length", -> - it "unwraps the line", -> - renderer.setMaxLineLength(50) - renderer.createFold([[3, 0], [3, 15]]) - expect(renderer.lineForRow(3).text).toBe '... items.shift(), current, left = [], right = [];' - expect(renderer.lineForRow(4).text).toBe ' while(items.length > 0) {' + [line4, line5] = renderer.linesForRows(4, 5) + expect(line4.fold).toBe outerFold + expect(line4.text).toMatch /4-+/ + expect(line4.bufferDelta).toEqual [5, 0] + expect(line4.screenDelta).toEqual [1, 0] + expect(line5.text).toMatch /9-+/ describe "when the buffer changes", -> [fold1, fold2] = [] beforeEach -> - fold1 = renderer.createFold([[4, 29], [7, 4]]) - fold2 = renderer.createFold([[7, 5], [8, 36]]) + fold1 = renderer.createFold(2, 4) + fold2 = renderer.createFold(6, 8) changeHandler.reset() - describe "when the old range precedes lines with a fold", -> - it "updates the buffer and re-positions subsequent folds", -> - buffer.change([[1, 5], [2, 10]], 'abc') - - expect(renderer.lineForRow(1).text).toBe ' varabcems.length <= 1) return items;' - expect(renderer.lineForRow(3).text).toBe ' while(items.length > 0) {...}...concat(sort(right));' - - expect(changeHandler).toHaveBeenCalled() - [[event]] = changeHandler.argsForCall - expect(event.oldRange).toEqual [[1, 0], [2, 40]] - expect(event.newRange).toEqual [[1, 0], [1, 38]] - changeHandler.reset() - - fold1.destroy() - expect(renderer.lineForRow(3).text).toBe ' while(items.length > 0) {' - expect(renderer.lineForRow(6).text).toBe ' }...concat(sort(right));' - - expect(changeHandler).toHaveBeenCalled() - [[event]] = changeHandler.argsForCall - expect(event.oldRange).toEqual [[3, 0], [3, 56]] - expect(event.newRange).toEqual [[3, 0], [6, 28]] - - describe "when the old range follows lines with a fold", -> - it "re-positions the screen ranges for the change event based on the preceding fold", -> - buffer.change([[9, 3], [10, 0]], 'abc') - - expect(renderer.lineForRow(5).text).toBe ' }abc' - expect(renderer.lineForRow(6).text).toBe ' return sort(Array.apply(this, arguments));' - - expect(changeHandler).toHaveBeenCalled() - [[event]] = changeHandler.argsForCall - expect(event.oldRange).toEqual [[5, 0], [6, 0]] - expect(event.newRange).toEqual [[5, 0], [5, 6]] - - describe "when the old range contains unfolded text on the first line of a fold, preceding the fold placeholder", -> - it "re-renders the line with the placeholder and re-positions the fold", -> - buffer.change([[4, 4], [4, 9]], 'slongaz') - - expect(renderer.lineForRow(4).text).toBe ' slongaz(items.length > 0) {...}...concat(sort(right));' - expect(changeHandler).toHaveBeenCalled() - [[event]] = changeHandler.argsForCall - expect(event.oldRange).toEqual [[4, 0], [4, 56]] - expect(event.newRange).toEqual [[4, 0], [4, 58]] - - fold1.destroy() - expect(renderer.lineForRow(4).text).toBe ' slongaz(items.length > 0) {' - - describe "when the old range is contained to a single line in-between two fold placeholders", -> - it "re-renders the line with the placeholder and re-positions the second fold", -> - buffer.insert([7, 4], 'abc') - expect(renderer.lineForRow(4).text).toBe ' while(items.length > 0) {...abc}...concat(sort(right));' - expect(changeHandler).toHaveBeenCalled() - [[event]] = changeHandler.argsForCall - expect(event.oldRange).toEqual [[4, 0], [4, 56]] - expect(event.newRange).toEqual [[4, 0], [4, 59]] - - fold2.destroy() - - expect(renderer.lineForRow(4).text).toBe ' while(items.length > 0) {...abc}' - - describe "when the old range is inside a fold", -> - it "does not trigger a change event, but updates the fold and ensures the change is present when the fold is destroyed", -> - buffer.change([[4, 29], [6, 0]], 'abc') - - expect(renderer.lineForRow(4).text).toBe ' while(items.length > 0) {...}...concat(sort(right));' - expect(changeHandler).not.toHaveBeenCalled() - - fold1.destroy() - expect(renderer.lineForRow(4).text).toBe ' while(items.length > 0) {abc current < pivot ? left.push(current) : right.push(current);' - expect(renderer.lineForRow(5).text).toBe ' }...concat(sort(right));' - - expect(changeHandler).toHaveBeenCalled() - [[event]] = changeHandler.argsForCall - expect(event.oldRange).toEqual [[4, 0], [4, 56]] - expect(event.newRange).toEqual [[4, 0], [5, 28]] - describe "when the old range surrounds a fold", -> - it "removes the fold and replaces the fold placeholder with the new text", -> - buffer.change([[4, 29], [7, 4]], 'party()') + it "removes the fold and replaces the selection with the new text", -> + buffer.change([[1, 0], [5, 1]], 'party!') + + expect(renderer.lineForRow(0).text).toBe "0" + expect(renderer.lineForRow(1).text).toBe "party!" + expect(renderer.lineForRow(2).fold).toBe fold2 + expect(renderer.lineForRow(3).text).toMatch /^9-+/ - expect(renderer.lineForRow(4).text).toBe ' while(items.length > 0) {party()}...concat(sort(right));' expect(changeHandler).toHaveBeenCalled() [[event]] = changeHandler.argsForCall - expect(event.oldRange).toEqual [[4, 0], [4, 56]] - expect(event.newRange).toEqual [[4, 0], [4, 60]] + expect(event.oldRange).toEqual [[1, 0], [3, 1]] + expect(event.newRange).toEqual [[1, 0], [1, 6]] + expect(event.lineNumbersChanged).toBeTruthy() describe "when the old range surrounds two nested folds", -> - it "removes both folds and replaces the fold placeholder with the new text", -> - renderer.createFold([[4, 25], [7, 5]]) - buffer.change([[4, 25], [7, 5]], '4)') + it "removes both folds and replaces the selection with the new text", -> + renderer.createFold(2, 9) + changeHandler.reset() - expect(renderer.lineForRow(4).text).toBe ' while(items.length > 4)...concat(sort(right));' - # expect(changeHandler).toHaveBeenCalled() - # [[event]] = changeHandler.argsForCall - # expect(event.oldRange).toEqual [[4, 0], [4, 56]] - # expect(event.newRange).toEqual [[4, 0], [4, 60]] + buffer.change([[1, 0], [10, 0]], 'goodbye') + expect(renderer.lineForRow(0).text).toBe "0" + expect(renderer.lineForRow(1).text).toBe "goodbye10" + expect(renderer.lineForRow(2).text).toBe "11" + + expect(changeHandler).toHaveBeenCalled() + [[event]] = changeHandler.argsForCall + expect(event.oldRange).toEqual [[1, 0], [3, 2]] + expect(event.newRange).toEqual [[1, 0], [1, 9]] + expect(event.lineNumbersChanged).toBeTruthy() + + describe "when multiple changes happen above the fold", -> + it "repositions folds correctly", -> + buffer.delete([[1, 1], [2, 0]]) + buffer.insert([0, 1], "\nnew") + + expect(fold1.startRow).toBe 2 + expect(fold1.endRow).toBe 4 + + describe "when the old range precedes lines with a fold", -> + describe "when the new range precedes lines with a fold", -> + it "updates the buffer and re-positions subsequent folds", -> + buffer.change([[0, 0], [1, 1]], 'abc') + + expect(renderer.lineForRow(0).text).toBe "abc" + expect(renderer.lineForRow(1).fold).toBe fold1 + expect(renderer.lineForRow(2).text).toBe "5" + expect(renderer.lineForRow(3).fold).toBe fold2 + expect(renderer.lineForRow(4).text).toMatch /^9-+/ + + expect(changeHandler).toHaveBeenCalled() + [[event]] = changeHandler.argsForCall + expect(event.oldRange).toEqual [[0, 0], [1, 1]] + expect(event.newRange).toEqual [[0, 0], [0, 3]] + expect(event.lineNumbersChanged).toBeTruthy() + changeHandler.reset() + + fold1.destroy() + expect(renderer.lineForRow(0).text).toBe "abc" + expect(renderer.lineForRow(1).text).toBe "2" + expect(renderer.lineForRow(3).text).toMatch /^4-+/ + expect(renderer.lineForRow(4).text).toBe "5" + expect(renderer.lineForRow(5).fold).toBe fold2 + expect(renderer.lineForRow(6).text).toMatch /^9-+/ + + expect(changeHandler).toHaveBeenCalled() + [[event]] = changeHandler.argsForCall + expect(event.oldRange).toEqual [[1, 0], [1, 1]] + expect(event.newRange).toEqual [[1, 0], [3, 101]] + expect(event.lineNumbersChanged).toBeTruthy() + + describe "when the old range straddles the beginning of a fold", -> + it "replaces lines in the portion of the range that precedes the fold and adjusts the end of the fold to encompass additional lines", -> + buffer.change([[1, 1], [3, 0]], "a\nb\nc\nd\n") + + expect(fold1.startRow).toBe 2 + expect(fold1.endRow).toBe 6 + + expect(renderer.lineForRow(1).text).toBe '1a' + expect(renderer.lineForRow(2).text).toBe 'b' + expect(renderer.lineForRow(2).fold).toBe fold1 + + describe "when the old range follows a fold", -> + it "re-positions the screen ranges for the change event based on the preceding fold", -> + buffer.change([[10, 0], [11, 0]], 'abc') + + expect(renderer.lineForRow(1).text).toBe "1" + expect(renderer.lineForRow(2).fold).toBe fold1 + expect(renderer.lineForRow(3).text).toBe "5" + expect(renderer.lineForRow(4).fold).toBe fold2 + expect(renderer.lineForRow(5).text).toMatch /^9-+/ + + expect(changeHandler).toHaveBeenCalled() + [[event]] = changeHandler.argsForCall + expect(event.oldRange).toEqual [[6, 0], [7, 2]] # Expands ranges to encompes entire line + expect(event.newRange).toEqual [[6, 0], [6, 5]] + expect(event.lineNumbersChanged).toBeTruthy() + + describe "when the old range is inside a fold", -> + describe "when the end of the new range precedes the end of the fold", -> + it "updates the fold and ensures the change is present when the fold is destroyed", -> + buffer.insert([3, 0], '\n') + expect(fold1.startRow).toBe 2 + expect(fold1.endRow).toBe 5 + + expect(renderer.lineForRow(1).text).toBe "1" + expect(renderer.lineForRow(2).text).toBe "2" + expect(renderer.lineForRow(2).fold).toBe fold1 + expect(renderer.lineForRow(2).bufferDelta).toEqual [4, 0] + expect(renderer.lineForRow(3).text).toMatch "5" + expect(renderer.lineForRow(4).fold).toBe fold2 + expect(renderer.lineForRow(5).text).toMatch /^9-+/ + + expect(changeHandler).toHaveBeenCalled() + [[event]] = changeHandler.argsForCall + expect(event.oldRange).toEqual [[2, 0], [2, 1]] + expect(event.newRange).toEqual [[2, 0], [2, 1]] + expect(event.lineNumbersChanged).toBeTruthy() + + describe "when the end of the new range exceeds the end of the fold", -> + it "expands the fold to contain all the inserted lines", -> + buffer.change([[3, 0], [4, 0]], 'a\nb\nc\nd\n') + expect(fold1.startRow).toBe 2 + expect(fold1.endRow).toBe 7 + + expect(renderer.lineForRow(1).text).toBe "1" + expect(renderer.lineForRow(2).text).toBe "2" + expect(renderer.lineForRow(2).fold).toBe fold1 + expect(renderer.lineForRow(2).bufferDelta).toEqual [6, 0] + expect(renderer.lineForRow(3).text).toMatch "5" + expect(renderer.lineForRow(4).fold).toBe fold2 + expect(renderer.lineForRow(5).text).toMatch /^9-+/ + + expect(changeHandler).toHaveBeenCalled() + [[event]] = changeHandler.argsForCall + expect(event.oldRange).toEqual [[2, 0], [2, 1]] + expect(event.newRange).toEqual [[2, 0], [2, 1]] + expect(event.lineNumbersChanged).toBeTruthy() + + describe "when the old range straddles the end of the fold", -> + describe "when the end of the new range precedes the end of the fold", -> + it "shortens the fold so its end matches the end of the new range", -> + fold2.destroy() + buffer.change([[3, 0], [6, 0]], 'a\n') + + expect(fold1.startRow).toBe 2 + expect(fold1.endRow).toBe 4 + + describe "when the old range is contained to a single line in-between two folds", -> + it "re-renders the line with the placeholder and re-positions the second fold", -> + buffer.insert([5, 0], 'abc\n') + + expect(renderer.lineForRow(1).text).toBe "1" + expect(renderer.lineForRow(2).fold).toBe fold1 + expect(renderer.lineForRow(3).text).toMatch "abc" + expect(renderer.lineForRow(4).text).toBe "5" + expect(renderer.lineForRow(5).fold).toBe fold2 + expect(renderer.lineForRow(6).text).toMatch /^9-+/ + + expect(changeHandler).toHaveBeenCalled() + [[event]] = changeHandler.argsForCall + expect(event.oldRange).toEqual [[3, 0], [3, 1]] + expect(event.newRange).toEqual [[3, 0], [4, 1]] + expect(event.lineNumbersChanged).toBeTruthy() describe "position translation", -> - describe "when there is single fold spanning multiple lines", -> - it "translates positions to account for folded lines and characters and the placeholder", -> - renderer.createFold([[4, 29], [7, 4]]) + it "translates positions to account for folded lines and characters and the placeholder", -> + renderer.createFold(4, 7) - # preceding fold: identity - expect(renderer.screenPositionForBufferPosition([3, 0])).toEqual [3, 0] - expect(renderer.screenPositionForBufferPosition([4, 0])).toEqual [4, 0] - expect(renderer.screenPositionForBufferPosition([4, 29])).toEqual [4, 29] + # preceding fold: identity + expect(renderer.screenPositionForBufferPosition([3, 0])).toEqual [3, 0] + expect(renderer.screenPositionForBufferPosition([4, 0])).toEqual [4, 0] - expect(renderer.bufferPositionForScreenPosition([3, 0])).toEqual [3, 0] - expect(renderer.bufferPositionForScreenPosition([4, 0])).toEqual [4, 0] - expect(renderer.bufferPositionForScreenPosition([4, 29])).toEqual [4, 29] + expect(renderer.bufferPositionForScreenPosition([3, 0])).toEqual [3, 0] + expect(renderer.bufferPositionForScreenPosition([4, 0])).toEqual [4, 0] - # inside of fold: translate to the start of the fold - expect(renderer.screenPositionForBufferPosition([4, 35])).toEqual [4, 29] - expect(renderer.screenPositionForBufferPosition([5, 5])).toEqual [4, 29] + # inside of fold: translate to the start of the fold + expect(renderer.screenPositionForBufferPosition([4, 35])).toEqual [4, 0] + expect(renderer.screenPositionForBufferPosition([5, 5])).toEqual [4, 0] - # following fold, on last line of fold - expect(renderer.screenPositionForBufferPosition([7, 4])).toEqual [4, 32] - expect(renderer.bufferPositionForScreenPosition([4, 32])).toEqual [7, 4] + # following fold + expect(renderer.screenPositionForBufferPosition([8, 0])).toEqual [5, 0] + expect(renderer.screenPositionForBufferPosition([11, 2])).toEqual [8, 2] - # # following fold, subsequent line - expect(renderer.screenPositionForBufferPosition([8, 0])).toEqual [5, 0] - expect(renderer.screenPositionForBufferPosition([11, 13])).toEqual [8, 13] + expect(renderer.bufferPositionForScreenPosition([5, 0])).toEqual [8, 0] + expect(renderer.bufferPositionForScreenPosition([9, 2])).toEqual [12, 2] - expect(renderer.bufferPositionForScreenPosition([5, 0])).toEqual [8, 0] - expect(renderer.bufferPositionForScreenPosition([9, 2])).toEqual [12, 2] + describe ".destroyFoldsContainingBufferRow(row)", -> + describe "when two folds start on the given buffer row", -> + it "destroys both folds", -> + renderer.createFold(2, 4) + renderer.createFold(2, 6) - describe "when there is a single fold spanning a single line", -> - it "translates positions to account for folded characters and the placeholder", -> - renderer.createFold([[4, 10], [4, 15]]) - - expect(renderer.screenPositionForBufferPosition([4, 5])).toEqual [4, 5] - expect(renderer.bufferPositionForScreenPosition([4, 5])).toEqual [4, 5] - - expect(renderer.screenPositionForBufferPosition([4, 15])).toEqual [4, 13] - expect(renderer.bufferPositionForScreenPosition([4, 13])).toEqual [4, 15] - - expect(renderer.screenPositionForBufferPosition([4, 20])).toEqual [4, 18] - expect(renderer.bufferPositionForScreenPosition([4, 18])).toEqual [4, 20] - - describe "when there is a fold on a wrapped line", -> - it "translates positions accounting for both the fold and the wrapped line", -> - renderer.setMaxLineLength(50) - renderer.createFold([[3, 51], [3, 58]]) - expect(renderer.screenPositionForBufferPosition([3, 58])).toEqual [4, 3] - expect(renderer.bufferPositionForScreenPosition([4, 3])).toEqual [3, 58] + expect(renderer.lineForRow(3).text).toBe '7' + renderer.destroyFoldsContainingBufferRow(2) + expect(renderer.lineForRow(3).text).toBe '3' describe ".clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, skipAtomicTokens: false)", -> beforeEach -> @@ -579,6 +488,10 @@ describe "Renderer", -> expect(renderer.clipScreenPosition([0, 30], wrapBeyondNewlines: true)).toEqual [1, 0] expect(renderer.clipScreenPosition([0, 1000], wrapBeyondNewlines: true)).toEqual [1, 0] + it "wraps positions in the middle of fold lines to the next screen line", -> + renderer.createFold(3, 5) + expect(renderer.clipScreenPosition([3, 5], wrapBeyondNewlines: true)).toEqual [4, 0] + describe "when wrapAtSoftNewlines is false (the default)", -> it "clips positions at the end of soft-wrapped lines to the character preceding the end of the line", -> expect(renderer.clipScreenPosition([3, 50])).toEqual [3, 50] @@ -586,11 +499,6 @@ describe "Renderer", -> expect(renderer.clipScreenPosition([3, 58])).toEqual [3, 50] expect(renderer.clipScreenPosition([3, 1000])).toEqual [3, 50] - describe "if there is a fold placeholder at the very end of the screen line", -> - it "clips positions at the end of the screen line to the position preceding the placeholder", -> - renderer.createFold([[3, 47], [3, 51]]) - expect(renderer.clipScreenPosition([3, 50])).toEqual [3, 47] - describe "when wrapAtSoftNewlines is true", -> it "wraps positions at the end of soft-wrapped lines to the next screen line", -> expect(renderer.clipScreenPosition([3, 50], wrapAtSoftNewlines: true)).toEqual [3, 50] @@ -599,12 +507,6 @@ describe "Renderer", -> expect(renderer.clipScreenPosition([3, 1000], wrapAtSoftNewlines: true)).toEqual [4, 0] describe "when skipAtomicTokens is false (the default)", -> - it "clips screen positions in the middle of fold placeholders to the to the beginning of fold placeholders", -> - renderer.createFold([[3, 55], [3, 59]]) - expect(renderer.clipScreenPosition([4, 5])).toEqual [4, 4] - expect(renderer.clipScreenPosition([4, 6])).toEqual [4, 4] - expect(renderer.clipScreenPosition([4, 7])).toEqual [4, 7] - it "clips screen positions in the middle of atomic tab characters to the beginning of the character", -> buffer.insert([0, 0], '\t') expect(renderer.clipScreenPosition([0, 0])).toEqual [0, 0] @@ -612,13 +514,7 @@ describe "Renderer", -> expect(renderer.clipScreenPosition([0, tabText.length])).toEqual [0, tabText.length] describe "when skipAtomicTokens is true", -> - it "wraps the screen positions in the middle of fold placeholders to the end of the placeholder", -> - renderer.createFold([[3, 55], [3, 59]]) - expect(renderer.clipScreenPosition([4, 4], skipAtomicTokens: true)).toEqual [4, 4] - expect(renderer.clipScreenPosition([4, 5], skipAtomicTokens: true)).toEqual [4, 7] - expect(renderer.clipScreenPosition([4, 6], skipAtomicTokens: true)).toEqual [4, 7] - - it "clips screen positions in the middle of atomic tab characters to the beginning of the character", -> + it "clips screen positions in the middle of atomic tab characters to the end of the character", -> buffer.insert([0, 0], '\t') expect(renderer.clipScreenPosition([0, 0], skipAtomicTokens: true)).toEqual [0, 0] expect(renderer.clipScreenPosition([0, 1], skipAtomicTokens: true)).toEqual [0, tabText.length] @@ -627,5 +523,5 @@ describe "Renderer", -> describe ".bufferRowsForScreenRows()", -> it "returns the buffer rows corresponding to each screen row in the given range", -> renderer.setMaxLineLength(50) - renderer.createFold([[4, 29], [7, 4]]) + renderer.createFold(4, 7) expect(renderer.bufferRowsForScreenRows()).toEqual [0, 1, 2, 3, 3, 4, 8, 8, 9, 10, 11, 12] diff --git a/spec/app/selection-spec.coffee b/spec/app/selection-spec.coffee index 2526e71c1..e3d699f8d 100644 --- a/spec/app/selection-spec.coffee +++ b/spec/app/selection-spec.coffee @@ -273,3 +273,31 @@ describe "Selection", -> expect(editor.buffer.lineForRow(1)).toBe "var sort = function(items) {" expect(editor.buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" expect(selection.getBufferRange()).toEqual [[0, 1], [3, 15 - tabLength]] + + describe "when the selection ends on the begining of a fold line", -> + beforeEach -> + editor.createFold(2,4) + editor.createFold(2,6) + + describe "inserting text", -> + it "destroys the fold", -> + selection.setBufferRange([[1,0], [2,0]]) + selection.insertText('holy cow') + expect(editor.screenLineForRow(3).text).toBe buffer.lineForRow(3) + + describe "backspace", -> + it "destroys the fold", -> + selection.setBufferRange([[1,0], [2,0]]) + selection.backspace() + expect(editor.screenLineForRow(3).text).toBe buffer.lineForRow(3) + + describe "when the selection is empty", -> + describe "delete, when the selection is empty", -> + it "removes the lines contained by the fold", -> + oldLine7 = buffer.lineForRow(7) + oldLine8 = buffer.lineForRow(8) + + selection.setBufferRange([[2, 0], [2, 0]]) + selection.delete() + expect(editor.screenLineForRow(2).text).toBe oldLine7 + expect(editor.screenLineForRow(3).text).toBe oldLine8 diff --git a/spec/fixtures/two-hundred.txt b/spec/fixtures/two-hundred.txt new file mode 100644 index 000000000..9001290d1 --- /dev/null +++ b/spec/fixtures/two-hundred.txt @@ -0,0 +1,200 @@ +0 +1 +2 +3 +4---------------------------------------------------------------------------------------------------- +5 +6 +7 +8 +9---------------------------------------------------------------------------------------------------- +10 +11 +12 +13 +14---------------------------------------------------------------------------------------------------- +15 +16 +17 +18 +19---------------------------------------------------------------------------------------------------- +20 +21 +22 +23 +24---------------------------------------------------------------------------------------------------- +25 +26 +27 +28 +29---------------------------------------------------------------------------------------------------- +30 +31 +32 +33 +34---------------------------------------------------------------------------------------------------- +35 +36 +37 +38 +39---------------------------------------------------------------------------------------------------- +40 +41 +42 +43 +44---------------------------------------------------------------------------------------------------- +45 +46 +47 +48 +49---------------------------------------------------------------------------------------------------- +50 +51 +52 +53 +54---------------------------------------------------------------------------------------------------- +55 +56 +57 +58 +59---------------------------------------------------------------------------------------------------- +60 +61 +62 +63 +64---------------------------------------------------------------------------------------------------- +65 +66 +67 +68 +69---------------------------------------------------------------------------------------------------- +70 +71 +72 +73 +74---------------------------------------------------------------------------------------------------- +75 +76 +77 +78 +79---------------------------------------------------------------------------------------------------- +80 +81 +82 +83 +84---------------------------------------------------------------------------------------------------- +85 +86 +87 +88 +89---------------------------------------------------------------------------------------------------- +90 +91 +92 +93 +94---------------------------------------------------------------------------------------------------- +95 +96 +97 +98 +99---------------------------------------------------------------------------------------------------- +100 +101 +102 +103 +104---------------------------------------------------------------------------------------------------- +105 +106 +107 +108 +109---------------------------------------------------------------------------------------------------- +110 +111 +112 +113 +114---------------------------------------------------------------------------------------------------- +115 +116 +117 +118 +119---------------------------------------------------------------------------------------------------- +120 +121 +122 +123 +124---------------------------------------------------------------------------------------------------- +125 +126 +127 +128 +129---------------------------------------------------------------------------------------------------- +130 +131 +132 +133 +134---------------------------------------------------------------------------------------------------- +135 +136 +137 +138 +139---------------------------------------------------------------------------------------------------- +140 +141 +142 +143 +144---------------------------------------------------------------------------------------------------- +145 +146 +147 +148 +149---------------------------------------------------------------------------------------------------- +150 +151 +152 +153 +154---------------------------------------------------------------------------------------------------- +155 +156 +157 +158 +159---------------------------------------------------------------------------------------------------- +160 +161 +162 +163 +164---------------------------------------------------------------------------------------------------- +165 +166 +167 +168 +169---------------------------------------------------------------------------------------------------- +170 +171 +172 +173 +174---------------------------------------------------------------------------------------------------- +175 +176 +177 +178 +179---------------------------------------------------------------------------------------------------- +180 +181 +182 +183 +184---------------------------------------------------------------------------------------------------- +185 +186 +187 +188 +189---------------------------------------------------------------------------------------------------- +190 +191 +192 +193 +194---------------------------------------------------------------------------------------------------- +195 +196 +197 +198 +199---------------------------------------------------------------------------------------------------- diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index ee349f219..0c1f9de6c 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -242,4 +242,9 @@ class Buffer backwardsScanInRange: (regex, range, iterator) -> @scanInRange regex, range, iterator, true + logLines: (start=0, end=@getLastRow())-> + for row in [start..end] + line = @lineForRow(row) + console.log row, line, line.length + _.extend(Buffer.prototype, EventEmitter) diff --git a/src/app/composite-selection.coffee b/src/app/composite-selection.coffee index 2d470b8f2..5a6887b45 100644 --- a/src/app/composite-selection.coffee +++ b/src/app/composite-selection.coffee @@ -74,6 +74,10 @@ class CompositeSeleciton getText: -> @getLastSelection().getText() + intersectsBufferRange: (bufferRange) -> + _.any @getSelections(), (selection) -> + selection.intersectsBufferRange(bufferRange) + expandSelectionsForward: (fn) -> fn(selection) for selection in @getSelections() @mergeIntersectingSelections() diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index 6bfcef620..a3fc37abd 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -177,4 +177,9 @@ class Cursor extends View if this == _.last(@editor.getCursors()) @editor.scrollTo(pixelPosition) + if @editor.isFoldedAtScreenRow(screenPosition.row) + @hide() + else + @show() + @selection.updateAppearance() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 7bb27d8a3..17482245a 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -63,6 +63,7 @@ class Editor extends View requireStylesheet 'theme/twilight.css' @id = Editor.idCounter++ + @lineCache = [] @bindKeys() @autoIndent = true @buildCursorAndSelection() @@ -120,6 +121,7 @@ class Editor extends View 'redo': @redo 'toggle-soft-wrap': @toggleSoftWrap 'fold-selection': @foldSelection + 'unfold': => @unfoldRow(@getCursorBufferPosition().row) 'split-left': @splitLeft 'split-right': @splitRight 'split-up': @splitUp @@ -174,8 +176,8 @@ class Editor extends View @isFocused = false @removeClass 'focused' - @visibleLines.on 'mousedown', '.fold-placeholder', (e) => - @destroyFold($(e.currentTarget).attr('foldId')) + @visibleLines.on 'mousedown', '.fold.line', (e) => + @destroyFold($(e.currentTarget).attr('fold-id')) false @visibleLines.on 'mousedown', (e) => @@ -276,8 +278,11 @@ class Editor extends View if options?.adjustVerticalScrollbar ? true @verticalScrollbar.scrollTop(scrollTop) - scrollBottom: -> - @scrollTop() + @scrollView.height() + scrollBottom: (scrollBottom) -> + if scrollBottom? + @scrollTop(scrollBottom - @scrollView.height()) + else + @scrollTop() + @scrollView.height() renderVisibleLines: -> @clearLines() @@ -294,8 +299,6 @@ class Editor extends View firstVisibleScreenRow = @getFirstVisibleScreenRow() lastVisibleScreenRow = @getLastVisibleScreenRow() - return if @firstRenderedScreenRow <= firstVisibleScreenRow and @lastRenderedScreenRow >= lastVisibleScreenRow - @gutter.renderLineNumbers(firstVisibleScreenRow, lastVisibleScreenRow) renderFrom = Math.max(0, firstVisibleScreenRow - @lineOverdraw) @@ -349,10 +352,24 @@ class Editor extends View getLastVisibleScreenRow: -> Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1 + highlightSelectedFolds: -> + screenLines = @screenLinesForRows(@firstRenderedScreenRow, @lastRenderedScreenRow) + for screenLine, i in screenLines + if fold = screenLine.fold + screenRow = @firstRenderedScreenRow + i + element = @lineElementForScreenRow(screenRow) + if @compositeSelection.intersectsBufferRange(fold.getBufferRange()) + element.addClass('selected') + else + element.removeClass('selected') + getScreenLines: -> @renderer.getLines() - linesForRows: (start, end) -> + screenLineForRow: (start) -> + @renderer.lineForRow(start) + + screenLinesForRows: (start, end) -> @renderer.linesForRows(start, end) screenLineCount: -> @@ -361,6 +378,12 @@ class Editor extends View getLastScreenRow: -> @screenLineCount() - 1 + isFoldedAtScreenRow: (screenRow) -> + @screenLineForRow(screenRow).fold? + + destroyFoldsContainingBufferRow: (bufferRow) -> + @renderer.destroyFoldsContainingBufferRow(bufferRow) + setBuffer: (buffer) -> if @buffer @saveCurrentEditSession() @@ -438,33 +461,60 @@ class Editor extends View @compositeCursor.updateBufferPosition() unless e.bufferChanged if @attached - unless newScreenRange.isSingleLine() and newScreenRange.coversSameRows(oldScreenRange) + if e.lineNumbersChanged @gutter.renderLineNumbers(@getFirstVisibleScreenRow(), @getLastVisibleScreenRow()) + @verticalScrollbarContent.height(@lineHeight * @screenLineCount()) + + return if oldScreenRange.start.row > @lastRenderedScreenRow + + newScreenRange = newScreenRange.copy() + oldScreenRange = oldScreenRange.copy() + endOfShortestRange = Math.min(oldScreenRange.end.row, newScreenRange.end.row) + + delta = @firstRenderedScreenRow - endOfShortestRange + if delta > 0 + newScreenRange.start.row += delta + newScreenRange.end.row += delta + oldScreenRange.start.row += delta + oldScreenRange.end.row += delta + + newScreenRange.start.row = Math.max(newScreenRange.start.row, @firstRenderedScreenRow) + oldScreenRange.start.row = Math.max(oldScreenRange.start.row, @firstRenderedScreenRow) + newScreenRange.end.row = Math.min(newScreenRange.end.row, @lastRenderedScreenRow) + oldScreenRange.end.row = Math.min(oldScreenRange.end.row, @lastRenderedScreenRow) + lineElements = @buildLineElements(newScreenRange.start.row, newScreenRange.end.row) @replaceLineElements(oldScreenRange.start.row, oldScreenRange.end.row, lineElements) - @verticalScrollbarContent.height(@lineHeight * @screenLineCount()) rowDelta = newScreenRange.end.row - oldScreenRange.end.row - @lastRenderedScreenRow += rowDelta - @updateVisibleLines() if rowDelta < 0 + + if rowDelta > 0 + @removeLineElements(@lastRenderedScreenRow + 1, @lastRenderedScreenRow + rowDelta) + else if rowDelta < 0 + @lastRenderedScreenRow += rowDelta + @updateVisibleLines() buildLineElements: (startRow, endRow) -> charWidth = @charWidth charHeight = @charHeight lines = @renderer.linesForRows(startRow, endRow) + compositeSelection = @compositeSelection + $$ -> for line in lines - @div class: 'line', => - appendNbsp = true - for token in line.tokens - if token.type is 'fold-placeholder' - @span ' ', class: 'fold-placeholder', style: "width: #{3 * charWidth}px; height: #{charHeight}px;", 'foldId': token.fold.id, => - @div class: "ellipsis", => @raw "…" - else - appendNbsp = false + if fold = line.fold + lineAttributes = { class: 'fold line', 'fold-id': fold.id } + if compositeSelection.intersectsBufferRange(fold.getBufferRange()) + lineAttributes.class += ' selected' + else + lineAttributes = { class: 'line' } + @div lineAttributes, => + if line.text == '' + @raw ' ' if line.text == '' + else + for token in line.tokens @span { class: token.type.replace('.', ' ') }, token.value - @raw ' ' if appendNbsp insertLineElements: (row, lineElements) -> @spliceLineElements(row, 0, lineElements) @@ -499,8 +549,9 @@ class Editor extends View elementsToReplace.forEach (element) => lines.removeChild(element) - getLineElement: (row) -> - @lineCache[row] + lineElementForScreenRow: (screenRow) -> + element = @lineCache[screenRow - @firstRenderedScreenRow] + $(element) toggleSoftWrap: -> @setSoftWrap(not @softWrap) @@ -515,8 +566,8 @@ class Editor extends View maxLineLength ?= @calcMaxLineLength() @renderer.setMaxLineLength(maxLineLength) if maxLineLength - createFold: (range) -> - @renderer.createFold(range) + createFold: (startRow, endRow) -> + @renderer.createFold(startRow, endRow) setSoftWrap: (@softWrap, maxLineLength=undefined) -> @setMaxLineLength(maxLineLength) if @attached @@ -673,6 +724,9 @@ class Editor extends View foldSelection: -> @getSelection().fold() + unfoldRow: (row) -> + @renderer.largestFoldForBufferRow(row)?.destroy() + undo: -> if ranges = @buffer.undo() @setSelectedBufferRanges(ranges) @@ -684,7 +738,7 @@ class Editor extends View destroyFold: (foldId) -> fold = @renderer.foldsById[foldId] fold.destroy() - @setCursorBufferPosition(fold.start) + @setCursorBufferPosition([fold.startRow, 0]) splitLeft: -> @pane()?.splitLeft(@copy()).wrappedView @@ -729,6 +783,9 @@ class Editor extends View @scrollVertically(pixelPosition) @scrollHorizontally(pixelPosition) + scrollToBottom: -> + @scrollBottom(@scrollView.prop('scrollHeight')) + scrollVertically: (pixelPosition) -> linesInView = @scrollView.height() / @lineHeight maxScrollMargin = Math.floor((linesInView - 1) / 2) @@ -762,5 +819,5 @@ class Editor extends View for cursor in @getCursors() do (cursor) -> cursor.resetCursorAnimation() - logLines: -> - @renderer.logLines() + logLines: (start, end) -> + @renderer.logLines(start, end) diff --git a/src/app/fold.coffee b/src/app/fold.coffee index 2e6bab84d..74838ba7a 100644 --- a/src/app/fold.coffee +++ b/src/app/fold.coffee @@ -1,51 +1,66 @@ Range = require 'range' +Point = require 'point' module.exports = class Fold @idCounter: 1 - start: null - end: null - constructor: (@lineFolder, {@start, @end}) -> + renderer: null + startRow: null + endRow: null + + constructor: (@renderer, @startRow, @endRow) -> @id = @constructor.idCounter++ destroy: -> - @lineFolder.destroyFold(this) + @renderer.destroyFold(this) - getRange: -> - new Range(@start, @end) + inspect: -> + "Fold(#{@startRow}, #{@endRow})" + + getBufferRange: -> + new Range([@startRow, 0], [@endRow, Infinity]) + + getBufferDelta: -> + new Point(@endRow - @startRow + 1, 0) handleBufferChange: (event) -> - oldStartRow = @start.row + oldStartRow = @startRow - { oldRange } = event - if oldRange.start.isLessThanOrEqual(@start) and oldRange.end.isGreaterThanOrEqual(@end) - @lineFolder.unregisterFold(oldStartRow, this) + if @isContainedByRange(event.oldRange) + @renderer.unregisterFold(@startRow, this) return - changeInsideFold = @start.isLessThanOrEqual(oldRange.start) and @end.isGreaterThan(oldRange.end) + @updateStartRow(event) + @updateEndRow(event) - @start = @updateAnchorPoint(@start, event) - @end = @updateAnchorPoint(@end, event, false) + if @startRow != oldStartRow + @renderer.unregisterFold(oldStartRow, this) + @renderer.registerFold(this) - if @start.row != oldStartRow - @lineFolder.unregisterFold(oldStartRow, this) - @lineFolder.registerFold(@start.row, this) + isContainedByRange: (range) -> + range.start.row <= @startRow and @endRow <= range.end.row - changeInsideFold - - updateAnchorPoint: (point, event, inclusive=true) -> + updateStartRow: (event) -> { newRange, oldRange } = event - if inclusive - return point if oldRange.end.isGreaterThan(point) - else - return point if oldRange.end.isGreaterThanOrEqual(point) - newRange.end.add(point.subtract(oldRange.end)) - - compare: (other) -> - startComparison = @start.compare(other.start) - if startComparison == 0 - other.end.compare(@end) + if oldRange.end.row < @startRow + delta = newRange.end.row - oldRange.end.row + else if newRange.end.row < @startRow + delta = newRange.end.row - @startRow else - startComparison + delta = 0 + + @startRow += delta + + updateEndRow: (event) -> + { newRange, oldRange } = event + + if oldRange.end.row <= @endRow + delta = newRange.end.row - oldRange.end.row + else if newRange.end.row <= @endRow + delta = newRange.end.row - @endRow + else + delta = 0 + + @endRow += delta diff --git a/src/app/gutter.coffee b/src/app/gutter.coffee index fbefad648..04d0e9deb 100644 --- a/src/app/gutter.coffee +++ b/src/app/gutter.coffee @@ -17,4 +17,6 @@ class Gutter extends View @lineNumbers[0].innerHTML = $$$ -> for row in rows @div {class: 'line-number'}, if row == lastScreenRow then '•' else row + 1 - lastScreenRow = row \ No newline at end of file + lastScreenRow = row + + @lineNumbers.width(editor.getLastScreenRow().toString().length * editor.charWidth) diff --git a/src/app/keymaps/editor.coffee b/src/app/keymaps/editor.coffee index 37d2e9a8f..fe392b3dd 100644 --- a/src/app/keymaps/editor.coffee +++ b/src/app/keymaps/editor.coffee @@ -28,6 +28,7 @@ window.keymap.bindKeys '.editor', 'meta-Z': 'redo' 'alt-meta-w': 'toggle-soft-wrap' 'alt-meta-f': 'fold-selection' + 'alt-meta-u': 'unfold' 'alt-meta-left': 'split-left' 'alt-meta-right': 'split-right' 'alt-meta-up': 'split-up' diff --git a/src/app/line-map.coffee b/src/app/line-map.coffee index 2b95436df..3e12a71f2 100644 --- a/src/app/line-map.coffee +++ b/src/app/line-map.coffee @@ -121,8 +121,8 @@ class LineMap targetDelta = traversalResult[targetDeltaType] return targetDelta unless lastLineFragment - maxSourceColumn = sourceDelta.column + lastLineFragment.text.length - maxTargetColumn = targetDelta.column + lastLineFragment.text.length + maxSourceColumn = sourceDelta.column + lastLineFragment.textLength() + maxTargetColumn = targetDelta.column + lastLineFragment.textLength() if lastLineFragment.isSoftWrapped() and sourcePosition.column >= maxSourceColumn if wrapAtSoftNewlines diff --git a/src/app/renderer.coffee b/src/app/renderer.coffee index af48284ea..a34dc0dd2 100644 --- a/src/app/renderer.coffee +++ b/src/app/renderer.coffee @@ -37,7 +37,7 @@ class Renderer oldRange = @rangeForAllLines() @buildLineMap() newRange = @rangeForAllLines() - @trigger 'change', { oldRange, newRange } + @trigger 'change', { oldRange, newRange, lineNumbersChanged: true } lineForRow: (row) -> @lineMap.lineForScreenRow(row) @@ -51,40 +51,56 @@ class Renderer bufferRowsForScreenRows: (startRow, endRow) -> @lineMap.bufferRowsForScreenRows(startRow, endRow) - createFold: (bufferRange) -> - bufferRange = Range.fromObject(bufferRange) - return if bufferRange.isEmpty() - - fold = new Fold(this, bufferRange) - @registerFold(bufferRange.start.row, fold) + createFold: (startRow, endRow) -> + fold = new Fold(this, startRow, endRow) + @registerFold(fold) + bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)]) oldScreenRange = @screenLineRangeForBufferRange(bufferRange) - lines = @buildLineForBufferRow(bufferRange.start.row) - @lineMap.replaceScreenRows( - oldScreenRange.start.row, - oldScreenRange.end.row, - lines) + + lines = @buildLineForBufferRow(startRow) + @lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines) newScreenRange = @screenLineRangeForBufferRange(bufferRange) - @trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange + @trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true @trigger 'fold', bufferRange fold destroyFold: (fold) -> - bufferRange = fold.getRange() - @unregisterFold(bufferRange.start.row, fold) - startScreenRow = @screenRowForBufferRow(bufferRange.start.row) + @unregisterFold(fold.startRow, fold) + { startRow, endRow } = fold + bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)]) oldScreenRange = @screenLineRangeForBufferRange(bufferRange) - lines = @buildLinesForBufferRows(bufferRange.start.row, bufferRange.end.row) - @lineMap.replaceScreenRows( - oldScreenRange.start.row, - oldScreenRange.end.row - lines) + lines = @buildLinesForBufferRows(startRow, endRow) + @lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines) newScreenRange = @screenLineRangeForBufferRange(bufferRange) - @trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange - @trigger 'unfold', fold.getRange() + @trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true + @trigger 'unfold', bufferRange + + destroyFoldsContainingBufferRow: (bufferRow) -> + folds = @activeFolds[bufferRow] ? [] + fold.destroy() for fold in new Array(folds...) + + registerFold: (fold) -> + @activeFolds[fold.startRow] ?= [] + @activeFolds[fold.startRow].push(fold) + @foldsById[fold.id] = fold + + unregisterFold: (bufferRow, fold) -> + folds = @activeFolds[bufferRow] + _.remove(folds, fold) + delete @foldsById[fold.id] + + largestFoldForBufferRow: (bufferRow) -> + return unless folds = @activeFolds[bufferRow] + (folds.sort (a, b) -> b.endRow - a.endRow)[0] + + screenLineRangeForBufferRange: (bufferRange) -> + @expandScreenRangeToLineEnds( + @lineMap.screenRangeForBufferRange( + @expandBufferRangeToLineEnds(bufferRange))) screenRowForBufferRow: (bufferRow) -> @lineMap.screenPositionForBufferPosition([bufferRow, 0]).row @@ -111,23 +127,27 @@ class Renderer @lineMap.clipScreenPosition(position, options) handleBufferChange: (e) -> - for row, folds of @activeFolds - for fold in new Array(folds...) - changeInsideFold = true if fold.handleBufferChange(e) + allFolds = [] # Folds can modify @activeFolds, so first make sure we have a stable array of folds + allFolds.push(folds...) for row, folds of @activeFolds + fold.handleBufferChange(e) for fold in allFolds - unless changeInsideFold - @handleHighlighterChange(@lastHighlighterChangeEvent) + @handleHighlighterChange(@lastHighlighterChangeEvent) handleHighlighterChange: (e) -> - oldBufferRange = e.oldRange - newBufferRange = e.newRange + newRange = e.newRange.copy() + newRange.start.row = @bufferRowForScreenRow(@screenRowForBufferRow(newRange.start.row)) - oldScreenRange = @screenLineRangeForBufferRange(oldBufferRange) - newScreenLines = @buildLinesForBufferRows(newBufferRange.start.row, newBufferRange.end.row) + oldScreenRange = @screenLineRangeForBufferRange(e.oldRange) + + newScreenLines = @buildLinesForBufferRows(newRange.start.row, newRange.end.row) @lineMap.replaceScreenRows oldScreenRange.start.row, oldScreenRange.end.row, newScreenLines - newScreenRange = @screenLineRangeForBufferRange(newBufferRange) + newScreenRange = @screenLineRangeForBufferRange(newRange) - @trigger 'change', { oldRange: oldScreenRange, newRange: newScreenRange, bufferChanged: true } + @trigger 'change', + oldRange: oldScreenRange + newRange: newScreenRange + bufferChanged: true + lineNumbersChanged: !e.oldRange.coversSameRows(newRange) or !oldScreenRange.coversSameRows(newScreenRange) buildLineForBufferRow: (bufferRow) -> @buildLinesForBufferRows(bufferRow, bufferRow) @@ -135,49 +155,36 @@ class Renderer buildLinesForBufferRows: (startBufferRow, endBufferRow) -> lineFragments = [] startBufferColumn = null + currentBufferRow = startBufferRow currentScreenLineLength = 0 - startBufferRow = @foldStartRowForBufferRow(startBufferRow) - loop - break if startBufferRow > endBufferRow and not startBufferColumn? + startBufferColumn = 0 + while currentBufferRow <= endBufferRow + screenLine = @highlighter.lineForRow(currentBufferRow) + + if fold = @largestFoldForBufferRow(currentBufferRow) + screenLine = screenLine.copy() + screenLine.fold = fold + screenLine.bufferDelta = fold.getBufferDelta() + lineFragments.push(screenLine) + currentBufferRow = fold.endRow + 1 + continue + startBufferColumn ?= 0 - line = @highlighter.lineForRow(startBufferRow) - line = line.splitAt(startBufferColumn)[1] - wrapScreenColumn = @findWrapColumn(line.text, @maxLineLength - currentScreenLineLength) - - continueMainLoop = false - for fold in @foldsForBufferRow(startBufferRow) - if fold.start.column >= startBufferColumn - foldStartSceenColumn = fold.start.column - startBufferColumn - if (foldStartSceenColumn) > wrapScreenColumn - foldPlaceholderLength - wrapScreenColumn = Math.min(wrapScreenColumn, foldStartSceenColumn) - break - prefix = line.splitAt(foldStartSceenColumn)[0] - placeholder = @buildFoldPlaceholder(fold) - lineFragments.push(prefix, placeholder) - startBufferRow = fold.end.row - startBufferColumn = fold.end.column - currentScreenLineLength = currentScreenLineLength + (prefix?.text.length ? 0) + foldPlaceholderLength - continueMainLoop = true - break - continue if continueMainLoop - + screenLine = screenLine.splitAt(startBufferColumn)[1] if startBufferColumn > 0 + wrapScreenColumn = @findWrapColumn(screenLine.text, @maxLineLength) if wrapScreenColumn? - line = line.splitAt(wrapScreenColumn)[0] - line.screenDelta = new Point(1, 0) + screenLine = screenLine.splitAt(wrapScreenColumn)[0] + screenLine.screenDelta = new Point(1, 0) startBufferColumn += wrapScreenColumn else - startBufferRow++ - startBufferColumn = null + currentBufferRow++ + startBufferColumn = 0 - lineFragments.push(line) - currentScreenLineLength = 0 + lineFragments.push(screenLine) lineFragments - foldStartRowForBufferRow: (bufferRow) -> - @bufferRowForScreenRow(@screenRowForBufferRow(bufferRow)) - findWrapColumn: (line, maxLineLength) -> return unless line.length > maxLineLength @@ -192,29 +199,6 @@ class Renderer return column + 1 if /\s/.test(line[column]) return maxLineLength - registerFold: (bufferRow, fold) -> - @activeFolds[bufferRow] ?= [] - @activeFolds[bufferRow].push(fold) - @foldsById[fold.id] = fold - - unregisterFold: (bufferRow, fold) -> - folds = @activeFolds[bufferRow] - folds.splice(folds.indexOf(fold), 1) - delete @foldsById[fold.id] - - foldsForBufferRow: (bufferRow) -> - folds = @activeFolds[bufferRow] or [] - folds.sort (a, b) -> a.compare(b) - - buildFoldPlaceholder: (fold) -> - token = new Token(value: '...', type: 'fold-placeholder', fold: fold, isAtomic: true) - new ScreenLineFragment([token], token.value, [0, token.value.length], fold.getRange().toDelta()) - - screenLineRangeForBufferRange: (bufferRange) -> - @expandScreenRangeToLineEnds( - @lineMap.screenRangeForBufferRange( - @expandBufferRangeToLineEnds(bufferRange))) - expandScreenRangeToLineEnds: (screenRange) -> screenRange = Range.fromObject(screenRange) { start, end } = screenRange @@ -232,7 +216,7 @@ class Renderer @highlighter.destroy() @buffer.off ".renderer#{@id}" - logLines: -> - @lineMap.logLines() + logLines: (start, end) -> + @lineMap.logLines(start, end) _.extend Renderer.prototype, EventEmitter diff --git a/src/app/screen-line-fragment.coffee b/src/app/screen-line-fragment.coffee index e16391d80..8dfdde1e2 100644 --- a/src/app/screen-line-fragment.coffee +++ b/src/app/screen-line-fragment.coffee @@ -14,6 +14,9 @@ class ScreenLineFragment @bufferDelta = Point.fromObject(bufferDelta) _.extend(this, extraFields) + copy: -> + new ScreenLineFragment(@tokens, @text, @screenDelta, @bufferDelta, { @state }) + splitAt: (column) -> return [new ScreenLineFragment([], '', [0, 0], [0, 0]), this] if column == 0 @@ -46,8 +49,7 @@ class ScreenLineFragment translateColumn: (sourceDeltaType, targetDeltaType, sourceColumn, options={}) -> { skipAtomicTokens } = options - textLength = @text.length - sourceColumn = Math.min(sourceColumn, textLength) + sourceColumn = Math.min(sourceColumn, @textLength()) currentSourceColumn = 0 currentTargetColumn = 0 @@ -69,6 +71,12 @@ class ScreenLineFragment remainingColumns = sourceColumn - currentSourceColumn currentTargetColumn + remainingColumns + textLength: -> + if @fold + textLength = 0 + else + textLength = @text.length + isSoftWrapped: -> @screenDelta.row == 1 and @bufferDelta.row == 0 diff --git a/src/app/selection.coffee b/src/app/selection.coffee index f734ef680..4ca43e49b 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -45,6 +45,8 @@ class Selection extends View @clearRegions() range = @getScreenRange() + + @editor.highlightSelectedFolds() return if range.isEmpty() rowSpan = range.end.row - range.start.row @@ -57,6 +59,7 @@ class Selection extends View @appendRegion(rowSpan - 1, { row: range.start.row + 1, column: 0}, null) @appendRegion(1, { row: range.end.row, column: 0 }, range.end) + appendRegion: (rows, start, end) -> { lineHeight, charWidth } = @editor css = @editor.pixelPositionForScreenPosition(start) @@ -80,8 +83,7 @@ class Selection extends View else new Range(@cursor.getScreenPosition(), @cursor.getScreenPosition()) - setScreenRange: (range, options={}) -> - { reverse } = options + setScreenRange: (range, {reverse}={}) -> { start, end } = range [start, end] = [end, start] if reverse @@ -97,9 +99,13 @@ class Selection extends View getText: -> @editor.buffer.getTextInRange @getBufferRange() + intersectsBufferRange: (bufferRange) -> + @getBufferRange().intersectsWith(bufferRange) + insertText: (text) -> { text, shouldOutdent } = @autoIndentText(text) oldBufferRange = @getBufferRange() + @editor.destroyFoldsContainingBufferRow(oldBufferRange.end.row) isReversed = @isReversed() @clearSelection() newBufferRange = @editor.buffer.change(oldBufferRange, text) @@ -139,6 +145,7 @@ class Selection extends View @editor.getCurrentMode().autoOutdent(state, new AceOutdentAdaptor(@editor.buffer, @editor), bufferRow) backspace: -> + @editor.destroyFoldsContainingBufferRow(@getBufferRange().end.row) @selectLeft() if @isEmpty() @deleteSelectedText() @@ -237,5 +244,5 @@ class Selection extends View fold: -> range = @getBufferRange() - @editor.createFold(range) - @cursor.setBufferPosition(range.end) + @editor.createFold(range.start.row, range.end.row) + @cursor.setBufferPosition([range.end.row + 1, 0]) diff --git a/static/editor.css b/static/editor.css index 89096923b..0a154f554 100644 --- a/static/editor.css +++ b/static/editor.css @@ -109,27 +109,3 @@ background: white; opacity: .25; } - -.fold-placeholder { - -webkit-box-sizing: border-box; - position: relative; - display: inline-block; - vertical-align: text-bottom; - -webkit-border-radius: 3px; - background: rgba(255, 255, 255, .07); - border: 1px solid rgba(255, 255, 255, .25); - color: rgba(255, 255, 255, .95); -} - -.fold-placeholder:hover { - background: rgba(255, 255, 255, .2); - border: 1px solid rgba(255, 255, 255, .3); - color: white; -} - -.fold-placeholder .ellipsis { - position: relative; - width: 100%; - bottom: 40%; - text-align: center; -} diff --git a/static/theme/twilight.css b/static/theme/twilight.css index 8384a69bf..ddd2d0e5e 100644 --- a/static/theme/twilight.css +++ b/static/theme/twilight.css @@ -70,8 +70,13 @@ color:#D2A8A1; } .fold { - background-color: #AC885B; - border-color: #F8F8F8; + background-color: #524228; + color: #969696; +} + +.fold.selected { + background-color: #2A3B2A; + color: #969696; } .support.function { diff --git a/vendor/jasmine-console-reporter.js b/vendor/jasmine-console-reporter.js index afad4657b..24ec50cc5 100644 --- a/vendor/jasmine-console-reporter.js +++ b/vendor/jasmine-console-reporter.js @@ -27,16 +27,17 @@ jasmine.ConsoleReporter.prototype.reportSpecResults = function(spec) { if (results.skipped) { status = 'skipped'; } - var resultItems = results.getItems(); for (var i = 0; i < resultItems.length; i++) { var result = resultItems[i]; - if (this.logErrors && result.type == 'expect' && result.passed && !result.passed()) { - console.log(spec.getFullName()) + console.log("ERROR: %s", spec.getFullName()) if (result.trace.stack) { console.log(result.trace.stack) } + else { + console.log(result.message) + } } } }; diff --git a/vendor/jasmine-focused.js b/vendor/jasmine-focused.js index b59bbac85..c10b1b976 100644 --- a/vendor/jasmine-focused.js +++ b/vendor/jasmine-focused.js @@ -1,20 +1,45 @@ -var fdescribe = function(description, specDefinitions) { - jasmine.getEnv().focus = true +var setGlobalFocusPriority = function(priority) { + env = jasmine.getEnv(); + if (!env.focusPriority) env.focusPriority = 1; + if (priority > env.focusPriority) env.focusPriority = priority; +}; + +var fdescribe = function(description, specDefinitions, priority) { + if (!priority) priority = 1; + setGlobalFocusPriority(priority) var suite = describe(description, specDefinitions); - suite.focus = true; + suite.focusPriority = priority; return suite; }; -var fit = function(description, definition) { - jasmine.getEnv().focus = true +var ffdescribe = function(description, specDefinitions) { + fdescribe(description, specDefinitions, 2); +}; + +var fffdescribe = function(description, specDefinitions) { + fdescribe(description, specDefinitions, 3); +}; + +var fit = function(description, definition, priority) { + if (!priority) priority = 1; + setGlobalFocusPriority(priority); var spec = it(description, definition); - spec.focus = true; + spec.focusPriority = priority; return spec; }; +var ffit = function(description, specDefinitions) { + fit(description, specDefinitions, 2); +}; + +var fffit = function(description, specDefinitions) { + fit(description, specDefinitions, 3); +}; + var fSpecFilter = function(specOrSuite) { - if (!jasmine.getEnv().focus) return true; - if (specOrSuite.focus) return true; + globalFocusPriority = jasmine.getEnv().focusPriority; + if (!globalFocusPriority) return true; + if (specOrSuite.focusPriority >= globalFocusPriority) return true; var parent = specOrSuite.parentSuite || specOrSuite.suite; if (!parent) return false; @@ -29,7 +54,7 @@ jasmine.AtomReporter.prototype.specFilter = function(spec) { paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); } - if (!paramMap.spec && !jasmine.getEnv().focus) { + if (!paramMap.spec && !jasmine.getEnv().focusPriority) { return true; }