diff --git a/benchmark/benchmark-helper.coffee b/benchmark/benchmark-helper.coffee index 257699616..312d2258d 100644 --- a/benchmark/benchmark-helper.coffee +++ b/benchmark/benchmark-helper.coffee @@ -73,8 +73,8 @@ window.clickEvent = (properties={}) -> window.mouseEvent = (type, properties) -> if properties.point - {point, editor} = properties - {top, left} = @pagePixelPositionForPoint(editor, point) + {point, editorView} = properties + {top, left} = @pagePixelPositionForPoint(editorView, point) properties.pageX = left + 1 properties.pageY = top + 1 properties.originalEvent ?= {detail: 1} @@ -86,14 +86,14 @@ window.mousedownEvent = (properties={}) -> window.mousemoveEvent = (properties={}) -> window.mouseEvent('mousemove', properties) -window.pagePixelPositionForPoint = (editor, point) -> +window.pagePixelPositionForPoint = (editorView, point) -> point = Point.fromObject point - top = editor.lines.offset().top + point.row * editor.lineHeight - left = editor.lines.offset().left + point.column * editor.charWidth - editor.lines.scrollLeft() + top = editorView.lines.offset().top + point.row * editorView.lineHeight + left = editorView.lines.offset().left + point.column * editorView.charWidth - editorView.lines.scrollLeft() { top, left } -window.setEditorWidthInChars = (editor, widthInChars, charWidth=editor.charWidth) -> - editor.width(charWidth * widthInChars + editor.lines.position().left) +window.seteditorViewWidthInChars = (editorView, widthInChars, charWidth=editorView.charWidth) -> + editorView.width(charWidth * widthInChars + editorView.lines.position().left) $.fn.resultOfTrigger = (type) -> event = $.Event(type) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 92193a634..ad080f64b 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -2,8 +2,8 @@ require './benchmark-helper' {$, _, RootView} = require 'atom' TokenizedBuffer = require '../src/tokenized-buffer' -describe "editor.", -> - editor = null +describe "editorView.", -> + editorView = null beforeEach -> window.rootViewParentSelector = '#jasmine-content' @@ -12,19 +12,19 @@ describe "editor.", -> rootView.width(1024) rootView.height(768) - rootView.openSync() # open blank editor - editor = rootView.getActiveView() + rootView.openSync() + editorView = rootView.getActiveView() afterEach -> - if editor.pendingDisplayUpdate + if editorView.pendingDisplayUpdate waitsFor "editor to finish rendering", (done) -> - editor.on 'editor:display-updated', done + editorView.on 'editor:display-updated', done describe "keymap.", -> event = null beforeEach -> - event = keydownEvent('x', target: editor.hiddenInput[0]) + event = keydownEvent('x', target: editorView.hiddenInput[0]) benchmark "keydown-event-with-no-binding", 10, -> keymap.handleKeyEvent(event) @@ -35,8 +35,8 @@ describe "editor.", -> describe "empty-file.", -> benchmark "insert-delete", -> - editor.insertText('x') - editor.backspace() + editorView.insertText('x') + editorView.backspace() describe "300-line-file.", -> beforeEach -> @@ -44,81 +44,81 @@ describe "editor.", -> describe "at-begining.", -> benchmark "insert-delete", -> - editor.insertText('x') - editor.backspace() + editorView.insertText('x') + editorView.backspace() benchmark "insert-delete-rehighlight", -> - editor.insertText('"') - editor.backspace() + editorView.insertText('"') + editorView.backspace() describe "at-end.", -> beforeEach -> - editor.moveCursorToBottom() + editorView.moveCursorToBottom() benchmark "insert-delete", -> - editor.insertText('"') - editor.backspace() + editorView.insertText('"') + editorView.backspace() describe "empty-vs-set-innerHTML.", -> [firstRow, lastRow] = [] beforeEach -> - firstRow = editor.getFirstVisibleScreenRow() - lastRow = editor.getLastVisibleScreenRow() + firstRow = editorView.getFirstVisibleScreenRow() + lastRow = editorView.getLastVisibleScreenRow() benchmark "build-gutter-html.", 1000, -> - editor.gutter.renderLineNumbers(null, firstRow, lastRow) + editorView.gutter.renderLineNumbers(null, firstRow, lastRow) benchmark "set-innerHTML.", 1000, -> - editor.gutter.renderLineNumbers(null, firstRow, lastRow) - editor.gutter.lineNumbers[0].innerHtml = '' + editorView.gutter.renderLineNumbers(null, firstRow, lastRow) + editorView.gutter.lineNumbers[0].innerHtml = '' benchmark "empty.", 1000, -> - editor.gutter.renderLineNumbers(null, firstRow, lastRow) - editor.gutter.lineNumbers.empty() + editorView.gutter.renderLineNumbers(null, firstRow, lastRow) + editorView.gutter.lineNumbers.empty() describe "positionLeftForLineAndColumn.", -> line = null beforeEach -> - editor.scrollTop(2000) - editor.resetDisplay() - line = editor.lineElementForScreenRow(106)[0] + editorView.scrollTop(2000) + editorView.resetDisplay() + line = editorView.lineElementForScreenRow(106)[0] describe "one-line.", -> beforeEach -> - editor.clearCharacterWidthCache() + editorView.clearCharacterWidthCache() benchmark "uncached", 5000, -> - editor.positionLeftForLineAndColumn(line, 106, 82) - editor.clearCharacterWidthCache() + editorView.positionLeftForLineAndColumn(line, 106, 82) + editorView.clearCharacterWidthCache() benchmark "cached", 5000, -> - editor.positionLeftForLineAndColumn(line, 106, 82) + editorView.positionLeftForLineAndColumn(line, 106, 82) describe "multiple-lines.", -> [firstRow, lastRow] = [] beforeEach -> - firstRow = editor.getFirstVisibleScreenRow() - lastRow = editor.getLastVisibleScreenRow() + firstRow = editorView.getFirstVisibleScreenRow() + lastRow = editorView.getLastVisibleScreenRow() benchmark "cache-entire-visible-area", 100, -> for i in [firstRow..lastRow] - line = editor.lineElementForScreenRow(i)[0] - editor.positionLeftForLineAndColumn(line, i, Math.max(0, editor.lineLengthForBufferRow(i))) + line = editorView.lineElementForScreenRow(i)[0] + editorView.positionLeftForLineAndColumn(line, i, Math.max(0, editorView.lineLengthForBufferRow(i))) describe "text-rendering.", -> beforeEach -> - editor.scrollTop(2000) + editorView.scrollTop(2000) benchmark "resetDisplay", 50, -> - editor.resetDisplay() + editorView.resetDisplay() benchmark "htmlForScreenRows", 1000, -> - lastRow = editor.getLastScreenRow() - editor.htmlForScreenRows(0, lastRow) + lastRow = editorView.getLastScreenRow() + editorView.htmlForScreenRows(0, lastRow) benchmark "htmlForScreenRows.htmlParsing", 50, -> - lastRow = editor.getLastScreenRow() - html = editor.htmlForScreenRows(0, lastRow) + lastRow = editorView.getLastScreenRow() + html = editorView.htmlForScreenRows(0, lastRow) div = document.createElement('div') div.innerHTML = html @@ -126,44 +126,44 @@ describe "editor.", -> describe "gutter-api.", -> describe "getLineNumberElementsForClass.", -> beforeEach -> - editor.gutter.addClassToLine(20, 'omgwow') - editor.gutter.addClassToLine(40, 'omgwow') + editorView.gutter.addClassToLine(20, 'omgwow') + editorView.gutter.addClassToLine(40, 'omgwow') benchmark "DOM", 20000, -> - editor.gutter.getLineNumberElementsForClass('omgwow') + editorView.gutter.getLineNumberElementsForClass('omgwow') benchmark "getLineNumberElement.DOM", 20000, -> - editor.gutter.getLineNumberElement(12) + editorView.gutter.getLineNumberElement(12) benchmark "toggle-class", 2000, -> - editor.gutter.addClassToLine(40, 'omgwow') - editor.gutter.removeClassFromLine(40, 'omgwow') + editorView.gutter.addClassToLine(40, 'omgwow') + editorView.gutter.removeClassFromLine(40, 'omgwow') describe "find-then-unset.", -> classes = ['one', 'two', 'three', 'four'] benchmark "single-class", 200, -> - editor.gutter.addClassToLine(30, 'omgwow') - editor.gutter.addClassToLine(40, 'omgwow') - editor.gutter.removeClassFromAllLines('omgwow') + editorView.gutter.addClassToLine(30, 'omgwow') + editorView.gutter.addClassToLine(40, 'omgwow') + editorView.gutter.removeClassFromAllLines('omgwow') benchmark "multiple-class", 200, -> - editor.gutter.addClassToLine(30, 'one') - editor.gutter.addClassToLine(30, 'two') + editorView.gutter.addClassToLine(30, 'one') + editorView.gutter.addClassToLine(30, 'two') - editor.gutter.addClassToLine(40, 'two') - editor.gutter.addClassToLine(40, 'three') - editor.gutter.addClassToLine(40, 'four') + editorView.gutter.addClassToLine(40, 'two') + editorView.gutter.addClassToLine(40, 'three') + editorView.gutter.addClassToLine(40, 'four') for klass in classes - editor.gutter.removeClassFromAllLines(klass) + editorView.gutter.removeClassFromAllLines(klass) describe "line-htmlification.", -> div = null html = null beforeEach -> - lastRow = editor.getLastScreenRow() - html = editor.htmlForScreenRows(0, lastRow) + lastRow = editorView.getLastScreenRow() + html = editorView.htmlForScreenRows(0, lastRow) div = document.createElement('div') benchmark "setInnerHTML", 1, -> @@ -178,40 +178,40 @@ describe "editor.", -> rootView.openSync('huge.js') benchmark "moving-to-eof.", 1, -> - editor.moveCursorToBottom() + editorView.moveCursorToBottom() describe "on-first-line.", -> benchmark "inserting-newline", 5, -> - editor.insertNewline() + editorView.insertNewline() describe "on-last-visible-line.", -> beforeEach -> - editor.setCursorScreenPosition([editor.getLastVisibleScreenRow(), 0]) + editorView.setCursorScreenPosition([editorView.getLastVisibleScreenRow(), 0]) benchmark "move-down-and-scroll", 300, -> - editor.trigger 'move-down' + editorView.trigger 'move-down' describe "at-eof.", -> endPosition = null beforeEach -> - editor.moveCursorToBottom() - endPosition = editor.getCursorScreenPosition() + editorView.moveCursorToBottom() + endPosition = editorView.getCursorScreenPosition() benchmark "move-to-beginning-of-word", -> - editor.moveCursorToBeginningOfWord() - editor.setCursorScreenPosition(endPosition) + editorView.moveCursorToBeginningOfWord() + editorView.setCursorScreenPosition(endPosition) benchmark "insert", -> - editor.insertText('x') + editorView.insertText('x') describe "TokenizedBuffer.", -> describe "coffee-script-grammar.", -> [languageMode, buffer] = [] beforeEach -> - editSession = benchmarkFixturesProject.openSync('medium.coffee') - { languageMode, buffer } = editSession + editor = benchmarkFixturesProject.openSync('medium.coffee') + { languageMode, buffer } = editor benchmark "construction", 20, -> new TokenizedBuffer(buffer, { languageMode, tabLength: 2}) diff --git a/exports/atom.coffee b/exports/atom.coffee index b68144665..96da95612 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -20,7 +20,7 @@ unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE module.exports.$ = $ module.exports.$$ = $$ module.exports.$$$ = $$$ - module.exports.Editor = require '../src/editor' + module.exports.Editor = require '../src/editor-view' module.exports.RootView = require '../src/root-view' module.exports.SelectList = require '../src/select-list' module.exports.ScrollView = require '../src/scroll-view' diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 6b9d200d8..1a462d74e 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -95,14 +95,14 @@ describe "the `atom` global", -> it "triggers the activation event on all handlers registered during activation", -> rootView.openSync() - editor = rootView.getActiveView() + editorView = rootView.getActiveView() eventHandler = jasmine.createSpy("activation-event") - editor.command 'activation-event', eventHandler - editor.trigger 'activation-event' + editorView.command 'activation-event', eventHandler + editorView.trigger 'activation-event' expect(mainModule.activate.callCount).toBe 1 expect(mainModule.activationEventCallCount).toBe 1 expect(eventHandler.callCount).toBe 1 - editor.trigger 'activation-event' + editorView.trigger 'activation-event' expect(mainModule.activationEventCallCount).toBe 2 expect(eventHandler.callCount).toBe 2 expect(mainModule.activate.callCount).toBe 1 diff --git a/spec/edit-session-spec.coffee b/spec/edit-session-spec.coffee deleted file mode 100644 index 2107c3928..000000000 --- a/spec/edit-session-spec.coffee +++ /dev/null @@ -1,2661 +0,0 @@ -clipboard = require 'clipboard' - -describe "EditSession", -> - [buffer, editSession, lineLengths] = [] - - convertToHardTabs = (buffer) -> - buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t")) - - describe "with an initial line option", -> - beforeEach -> - editSession = project.openSync('sample.js', initialLine: 2) - buffer = editSession.buffer - - it "opens the file and positions the cursor on line 2", -> - expect(editSession.getCursor().getBufferPosition().row).toEqual 2 - - describe "with default options", -> - beforeEach -> - atom.activatePackage('language-javascript', sync: true) - editSession = project.openSync('sample.js', autoIndent: false) - buffer = editSession.buffer - lineLengths = buffer.getLines().map (line) -> line.length - - describe "@deserialize(state)", -> - it "restores selections and folds based on markers in the buffer", -> - editSession.setSelectedBufferRange([[1, 2], [3, 4]]) - editSession.addSelectionForBufferRange([[5, 6], [7, 5]], isReversed: true) - editSession.foldBufferRow(4) - expect(editSession.isFoldedAtBufferRow(4)).toBeTruthy() - - editSession2 = deserialize(editSession.serialize()) - - expect(editSession2.id).toBe editSession.id - expect(editSession2.getBuffer().getPath()).toBe editSession.getBuffer().getPath() - expect(editSession2.getSelectedBufferRanges()).toEqual [[[1, 2], [3, 4]], [[5, 6], [7, 5]]] - expect(editSession2.getSelection(1).isReversed()).toBeTruthy() - expect(editSession2.isFoldedAtBufferRow(4)).toBeTruthy() - - describe ".copy()", -> - it "returns a different edit session with the same initial state", -> - editSession.setSelectedBufferRange([[1, 2], [3, 4]]) - editSession.addSelectionForBufferRange([[5, 6], [7, 8]], isReversed: true) - editSession.foldBufferRow(4) - expect(editSession.isFoldedAtBufferRow(4)).toBeTruthy() - - editSession2 = editSession.copy() - expect(editSession2.id).not.toBe editSession.id - expect(editSession2.getSelectedBufferRanges()).toEqual editSession.getSelectedBufferRanges() - expect(editSession2.getSelection(1).isReversed()).toBeTruthy() - expect(editSession2.isFoldedAtBufferRow(4)).toBeTruthy() - - # editSession2 can now diverge from its origin edit session - editSession2.getSelection().setBufferRange([[2, 1], [4, 3]]) - expect(editSession2.getSelectedBufferRanges()).not.toEqual editSession.getSelectedBufferRanges() - editSession2.unfoldBufferRow(4) - expect(editSession2.isFoldedAtBufferRow(4)).not.toBe editSession.isFoldedAtBufferRow(4) - - describe "config defaults", -> - it "uses the `editor.tabLength`, `editor.softWrap`, and `editor.softTabs` config values", -> - atom.config.set('editor.tabLength', 4) - atom.config.set('editor.softWrap', true) - atom.config.set('editor.softTabs', false) - editSession1 = project.openSync('a') - expect(editSession1.getTabLength()).toBe 4 - expect(editSession1.getSoftWrap()).toBe true - expect(editSession1.getSoftTabs()).toBe false - - atom.config.set('editor.tabLength', 100) - atom.config.set('editor.softWrap', false) - atom.config.set('editor.softTabs', true) - editSession2 = project.openSync('b') - expect(editSession2.getTabLength()).toBe 100 - expect(editSession2.getSoftWrap()).toBe false - expect(editSession2.getSoftTabs()).toBe true - - describe "title", -> - describe ".getTitle()", -> - it "uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", -> - expect(editSession.getTitle()).toBe 'sample.js' - buffer.setPath(undefined) - expect(editSession.getTitle()).toBe 'untitled' - - describe ".getLongTitle()", -> - it "appends the name of the containing directory to the basename of the file", -> - expect(editSession.getLongTitle()).toBe 'sample.js - fixtures' - buffer.setPath(undefined) - expect(editSession.getLongTitle()).toBe 'untitled' - - it "emits 'title-changed' events when the underlying buffer path", -> - titleChangedHandler = jasmine.createSpy("titleChangedHandler") - editSession.on 'title-changed', titleChangedHandler - - buffer.setPath('/foo/bar/baz.txt') - buffer.setPath(undefined) - expect(titleChangedHandler.callCount).toBe 2 - - describe "cursor", -> - describe ".getCursor()", -> - it "returns the most recently created cursor", -> - editSession.addCursorAtScreenPosition([1, 0]) - lastCursor = editSession.addCursorAtScreenPosition([2, 0]) - expect(editSession.getCursor()).toBe lastCursor - - describe ".setCursorScreenPosition(screenPosition)", -> - it "clears a goal column established by vertical movement", -> - # set a goal column by moving down - editSession.setCursorScreenPosition(row: 3, column: lineLengths[3]) - editSession.moveCursorDown() - expect(editSession.getCursorScreenPosition().column).not.toBe 6 - - # clear the goal column by explicitly setting the cursor position - editSession.setCursorScreenPosition([4,6]) - expect(editSession.getCursorScreenPosition().column).toBe 6 - - editSession.moveCursorDown() - expect(editSession.getCursorScreenPosition().column).toBe 6 - - it "merges multiple cursors", -> - editSession.setCursorScreenPosition([0, 0]) - editSession.addCursorAtScreenPosition([0, 1]) - [cursor1, cursor2] = editSession.getCursors() - editSession.setCursorScreenPosition([4, 7]) - expect(editSession.getCursors().length).toBe 1 - expect(editSession.getCursors()).toEqual [cursor1] - expect(editSession.getCursorScreenPosition()).toEqual [4, 7] - - describe "when soft-wrap is enabled and code is folded", -> - beforeEach -> - editSession.setSoftWrap(true) - editSession.setEditorWidthInChars(50) - editSession.createFold(2, 3) - - it "positions the cursor at the buffer position that corresponds to the given screen position", -> - editSession.setCursorScreenPosition([9, 0]) - expect(editSession.getCursorBufferPosition()).toEqual [8, 11] - - describe ".moveCursorUp()", -> - it "moves the cursor up", -> - editSession.setCursorScreenPosition([2, 2]) - editSession.moveCursorUp() - expect(editSession.getCursorScreenPosition()).toEqual [1, 2] - - it "retains the goal column across lines of differing length", -> - expect(lineLengths[6]).toBeGreaterThan(32) - editSession.setCursorScreenPosition(row: 6, column: 32) - - editSession.moveCursorUp() - expect(editSession.getCursorScreenPosition().column).toBe lineLengths[5] - - editSession.moveCursorUp() - expect(editSession.getCursorScreenPosition().column).toBe lineLengths[4] - - editSession.moveCursorUp() - expect(editSession.getCursorScreenPosition().column).toBe 32 - - describe "when the cursor is on the first line", -> - it "moves the cursor to the beginning of the line, but retains the goal column", -> - editSession.setCursorScreenPosition([0, 4]) - editSession.moveCursorUp() - expect(editSession.getCursorScreenPosition()).toEqual([0, 0]) - - editSession.moveCursorDown() - expect(editSession.getCursorScreenPosition()).toEqual([1, 4]) - - describe "when there is a selection", -> - beforeEach -> - editSession.setSelectedBufferRange([[4, 9],[5, 10]]) - - it "moves above the selection", -> - cursor = editSession.getCursor() - editSession.moveCursorUp() - expect(cursor.getBufferPosition()).toEqual [3, 9] - - it "merges cursors when they overlap", -> - editSession.addCursorAtScreenPosition([1, 0]) - [cursor1, cursor2] = editSession.getCursors() - - editSession.moveCursorUp() - expect(editSession.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [0,0] - - describe ".moveCursorDown()", -> - it "moves the cursor down", -> - editSession.setCursorScreenPosition([2, 2]) - editSession.moveCursorDown() - expect(editSession.getCursorScreenPosition()).toEqual [3, 2] - - it "retains the goal column across lines of differing length", -> - editSession.setCursorScreenPosition(row: 3, column: lineLengths[3]) - - editSession.moveCursorDown() - expect(editSession.getCursorScreenPosition().column).toBe lineLengths[4] - - editSession.moveCursorDown() - expect(editSession.getCursorScreenPosition().column).toBe lineLengths[5] - - editSession.moveCursorDown() - expect(editSession.getCursorScreenPosition().column).toBe lineLengths[3] - - describe "when the cursor is on the last line", -> - it "moves the cursor to the end of line, but retains the goal column when moving back up", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - editSession.setCursorScreenPosition(row: lastLineIndex, column: editSession.getTabLength()) - editSession.moveCursorDown() - expect(editSession.getCursorScreenPosition()).toEqual(row: lastLineIndex, column: lastLine.length) - - editSession.moveCursorUp() - expect(editSession.getCursorScreenPosition().column).toBe editSession.getTabLength() - - it "retains a goal column of 0 when moving back up", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - editSession.setCursorScreenPosition(row: lastLineIndex, column: 0) - editSession.moveCursorDown() - editSession.moveCursorUp() - expect(editSession.getCursorScreenPosition().column).toBe 0 - - describe "when there is a selection", -> - beforeEach -> - editSession.setSelectedBufferRange([[4, 9],[5, 10]]) - - it "moves below the selection", -> - cursor = editSession.getCursor() - editSession.moveCursorDown() - expect(cursor.getBufferPosition()).toEqual [6, 10] - - it "merges cursors when they overlap", -> - editSession.setCursorScreenPosition([12, 2]) - editSession.addCursorAtScreenPosition([11, 2]) - [cursor1, cursor2] = editSession.getCursors() - - editSession.moveCursorDown() - expect(editSession.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [12,2] - - describe ".moveCursorLeft()", -> - it "moves the cursor by one column to the left", -> - editSession.setCursorScreenPosition([1, 8]) - editSession.moveCursorLeft() - expect(editSession.getCursorScreenPosition()).toEqual [1, 7] - - describe "when the cursor is in the first column", -> - describe "when there is a previous line", -> - it "wraps to the end of the previous line", -> - editSession.setCursorScreenPosition(row: 1, column: 0) - editSession.moveCursorLeft() - expect(editSession.getCursorScreenPosition()).toEqual(row: 0, column: buffer.lineForRow(0).length) - - describe "when the cursor is on the first line", -> - it "remains in the same position (0,0)", -> - editSession.setCursorScreenPosition(row: 0, column: 0) - editSession.moveCursorLeft() - expect(editSession.getCursorScreenPosition()).toEqual(row: 0, column: 0) - - describe "when softTabs is enabled and the cursor is preceded by leading whitespace", -> - it "skips tabLength worth of whitespace at a time", -> - editSession.setCursorBufferPosition([5, 6]) - - editSession.moveCursorLeft() - expect(editSession.getCursorBufferPosition()).toEqual [5, 4] - - describe "when there is a selection", -> - beforeEach -> - editSession.setSelectedBufferRange([[5, 22],[5, 27]]) - - it "moves to the left of the selection", -> - cursor = editSession.getCursor() - editSession.moveCursorLeft() - expect(cursor.getBufferPosition()).toEqual [5, 22] - - editSession.moveCursorLeft() - expect(cursor.getBufferPosition()).toEqual [5, 21] - - it "merges cursors when they overlap", -> - editSession.setCursorScreenPosition([0, 0]) - editSession.addCursorAtScreenPosition([0, 1]) - - [cursor1, cursor2] = editSession.getCursors() - editSession.moveCursorLeft() - expect(editSession.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [0,0] - - describe ".moveCursorRight()", -> - it "moves the cursor by one column to the right", -> - editSession.setCursorScreenPosition([3, 3]) - editSession.moveCursorRight() - expect(editSession.getCursorScreenPosition()).toEqual [3, 4] - - describe "when the cursor is on the last column of a line", -> - describe "when there is a subsequent line", -> - it "wraps to the beginning of the next line", -> - editSession.setCursorScreenPosition([0, buffer.lineForRow(0).length]) - editSession.moveCursorRight() - expect(editSession.getCursorScreenPosition()).toEqual [1, 0] - - describe "when the cursor is on the last line", -> - it "remains in the same position", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - lastPosition = { row: lastLineIndex, column: lastLine.length } - editSession.setCursorScreenPosition(lastPosition) - editSession.moveCursorRight() - - expect(editSession.getCursorScreenPosition()).toEqual(lastPosition) - - describe "when there is a selection", -> - beforeEach -> - editSession.setSelectedBufferRange([[5, 22],[5, 27]]) - - it "moves to the left of the selection", -> - cursor = editSession.getCursor() - editSession.moveCursorRight() - expect(cursor.getBufferPosition()).toEqual [5, 27] - - editSession.moveCursorRight() - expect(cursor.getBufferPosition()).toEqual [5, 28] - - it "merges cursors when they overlap", -> - editSession.setCursorScreenPosition([12, 2]) - editSession.addCursorAtScreenPosition([12, 1]) - [cursor1, cursor2] = editSession.getCursors() - - editSession.moveCursorRight() - expect(editSession.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [12,2] - - describe ".moveCursorToTop()", -> - it "moves the cursor to the top of the buffer", -> - editSession.setCursorScreenPosition [11,1] - editSession.addCursorAtScreenPosition [12,0] - editSession.moveCursorToTop() - expect(editSession.getCursors().length).toBe 1 - expect(editSession.getCursorBufferPosition()).toEqual [0,0] - - describe ".moveCursorToBottom()", -> - it "moves the cusor to the bottom of the buffer", -> - editSession.setCursorScreenPosition [0,0] - editSession.addCursorAtScreenPosition [1,0] - editSession.moveCursorToBottom() - expect(editSession.getCursors().length).toBe 1 - expect(editSession.getCursorBufferPosition()).toEqual [12,2] - - describe ".moveCursorToBeginningOfLine()", -> - describe "when soft wrap is on", -> - it "moves cursor to the beginning of the screen line", -> - editSession.setSoftWrap(true) - editSession.setEditorWidthInChars(10) - editSession.setCursorScreenPosition([1, 2]) - editSession.moveCursorToBeginningOfLine() - cursor = editSession.getCursor() - expect(cursor.getScreenPosition()).toEqual [1, 0] - - describe "when soft wrap is off", -> - it "moves cursor to the beginning of then line", -> - editSession.setCursorScreenPosition [0,5] - editSession.addCursorAtScreenPosition [1,7] - editSession.moveCursorToBeginningOfLine() - expect(editSession.getCursors().length).toBe 2 - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0,0] - expect(cursor2.getBufferPosition()).toEqual [1,0] - - describe ".moveCursorToEndOfLine()", -> - describe "when soft wrap is on", -> - it "moves cursor to the beginning of the screen line", -> - editSession.setSoftWrap(true) - editSession.setEditorWidthInChars(10) - editSession.setCursorScreenPosition([1, 2]) - editSession.moveCursorToEndOfLine() - cursor = editSession.getCursor() - expect(cursor.getScreenPosition()).toEqual [1, 9] - - describe "when soft wrap is off", -> - it "moves cursor to the end of line", -> - editSession.setCursorScreenPosition [0,0] - editSession.addCursorAtScreenPosition [1,0] - editSession.moveCursorToEndOfLine() - expect(editSession.getCursors().length).toBe 2 - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0,29] - expect(cursor2.getBufferPosition()).toEqual [1,30] - - describe ".moveCursorToFirstCharacterOfLine()", -> - describe "when soft wrap is on", -> - it "moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", -> - editSession.setSoftWrap(true) - editSession.setEditorWidthInChars(10) - editSession.setCursorScreenPosition [2,5] - editSession.addCursorAtScreenPosition [8,7] - - editSession.moveCursorToFirstCharacterOfLine() - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getScreenPosition()).toEqual [2,0] - expect(cursor2.getScreenPosition()).toEqual [8,4] - - editSession.moveCursorToFirstCharacterOfLine() - expect(cursor1.getScreenPosition()).toEqual [2,0] - expect(cursor2.getScreenPosition()).toEqual [8,0] - - describe "when soft wrap is off", -> - it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> - editSession.setCursorScreenPosition [0,5] - editSession.addCursorAtScreenPosition [1,7] - - editSession.moveCursorToFirstCharacterOfLine() - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0,0] - expect(cursor2.getBufferPosition()).toEqual [1,2] - - editSession.moveCursorToFirstCharacterOfLine() - expect(cursor1.getBufferPosition()).toEqual [0,0] - expect(cursor2.getBufferPosition()).toEqual [1,0] - - describe ".moveCursorToBeginningOfWord()", -> - it "moves the cursor to the beginning of the word", -> - editSession.setCursorBufferPosition [0, 8] - editSession.addCursorAtBufferPosition [1, 12] - editSession.addCursorAtBufferPosition [3, 0] - [cursor1, cursor2, cursor3] = editSession.getCursors() - - editSession.moveCursorToBeginningOfWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [1, 11] - expect(cursor3.getBufferPosition()).toEqual [2, 39] - - it "does not fail at position [0, 0]", -> - editSession.setCursorBufferPosition([0, 0]) - editSession.moveCursorToBeginningOfWord() - - it "treats lines with only whitespace as a word", -> - editSession.setCursorBufferPosition([11, 0]) - editSession.moveCursorToBeginningOfWord() - expect(editSession.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editSession.setCursorBufferPosition([10, 0]) - editSession.moveCursorToBeginningOfWord() - expect(editSession.getCursorBufferPosition()).toEqual [9, 2] - - describe ".moveCursorToPreviousWordBoundary()", -> - it "moves the cursor to the previous word boundary", -> - editSession.setCursorBufferPosition [0, 8] - editSession.addCursorAtBufferPosition [2, 0] - editSession.addCursorAtBufferPosition [2, 4] - editSession.addCursorAtBufferPosition [3, 14] - [cursor1, cursor2, cursor3, cursor4] = editSession.getCursors() - - editSession.moveCursorToPreviousWordBoundary() - - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [1, 30] - expect(cursor3.getBufferPosition()).toEqual [2, 0] - expect(cursor4.getBufferPosition()).toEqual [3, 13] - - describe ".moveCursorToNextWordBoundary()", -> - it "moves the cursor to the previous word boundary", -> - editSession.setCursorBufferPosition [0, 8] - editSession.addCursorAtBufferPosition [2, 40] - editSession.addCursorAtBufferPosition [3, 0] - editSession.addCursorAtBufferPosition [3, 30] - [cursor1, cursor2, cursor3, cursor4] = editSession.getCursors() - - editSession.moveCursorToNextWordBoundary() - - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 0] - expect(cursor3.getBufferPosition()).toEqual [3, 4] - expect(cursor4.getBufferPosition()).toEqual [3, 31] - - describe ".moveCursorToEndOfWord()", -> - it "moves the cursor to the end of the word", -> - editSession.setCursorBufferPosition [0, 6] - editSession.addCursorAtBufferPosition [1, 10] - editSession.addCursorAtBufferPosition [2, 40] - [cursor1, cursor2, cursor3] = editSession.getCursors() - - editSession.moveCursorToEndOfWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [1, 12] - expect(cursor3.getBufferPosition()).toEqual [3, 7] - - it "does not blow up when there is no next word", -> - editSession.setCursorBufferPosition [Infinity, Infinity] - endPosition = editSession.getCursorBufferPosition() - editSession.moveCursorToEndOfWord() - expect(editSession.getCursorBufferPosition()).toEqual endPosition - - it "treats lines with only whitespace as a word", -> - editSession.setCursorBufferPosition([9, 4]) - editSession.moveCursorToEndOfWord() - expect(editSession.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editSession.setCursorBufferPosition([10, 0]) - editSession.moveCursorToEndOfWord() - expect(editSession.getCursorBufferPosition()).toEqual [11, 8] - - describe ".moveCursorToBeginningOfNextWord()", -> - it "moves the cursor before the first character of the next word", -> - editSession.setCursorBufferPosition [0,6] - editSession.addCursorAtBufferPosition [1,11] - editSession.addCursorAtBufferPosition [2,0] - [cursor1, cursor2, cursor3] = editSession.getCursors() - - editSession.moveCursorToBeginningOfNextWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [1, 13] - expect(cursor3.getBufferPosition()).toEqual [2, 4] - - # When the cursor is on whitespace - editSession.setText("ab cde- ") - editSession.setCursorBufferPosition [0,2] - cursor = editSession.getCursor() - editSession.moveCursorToBeginningOfNextWord() - - expect(cursor.getBufferPosition()).toEqual [0, 3] - - it "does not blow up when there is no next word", -> - editSession.setCursorBufferPosition [Infinity, Infinity] - endPosition = editSession.getCursorBufferPosition() - editSession.moveCursorToBeginningOfNextWord() - expect(editSession.getCursorBufferPosition()).toEqual endPosition - - it "treats lines with only whitespace as a word", -> - editSession.setCursorBufferPosition([9, 4]) - editSession.moveCursorToBeginningOfNextWord() - expect(editSession.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editSession.setCursorBufferPosition([10, 0]) - editSession.moveCursorToBeginningOfNextWord() - expect(editSession.getCursorBufferPosition()).toEqual [11, 9] - - describe ".getCurrentParagraphBufferRange()", -> - it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", -> - buffer.setText """ - I am the first paragraph, - bordered by the beginning of - the file - #{' '} - - I am the second paragraph - with blank lines above and below - me. - - I am the last paragraph, - bordered by the end of the file. - """ - - # in a paragraph - editSession.setCursorBufferPosition([1, 7]) - expect(editSession.getCurrentParagraphBufferRange()).toEqual [[0, 0], [2, 8]] - - editSession.setCursorBufferPosition([7, 1]) - expect(editSession.getCurrentParagraphBufferRange()).toEqual [[5, 0], [7, 3]] - - editSession.setCursorBufferPosition([9, 10]) - expect(editSession.getCurrentParagraphBufferRange()).toEqual [[9, 0], [10, 32]] - - # between paragraphs - editSession.setCursorBufferPosition([3, 1]) - expect(editSession.getCurrentParagraphBufferRange()).toBeUndefined() - - describe "cursor-moved events", -> - cursorMovedHandler = null - - beforeEach -> - editSession.foldBufferRow(4) - editSession.setSelectedBufferRange([[8, 1], [9, 0]]) - cursorMovedHandler = jasmine.createSpy("cursorMovedHandler") - editSession.on 'cursor-moved', cursorMovedHandler - - describe "when the position of the cursor changes", -> - it "emits a cursor-moved event", -> - buffer.insert([9, 0], '...') - expect(cursorMovedHandler).toHaveBeenCalledWith( - oldBufferPosition: [9, 0] - oldScreenPosition: [6, 0] - newBufferPosition: [9, 3] - newScreenPosition: [6, 3] - textChanged: true - ) - - describe "when the position of the associated selection's tail changes, but not the cursor's position", -> - it "does not emit a cursor-moved event", -> - buffer.insert([8, 0], '...') - expect(cursorMovedHandler).not.toHaveBeenCalled() - - describe "addCursorAtScreenPosition(screenPosition)", -> - describe "when a cursor already exists at the position", -> - it "returns the existing cursor", -> - cursor1 = editSession.addCursorAtScreenPosition([0,2]) - cursor2 = editSession.addCursorAtScreenPosition([0,2]) - expect(cursor2.marker).toBe cursor1.marker - - describe "addCursorAtBufferPosition(bufferPosition)", -> - describe "when a cursor already exists at the position", -> - it "returns the existing cursor", -> - cursor1 = editSession.addCursorAtBufferPosition([1,4]) - cursor2 = editSession.addCursorAtBufferPosition([1,4]) - expect(cursor2.marker).toBe cursor1.marker - - describe "selection", -> - selection = null - - beforeEach -> - selection = editSession.getSelection() - - describe ".selectUp/Down/Left/Right()", -> - it "expands each selection to its cursor's new location", -> - editSession.setSelectedBufferRanges([[[0,9], [0,13]], [[3,16], [3,21]]]) - [selection1, selection2] = editSession.getSelections() - - editSession.selectRight() - expect(selection1.getBufferRange()).toEqual [[0,9], [0,14]] - expect(selection2.getBufferRange()).toEqual [[3,16], [3,22]] - - editSession.selectLeft() - editSession.selectLeft() - expect(selection1.getBufferRange()).toEqual [[0,9], [0,12]] - expect(selection2.getBufferRange()).toEqual [[3,16], [3,20]] - - editSession.selectDown() - expect(selection1.getBufferRange()).toEqual [[0,9], [1,12]] - expect(selection2.getBufferRange()).toEqual [[3,16], [4,20]] - - editSession.selectUp() - expect(selection1.getBufferRange()).toEqual [[0,9], [0,12]] - expect(selection2.getBufferRange()).toEqual [[3,16], [3,20]] - - it "merges selections when they intersect when moving down", -> - editSession.setSelectedBufferRanges([[[0,9], [0,13]], [[1,10], [1,20]], [[2,15], [3,25]]]) - [selection1, selection2, selection3] = editSession.getSelections() - - editSession.selectDown() - expect(editSession.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 9], [4, 25]]) - expect(selection1.isReversed()).toBeFalsy() - - it "merges selections when they intersect when moving up", -> - editSession.setSelectedBufferRanges([[[0,9], [0,13]], [[1,10], [1,20]]], isReversed: true) - [selection1, selection2] = editSession.getSelections() - - editSession.selectUp() - - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 0], [1, 20]]) - expect(selection1.isReversed()).toBeTruthy() - - it "merges selections when they intersect when moving left", -> - editSession.setSelectedBufferRanges([[[0,9], [0,13]], [[0,14], [1,20]]], isReversed: true) - [selection1, selection2] = editSession.getSelections() - - editSession.selectLeft() - expect(editSession.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 8], [1, 20]]) - expect(selection1.isReversed()).toBeTruthy() - - it "merges selections when they intersect when moving right", -> - editSession.setSelectedBufferRanges([[[0,9], [0,13]], [[0,14], [1,20]]]) - [selection1, selection2] = editSession.getSelections() - - editSession.selectRight() - expect(editSession.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]) - expect(selection1.isReversed()).toBeFalsy() - - describe ".selectToScreenPosition(screenPosition)", -> - it "expands the last selection to the given position", -> - editSession.setSelectedBufferRange([[3, 0], [4, 5]]) - editSession.addCursorAtScreenPosition([5, 6]) - editSession.selectToScreenPosition([6, 2]) - - selections = editSession.getSelections() - expect(selections.length).toBe 2 - [selection1, selection2] = selections - expect(selection1.getScreenRange()).toEqual [[3, 0], [4, 5]] - expect(selection2.getScreenRange()).toEqual [[5, 6], [6, 2]] - - it "merges selections if they intersect, maintaining the directionality of the last selection", -> - editSession.setCursorScreenPosition([4, 10]) - editSession.selectToScreenPosition([5, 27]) - editSession.addCursorAtScreenPosition([3, 10]) - editSession.selectToScreenPosition([6, 27]) - - selections = editSession.getSelections() - expect(selections.length).toBe 1 - [selection1] = selections - expect(selection1.getScreenRange()).toEqual [[3, 10], [6, 27]] - expect(selection1.isReversed()).toBeFalsy() - - editSession.addCursorAtScreenPosition([7, 4]) - editSession.selectToScreenPosition([4, 11]) - - selections = editSession.getSelections() - expect(selections.length).toBe 1 - [selection1] = selections - expect(selection1.getScreenRange()).toEqual [[3, 10], [7, 4]] - expect(selection1.isReversed()).toBeTruthy() - - describe ".selectToTop()", -> - it "selects text from cusor position to the top of the buffer", -> - editSession.setCursorScreenPosition [11,2] - editSession.addCursorAtScreenPosition [10,0] - editSession.selectToTop() - expect(editSession.getCursors().length).toBe 1 - expect(editSession.getCursorBufferPosition()).toEqual [0,0] - expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [11,2]] - expect(editSession.getSelection().isReversed()).toBeTruthy() - - describe ".selectToBottom()", -> - it "selects text from cusor position to the bottom of the buffer", -> - editSession.setCursorScreenPosition [10,0] - editSession.addCursorAtScreenPosition [9,3] - editSession.selectToBottom() - expect(editSession.getCursors().length).toBe 1 - expect(editSession.getCursorBufferPosition()).toEqual [12,2] - expect(editSession.getSelection().getBufferRange()).toEqual [[9,3], [12,2]] - expect(editSession.getSelection().isReversed()).toBeFalsy() - - describe ".selectAll()", -> - it "selects the entire buffer", -> - editSession.selectAll() - expect(editSession.getSelection().getBufferRange()).toEqual buffer.getRange() - - describe ".selectToBeginningOfLine()", -> - it "selects text from cusor position to beginning of line", -> - editSession.setCursorScreenPosition [12,2] - editSession.addCursorAtScreenPosition [11,3] - - editSession.selectToBeginningOfLine() - - expect(editSession.getCursors().length).toBe 2 - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [12,0] - expect(cursor2.getBufferPosition()).toEqual [11,0] - - expect(editSession.getSelections().length).toBe 2 - [selection1, selection2] = editSession.getSelections() - expect(selection1.getBufferRange()).toEqual [[12,0], [12,2]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[11,0], [11,3]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".selectToEndOfLine()", -> - it "selects text from cusor position to end of line", -> - editSession.setCursorScreenPosition [12,0] - editSession.addCursorAtScreenPosition [11,3] - - editSession.selectToEndOfLine() - - expect(editSession.getCursors().length).toBe 2 - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [12,2] - expect(cursor2.getBufferPosition()).toEqual [11,44] - - expect(editSession.getSelections().length).toBe 2 - [selection1, selection2] = editSession.getSelections() - expect(selection1.getBufferRange()).toEqual [[12,0], [12,2]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[11,3], [11,44]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectLine()", -> - it "selects the entire line (including newlines) at given row", -> - editSession.setCursorScreenPosition([1, 2]) - editSession.selectLine() - expect(editSession.getSelectedBufferRange()).toEqual [[1,0], [2,0]] - expect(editSession.getSelectedText()).toBe " var sort = function(items) {\n" - - editSession.setCursorScreenPosition([12, 2]) - editSession.selectLine() - expect(editSession.getSelectedBufferRange()).toEqual [[12,0], [12,2]] - - describe ".selectToBeginningOfWord()", -> - it "selects text from cusor position to beginning of word", -> - editSession.setCursorScreenPosition [0,13] - editSession.addCursorAtScreenPosition [3,49] - - editSession.selectToBeginningOfWord() - - expect(editSession.getCursors().length).toBe 2 - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0,4] - expect(cursor2.getBufferPosition()).toEqual [3,47] - - expect(editSession.getSelections().length).toBe 2 - [selection1, selection2] = editSession.getSelections() - expect(selection1.getBufferRange()).toEqual [[0,4], [0,13]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[3,47], [3,49]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".selectToEndOfWord()", -> - it "selects text from cusor position to end of word", -> - editSession.setCursorScreenPosition [0,4] - editSession.addCursorAtScreenPosition [3,48] - - editSession.selectToEndOfWord() - - expect(editSession.getCursors().length).toBe 2 - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0,13] - expect(cursor2.getBufferPosition()).toEqual [3,50] - - expect(editSession.getSelections().length).toBe 2 - [selection1, selection2] = editSession.getSelections() - expect(selection1.getBufferRange()).toEqual [[0,4], [0,13]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[3,48], [3,50]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectToBeginningOfNextWord()", -> - it "selects text from cusor position to beginning of next word", -> - editSession.setCursorScreenPosition [0,4] - editSession.addCursorAtScreenPosition [3,48] - - editSession.selectToBeginningOfNextWord() - - expect(editSession.getCursors().length).toBe 2 - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0,14] - expect(cursor2.getBufferPosition()).toEqual [3,51] - - expect(editSession.getSelections().length).toBe 2 - [selection1, selection2] = editSession.getSelections() - expect(selection1.getBufferRange()).toEqual [[0,4], [0,14]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[3,48], [3,51]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectToPreviousWordBoundary()", -> - it "select to the previous word boundary", -> - editSession.setCursorBufferPosition [0, 8] - editSession.addCursorAtBufferPosition [2, 0] - editSession.addCursorAtBufferPosition [3, 4] - editSession.addCursorAtBufferPosition [3, 14] - - editSession.selectToPreviousWordBoundary() - - expect(editSession.getSelections().length).toBe 4 - [selection1, selection2, selection3, selection4] = editSession.getSelections() - expect(selection1.getBufferRange()).toEqual [[0,8], [0,4]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[2,0], [1,30]] - expect(selection2.isReversed()).toBeTruthy() - expect(selection3.getBufferRange()).toEqual [[3,4], [3,0]] - expect(selection3.isReversed()).toBeTruthy() - expect(selection4.getBufferRange()).toEqual [[3,14], [3,13]] - expect(selection4.isReversed()).toBeTruthy() - - describe ".selectToNextWordBoundary()", -> - it "select to the next word boundary", -> - editSession.setCursorBufferPosition [0, 8] - editSession.addCursorAtBufferPosition [2, 40] - editSession.addCursorAtBufferPosition [4, 0] - editSession.addCursorAtBufferPosition [3, 30] - - editSession.selectToNextWordBoundary() - - expect(editSession.getSelections().length).toBe 4 - [selection1, selection2, selection3, selection4] = editSession.getSelections() - expect(selection1.getBufferRange()).toEqual [[0,8], [0,13]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[2,40], [3,0]] - expect(selection2.isReversed()).toBeFalsy() - expect(selection3.getBufferRange()).toEqual [[4,0], [4,4]] - expect(selection3.isReversed()).toBeFalsy() - expect(selection4.getBufferRange()).toEqual [[3,30], [3,31]] - expect(selection4.isReversed()).toBeFalsy() - - describe ".selectWord()", -> - describe "when the cursor is inside a word", -> - it "selects the entire word", -> - editSession.setCursorScreenPosition([0, 8]) - editSession.selectWord() - expect(editSession.getSelectedText()).toBe 'quicksort' - - describe "when the cursor is between two words", -> - it "selects the word the cursor is on", -> - editSession.setCursorScreenPosition([0, 4]) - editSession.selectWord() - expect(editSession.getSelectedText()).toBe 'quicksort' - - editSession.setCursorScreenPosition([0, 3]) - editSession.selectWord() - expect(editSession.getSelectedText()).toBe 'var' - - - describe "when the cursor is inside a region of whitespace", -> - it "selects the whitespace region", -> - editSession.setCursorScreenPosition([5, 2]) - editSession.selectWord() - expect(editSession.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] - - editSession.setCursorScreenPosition([5, 0]) - editSession.selectWord() - expect(editSession.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] - - describe "when the cursor is at the end of the text", -> - it "select the previous word", -> - editSession.buffer.append 'word' - editSession.moveCursorToBottom() - editSession.selectWord() - expect(editSession.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]] - - describe ".selectToFirstCharacterOfLine()", -> - it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> - editSession.setCursorScreenPosition [0,5] - editSession.addCursorAtScreenPosition [1,7] - - editSession.selectToFirstCharacterOfLine() - - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0,0] - expect(cursor2.getBufferPosition()).toEqual [1,2] - - expect(editSession.getSelections().length).toBe 2 - [selection1, selection2] = editSession.getSelections() - expect(selection1.getBufferRange()).toEqual [[0,0], [0,5]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[1,2], [1,7]] - expect(selection2.isReversed()).toBeTruthy() - - editSession.selectToFirstCharacterOfLine() - [selection1, selection2] = editSession.getSelections() - expect(selection1.getBufferRange()).toEqual [[0,0], [0,5]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[1,0], [1,7]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".setSelectedBufferRanges(ranges)", -> - it "clears existing selections and creates selections for each of the given ranges", -> - editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) - expect(editSession.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[4, 4], [5, 5]]] - - editSession.setSelectedBufferRanges([[[5, 5], [6, 6]]]) - expect(editSession.getSelectedBufferRanges()).toEqual [[[5, 5], [6, 6]]] - - it "merges intersecting selections", -> - editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) - expect(editSession.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] - - it "recyles existing selection instances", -> - selection = editSession.getSelection() - editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) - - [selection1, selection2] = editSession.getSelections() - expect(selection1).toBe selection - expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]] - - describe "when the preserveFolds option is false (the default)", -> - it "removes folds that contain the selections", -> - editSession.setSelectedBufferRange([[0,0], [0,0]]) - editSession.createFold(1, 4) - editSession.createFold(2, 3) - editSession.createFold(6, 8) - editSession.createFold(10, 11) - - editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) - expect(editSession.lineForScreenRow(1).fold).toBeUndefined() - expect(editSession.lineForScreenRow(2).fold).toBeUndefined() - expect(editSession.lineForScreenRow(6).fold).toBeUndefined() - expect(editSession.lineForScreenRow(10).fold).toBeDefined() - - describe "when the preserve folds option is true", -> - it "does not remove folds that contain the selections", -> - editSession.setSelectedBufferRange([[0,0], [0,0]]) - editSession.createFold(1, 4) - editSession.createFold(6, 8) - editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], preserveFolds: true) - expect(editSession.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editSession.isFoldedAtBufferRow(6)).toBeTruthy() - - describe ".selectMarker(marker)", -> - describe "if the marker is valid", -> - it "selects the marker's range and returns the selected range", -> - marker = editSession.markBufferRange([[0, 1], [3, 3]]) - expect(editSession.selectMarker(marker)).toEqual [[0, 1], [3, 3]] - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [3, 3]] - - describe "if the marker is invalid", -> - it "does not change the selection and returns a falsy value", -> - marker = editSession.markBufferRange([[0, 1], [3, 3]]) - marker.destroy() - expect(editSession.selectMarker(marker)).toBeFalsy() - expect(editSession.getSelectedBufferRange()).toEqual [[0, 0], [0, 0]] - - describe ".addSelectionBelow()", -> - describe "when the selection is non-empty", -> - it "selects the same region of the line below current selections if possible", -> - editSession.setSelectedBufferRange([[3, 16], [3, 21]]) - editSession.addSelectionForBufferRange([[3, 25], [3, 34]]) - editSession.addSelectionBelow() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 16], [3, 21]] - [[3, 25], [3, 34]] - [[4, 16], [4, 21]] - [[4, 25], [4, 29]] - ] - for cursor in editSession.getCursors() - expect(cursor.isVisible()).toBeFalsy() - - it "skips lines that are too short to create a non-empty selection", -> - editSession.setSelectedBufferRange([[3, 31], [3, 38]]) - editSession.addSelectionBelow() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 31], [3, 38]] - [[6, 31], [6, 38]] - ] - - it "honors the original selection's range (goal range) when adding across shorter lines", -> - editSession.setSelectedBufferRange([[3, 22], [3, 38]]) - editSession.addSelectionBelow() - editSession.addSelectionBelow() - editSession.addSelectionBelow() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 38]] - [[4, 22], [4, 29]] - [[5, 22], [5, 30]] - [[6, 22], [6, 38]] - ] - - it "clears selection goal ranges when the selection changes", -> - editSession.setSelectedBufferRange([[3, 22], [3, 38]]) - editSession.addSelectionBelow() - editSession.selectLeft() - editSession.addSelectionBelow() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 37]] - [[4, 22], [4, 29]] - [[5, 22], [5, 28]] - ] - - # goal range from previous add selection is honored next time - editSession.addSelectionBelow() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 37]] - [[4, 22], [4, 29]] - [[5, 22], [5, 30]] # select to end of line 5 because line 4's goal range was reset by line 3 previously - [[6, 22], [6, 28]] - ] - - describe "when the selection is empty", -> - it "does not skip lines that are shorter than the current column", -> - editSession.setCursorBufferPosition([3, 36]) - editSession.addSelectionBelow() - editSession.addSelectionBelow() - editSession.addSelectionBelow() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 36], [3, 36]] - [[4, 29], [4, 29]] - [[5, 30], [5, 30]] - [[6, 36], [6, 36]] - ] - - it "skips empty lines when the column is non-zero", -> - editSession.setCursorBufferPosition([9, 4]) - editSession.addSelectionBelow() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[9, 4], [9, 4]] - [[11, 4], [11, 4]] - ] - - it "does not skip empty lines when the column is zero", -> - editSession.setCursorBufferPosition([9, 0]) - editSession.addSelectionBelow() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[9, 0], [9, 0]] - [[10, 0], [10, 0]] - ] - - describe ".addSelectionAbove()", -> - describe "when the selection is non-empty", -> - it "selects the same region of the line above current selections if possible", -> - editSession.setSelectedBufferRange([[3, 16], [3, 21]]) - editSession.addSelectionForBufferRange([[3, 37], [3, 44]]) - editSession.addSelectionAbove() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[2, 16], [2, 21]] - [[2, 37], [2, 40]] - [[3, 16], [3, 21]] - [[3, 37], [3, 44]] - ] - for cursor in editSession.getCursors() - expect(cursor.isVisible()).toBeFalsy() - - it "skips lines that are too short to create a non-empty selection", -> - editSession.setSelectedBufferRange([[6, 31], [6, 38]]) - editSession.addSelectionAbove() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 31], [3, 38]] - [[6, 31], [6, 38]] - ] - - it "honors the original selection's range (goal range) when adding across shorter lines", -> - editSession.setSelectedBufferRange([[6, 22], [6, 38]]) - editSession.addSelectionAbove() - editSession.addSelectionAbove() - editSession.addSelectionAbove() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 38]] - [[4, 22], [4, 29]] - [[5, 22], [5, 30]] - [[6, 22], [6, 38]] - ] - - describe "when the selection is empty", -> - it "does not skip lines that are shorter than the current column", -> - editSession.setCursorBufferPosition([6, 36]) - editSession.addSelectionAbove() - editSession.addSelectionAbove() - editSession.addSelectionAbove() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 36], [3, 36]] - [[4, 29], [4, 29]] - [[5, 30], [5, 30]] - [[6, 36], [6, 36]] - ] - - it "skips empty lines when the column is non-zero", -> - editSession.setCursorBufferPosition([11, 4]) - editSession.addSelectionAbove() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[9, 4], [9, 4]] - [[11, 4], [11, 4]] - ] - - it "does not skip empty lines when the column is zero", -> - editSession.setCursorBufferPosition([10, 0]) - editSession.addSelectionAbove() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[9, 0], [9, 0]] - [[10, 0], [10, 0]] - ] - - describe ".consolidateSelections()", -> - it "destroys all selections but the most recent, returning true if any selections were destroyed", -> - editSession.setSelectedBufferRange([[3, 16], [3, 21]]) - selection1 = editSession.getSelection() - selection2 = editSession.addSelectionForBufferRange([[3, 25], [3, 34]]) - selection3 = editSession.addSelectionForBufferRange([[8, 4], [8, 10]]) - - expect(editSession.getSelections()).toEqual [selection1, selection2, selection3] - expect(editSession.consolidateSelections()).toBeTruthy() - expect(editSession.getSelections()).toEqual [selection3] - expect(selection3.isEmpty()).toBeFalsy() - expect(editSession.consolidateSelections()).toBeFalsy() - expect(editSession.getSelections()).toEqual [selection3] - - describe "when the cursor is moved while there is a selection", -> - makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] - - it "clears the selection", -> - makeSelection() - editSession.moveCursorDown() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editSession.moveCursorUp() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editSession.moveCursorLeft() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editSession.moveCursorRight() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editSession.setCursorScreenPosition([3, 3]) - expect(selection.isEmpty()).toBeTruthy() - - it "does not share selections between different edit sessions for the same buffer", -> - editSession2 = project.openSync('sample.js') - editSession.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) - editSession2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]) - expect(editSession2.getSelectedBufferRanges()).not.toEqual editSession.getSelectedBufferRanges() - - describe "buffer manipulation", -> - describe ".insertText(text)", -> - describe "when there are multiple empty selections", -> - describe "when the cursors are on the same line", -> - it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> - editSession.setCursorScreenPosition([1, 2]) - editSession.addCursorAtScreenPosition([1, 5]) - - editSession.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe ' xxxvarxxx sort = function(items) {' - [cursor1, cursor2] = editSession.getCursors() - - expect(cursor1.getBufferPosition()).toEqual [1, 5] - expect(cursor2.getBufferPosition()).toEqual [1, 11] - - describe "when the cursors are on different lines", -> - it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> - editSession.setCursorScreenPosition([1, 2]) - editSession.addCursorAtScreenPosition([2, 4]) - - editSession.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe ' xxxvar sort = function(items) {' - expect(buffer.lineForRow(2)).toBe ' xxxif (items.length <= 1) return items;' - [cursor1, cursor2] = editSession.getCursors() - - expect(cursor1.getBufferPosition()).toEqual [1, 5] - expect(cursor2.getBufferPosition()).toEqual [2, 7] - - describe "when there are multiple non-empty selections", -> - describe "when the selections are on the same line", -> - it "replaces each selection range with the inserted characters", -> - editSession.setSelectedBufferRanges([[[0,4], [0,13]], [[0,22], [0,24]]]) - editSession.insertText("x") - - [cursor1, cursor2] = editSession.getCursors() - [selection1, selection2] = editSession.getSelections() - - expect(cursor1.getScreenPosition()).toEqual [0, 5] - expect(cursor2.getScreenPosition()).toEqual [0, 15] - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - expect(editSession.lineForBufferRow(0)).toBe "var x = functix () {" - - describe "when the selections are on different lines", -> - it "replaces each selection with the given text, clears the selections, and places the cursor at the end of each selection's inserted text", -> - editSession.setSelectedBufferRanges([[[1, 0], [1, 2]], [[2, 0], [2, 4]]]) - - editSession.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - expect(buffer.lineForRow(2)).toBe 'xxxif (items.length <= 1) return items;' - [selection1, selection2] = editSession.getSelections() - - expect(selection1.isEmpty()).toBeTruthy() - expect(selection1.cursor.getBufferPosition()).toEqual [1, 3] - expect(selection2.isEmpty()).toBeTruthy() - expect(selection2.cursor.getBufferPosition()).toEqual [2, 3] - - describe "when there is a selection that ends on a folded line", -> - it "destroys the selection", -> - editSession.createFold(2,4) - editSession.setSelectedBufferRange([[1,0], [2,0]]) - editSession.insertText('holy cow') - expect(editSession.lineForScreenRow(2).fold).toBeUndefined() - - describe ".insertNewline()", -> - describe "when there is a single cursor", -> - describe "when the cursor is at the beginning of a line", -> - it "inserts an empty line before it", -> - editSession.setCursorScreenPosition(row: 1, column: 0) - - editSession.insertNewline() - - expect(buffer.lineForRow(1)).toBe '' - expect(editSession.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when the cursor is in the middle of a line", -> - it "splits the current line to form a new line", -> - editSession.setCursorScreenPosition(row: 1, column: 6) - originalLine = buffer.lineForRow(1) - lineBelowOriginalLine = buffer.lineForRow(2) - - editSession.insertNewline() - - expect(buffer.lineForRow(1)).toBe originalLine[0...6] - expect(buffer.lineForRow(2)).toBe originalLine[6..] - expect(buffer.lineForRow(3)).toBe lineBelowOriginalLine - expect(editSession.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when the cursor is on the end of a line", -> - it "inserts an empty line after it", -> - editSession.setCursorScreenPosition(row: 1, column: buffer.lineForRow(1).length) - - editSession.insertNewline() - - expect(buffer.lineForRow(2)).toBe '' - expect(editSession.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when there are multiple cursors", -> - describe "when the cursors are on the same line", -> - it "breaks the line at the cursor locations", -> - editSession.setCursorScreenPosition([3, 13]) - editSession.addCursorAtScreenPosition([3, 38]) - - editSession.insertNewline() - - expect(editSession.lineForBufferRow(3)).toBe " var pivot" - expect(editSession.lineForBufferRow(4)).toBe " = items.shift(), current" - expect(editSession.lineForBufferRow(5)).toBe ", left = [], right = [];" - expect(editSession.lineForBufferRow(6)).toBe " while(items.length > 0) {" - - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [4, 0] - expect(cursor2.getBufferPosition()).toEqual [5, 0] - - describe "when the cursors are on different lines", -> - it "inserts newlines at each cursor location", -> - editSession.setCursorScreenPosition([3, 0]) - editSession.addCursorAtScreenPosition([6, 0]) - - editSession.insertText("\n") - expect(editSession.lineForBufferRow(3)).toBe "" - expect(editSession.lineForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editSession.lineForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editSession.lineForBufferRow(6)).toBe " current = items.shift();" - expect(editSession.lineForBufferRow(7)).toBe "" - expect(editSession.lineForBufferRow(8)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editSession.lineForBufferRow(9)).toBe " }" - - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [4,0] - expect(cursor2.getBufferPosition()).toEqual [8,0] - - describe ".insertNewlineBelow()", -> - describe "when the operation is undone", -> - it "places the cursor back at the previous location", -> - editSession.setCursorBufferPosition([0,2]) - editSession.insertNewlineBelow() - expect(editSession.getCursorBufferPosition()).toEqual [1,0] - editSession.undo() - expect(editSession.getCursorBufferPosition()).toEqual [0,2] - - it "inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", -> - atom.config.set("editor.autoIndent", true) - editSession.insertNewlineBelow() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " " - expect(editSession.getCursorBufferPosition()).toEqual [1, 2] - - describe ".insertNewlineAbove()", -> - describe "when the cursor is on first line", -> - it "inserts a newline on the first line and moves the cursor to the first line", -> - editSession.setCursorBufferPosition([0]) - editSession.insertNewlineAbove() - expect(editSession.getCursorBufferPosition()).toEqual [0,0] - expect(editSession.lineForBufferRow(0)).toBe '' - expect(editSession.lineForBufferRow(1)).toBe 'var quicksort = function () {' - expect(editSession.buffer.getLineCount()).toBe 14 - - describe "when the cursor is not on the first line", -> - it "inserts a newline above the current line and moves the cursor to the inserted line", -> - editSession.setCursorBufferPosition([3,4]) - editSession.insertNewlineAbove() - expect(editSession.getCursorBufferPosition()).toEqual [3,0] - expect(editSession.lineForBufferRow(3)).toBe '' - expect(editSession.lineForBufferRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(editSession.buffer.getLineCount()).toBe 14 - - editSession.undo() - expect(editSession.getCursorBufferPosition()).toEqual [3,4] - - describe ".backspace()", -> - describe "when there is a single cursor", -> - changeScreenRangeHandler = null - - beforeEach -> - selection = editSession.getLastSelection() - changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') - selection.on 'screen-range-changed', changeScreenRangeHandler - - describe "when the cursor is on the middle of the line", -> - it "removes the character before the cursor", -> - editSession.setCursorScreenPosition(row: 1, column: 7) - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - - editSession.backspace() - - line = buffer.lineForRow(1) - expect(line).toBe " var ort = function(items) {" - expect(editSession.getCursorScreenPosition()).toEqual {row: 1, column: 6} - expect(changeScreenRangeHandler).toHaveBeenCalled() - expect(editSession.getCursor().isVisible()).toBeTruthy() - - describe "when the cursor is at the beginning of a line", -> - it "joins it with the line above", -> - originalLine0 = buffer.lineForRow(0) - expect(originalLine0).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - - editSession.setCursorScreenPosition(row: 1, column: 0) - editSession.backspace() - - line0 = buffer.lineForRow(0) - line1 = buffer.lineForRow(1) - expect(line0).toBe "var quicksort = function () { var sort = function(items) {" - expect(line1).toBe " if (items.length <= 1) return items;" - expect(editSession.getCursorScreenPosition()).toEqual [0, originalLine0.length] - - expect(changeScreenRangeHandler).toHaveBeenCalled() - - describe "when the cursor is at the first column of the first line", -> - it "does nothing, but doesn't raise an error", -> - editSession.setCursorScreenPosition(row: 0, column: 0) - editSession.backspace() - - describe "when the cursor is on the first column of a line below a fold", -> - it "deletes the folded lines", -> - editSession.setCursorScreenPosition([4,0]) - editSession.foldCurrentRow() - editSession.setCursorScreenPosition([5,0]) - editSession.backspace() - - expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(buffer.lineForRow(4).fold).toBeUndefined() - - describe "when the cursor is in the middle of a line below a fold", -> - it "backspaces as normal", -> - editSession.setCursorScreenPosition([4,0]) - editSession.foldCurrentRow() - editSession.setCursorScreenPosition([5,5]) - editSession.backspace() - - expect(buffer.lineForRow(7)).toBe " }" - expect(buffer.lineForRow(8)).toBe " eturn sort(left).concat(pivot).concat(sort(right));" - - describe "when the cursor is on a folded screen line", -> - it "deletes all of the folded lines along with the fold", -> - editSession.setCursorBufferPosition([3, 0]) - editSession.foldCurrentRow() - editSession.backspace() - - expect(buffer.lineForRow(1)).toBe "" - expect(buffer.lineForRow(2)).toBe " return sort(Array.apply(this, arguments));" - expect(editSession.getCursorScreenPosition()).toEqual [1, 0] - - describe "when there are multiple cursors", -> - describe "when cursors are on the same line", -> - it "removes the characters preceding each cursor", -> - editSession.setCursorScreenPosition([3, 13]) - editSession.addCursorAtScreenPosition([3, 38]) - - editSession.backspace() - - expect(editSession.lineForBufferRow(3)).toBe " var pivo = items.shift(), curren, left = [], right = [];" - - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 12] - expect(cursor2.getBufferPosition()).toEqual [3, 36] - - [selection1, selection2] = editSession.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when cursors are on different lines", -> - describe "when the cursors are in the middle of their lines", -> - it "removes the characters preceding each cursor", -> - editSession.setCursorScreenPosition([3, 13]) - editSession.addCursorAtScreenPosition([4, 10]) - - editSession.backspace() - - expect(editSession.lineForBufferRow(3)).toBe " var pivo = items.shift(), current, left = [], right = [];" - expect(editSession.lineForBufferRow(4)).toBe " whileitems.length > 0) {" - - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 12] - expect(cursor2.getBufferPosition()).toEqual [4, 9] - - [selection1, selection2] = editSession.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when the cursors are on the first column of their lines", -> - it "removes the newlines preceding each cursor", -> - editSession.setCursorScreenPosition([3, 0]) - editSession.addCursorAtScreenPosition([6, 0]) - - editSession.backspace() - expect(editSession.lineForBufferRow(2)).toBe " if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = [];" - expect(editSession.lineForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editSession.lineForBufferRow(4)).toBe " current = items.shift(); current < pivot ? left.push(current) : right.push(current);" - expect(editSession.lineForBufferRow(5)).toBe " }" - - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [2,40] - expect(cursor2.getBufferPosition()).toEqual [4,30] - - describe "when there is a single selection", -> - it "deletes the selection, but not the character before it", -> - editSession.setSelectedBufferRange([[0,5], [0,9]]) - editSession.backspace() - expect(editSession.buffer.lineForRow(0)).toBe 'var qsort = function () {' - - describe "when the selection ends on a folded line", -> - it "preserves the fold", -> - editSession.setSelectedBufferRange([[3,0], [4,0]]) - editSession.foldBufferRow(4) - editSession.backspace() - - expect(buffer.lineForRow(3)).toBe " while(items.length > 0) {" - expect(editSession.lineForScreenRow(3).fold).toBeDefined() - - describe "when there are multiple selections", -> - it "removes all selected text", -> - editSession.setSelectedBufferRanges([[[0,4], [0,13]], [[0,16], [0,24]]]) - editSession.backspace() - expect(editSession.lineForBufferRow(0)).toBe 'var = () {' - - describe ".backspaceToBeginningOfWord()", -> - describe "when no text is selected", -> - it "deletes all text between the cursor and the beginning of the word", -> - editSession.setCursorBufferPosition([1, 24]) - editSession.addCursorAtBufferPosition([3, 5]) - [cursor1, cursor2] = editSession.getCursors() - - editSession.backspaceToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(ems) {' - expect(buffer.lineForRow(3)).toBe ' ar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 22] - expect(cursor2.getBufferPosition()).toEqual [3, 4] - - editSession.backspaceToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = functionems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 21] - expect(cursor2.getBufferPosition()).toEqual [2, 39] - - editSession.backspaceToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = ems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 13] - expect(cursor2.getBufferPosition()).toEqual [2, 34] - - describe "when text is selected", -> - it "deletes only selected text", -> - editSession.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editSession.backspaceToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - - describe ".backspaceToBeginningOfLine()", -> - describe "when no text is selected", -> - it "deletes all text between the cursor and the beginning of the line", -> - editSession.setCursorBufferPosition([1, 24]) - editSession.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editSession.getCursors() - - editSession.backspaceToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe 'ems) {' - expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 0] - expect(cursor2.getBufferPosition()).toEqual [2, 0] - - describe "when at the beginning of the line", -> - it "deletes the newline", -> - editSession.setCursorBufferPosition([2]) - editSession.backspaceToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe "when text is selected", -> - it "still deletes all text to begginning of the line", -> - editSession.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editSession.backspaceToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe 'ems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' - - describe ".delete()", -> - describe "when there is a single cursor", -> - describe "when the cursor is on the middle of a line", -> - it "deletes the character following the cursor", -> - editSession.setCursorScreenPosition([1, 6]) - editSession.delete() - expect(buffer.lineForRow(1)).toBe ' var ort = function(items) {' - - describe "when the cursor is on the end of a line", -> - it "joins the line with the following line", -> - editSession.setCursorScreenPosition([1, buffer.lineForRow(1).length]) - editSession.delete() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe "when the cursor is on the last column of the last line", -> - it "does nothing, but doesn't raise an error", -> - editSession.setCursorScreenPosition([12, buffer.lineForRow(12).length]) - editSession.delete() - expect(buffer.lineForRow(12)).toBe '};' - - describe "when the cursor is on the end of a line above a fold", -> - it "only deletes the lines inside the fold", -> - editSession.foldBufferRow(4) - editSession.setCursorScreenPosition([3, Infinity]) - cursorPositionBefore = editSession.getCursorScreenPosition() - - editSession.delete() - - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editSession.getCursorScreenPosition()).toEqual cursorPositionBefore - - describe "when the cursor is in the middle a line above a fold", -> - it "deletes as normal", -> - editSession.foldBufferRow(4) - editSession.setCursorScreenPosition([3, 4]) - cursorPositionBefore = editSession.getCursorScreenPosition() - - editSession.delete() - - expect(buffer.lineForRow(3)).toBe " ar pivot = items.shift(), current, left = [], right = [];" - expect(editSession.lineForScreenRow(4).fold).toBeDefined() - expect(editSession.getCursorScreenPosition()).toEqual [3, 4] - - describe "when the cursor is on a folded line", -> - it "removes the lines contained by the fold", -> - editSession.setSelectedBufferRange([[2, 0], [2, 0]]) - editSession.createFold(2,4) - editSession.createFold(2,6) - oldLine7 = buffer.lineForRow(7) - oldLine8 = buffer.lineForRow(8) - - editSession.delete() - expect(editSession.lineForScreenRow(2).text).toBe oldLine7 - expect(editSession.lineForScreenRow(3).text).toBe oldLine8 - - describe "when there are multiple cursors", -> - describe "when cursors are on the same line", -> - it "removes the characters following each cursor", -> - editSession.setCursorScreenPosition([3, 13]) - editSession.addCursorAtScreenPosition([3, 38]) - - editSession.delete() - - expect(editSession.lineForBufferRow(3)).toBe " var pivot= items.shift(), current left = [], right = [];" - - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 37] - - [selection1, selection2] = editSession.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when cursors are on different lines", -> - describe "when the cursors are in the middle of the lines", -> - it "removes the characters following each cursor", -> - editSession.setCursorScreenPosition([3, 13]) - editSession.addCursorAtScreenPosition([4, 10]) - - editSession.delete() - - expect(editSession.lineForBufferRow(3)).toBe " var pivot= items.shift(), current, left = [], right = [];" - expect(editSession.lineForBufferRow(4)).toBe " while(tems.length > 0) {" - - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 13] - expect(cursor2.getBufferPosition()).toEqual [4, 10] - - [selection1, selection2] = editSession.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when the cursors are at the end of their lines", -> - it "removes the newlines following each cursor", -> - editSession.setCursorScreenPosition([0, 29]) - editSession.addCursorAtScreenPosition([1, 30]) - - editSession.delete() - - expect(editSession.lineForBufferRow(0)).toBe "var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items;" - - [cursor1, cursor2] = editSession.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0,29] - expect(cursor2.getBufferPosition()).toEqual [0,59] - - describe "when there is a single selection", -> - it "deletes the selection, but not the character following it", -> - editSession.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editSession.delete() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - expect(editSession.getSelection().isEmpty()).toBeTruthy() - - describe "when there are multiple selections", -> - describe "when selections are on the same line", -> - it "removes all selected text", -> - editSession.setSelectedBufferRanges([[[0,4], [0,13]], [[0,16], [0,24]]]) - editSession.delete() - expect(editSession.lineForBufferRow(0)).toBe 'var = () {' - - describe ".deleteToEndOfWord()", -> - describe "when no text is selected", -> - it "deletes to the end of the word", -> - editSession.setCursorBufferPosition([1, 24]) - editSession.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editSession.getCursors() - - editSession.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe ' i (items.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - editSession.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' - expect(buffer.lineForRow(2)).toBe ' iitems.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - describe "when text is selected", -> - it "deletes only selected text", -> - editSession.setSelectedBufferRange([[1, 24], [1, 27]]) - editSession.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".indent()", -> - describe "when the selection is empty", -> - describe "when autoIndent is disabled", -> - describe "if 'softTabs' is true (the default)", -> - it "inserts 'tabLength' spaces into the buffer", -> - tabRegex = new RegExp("^[ ]{#{editSession.getTabLength()}}") - expect(buffer.lineForRow(0)).not.toMatch(tabRegex) - editSession.indent() - expect(buffer.lineForRow(0)).toMatch(tabRegex) - - describe "if 'softTabs' is false", -> - it "insert a \t into the buffer", -> - editSession.setSoftTabs(false) - expect(buffer.lineForRow(0)).not.toMatch(/^\t/) - editSession.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t/) - - describe "when autoIndent is enabled", -> - describe "when the cursor's column is less than the suggested level of indentation", -> - describe "when 'softTabs' is true (the default)", -> - it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentaion", -> - buffer.insert([5, 0], " \n") - editSession.setCursorBufferPosition [5, 0] - editSession.indent(autoIndent: true) - expect(buffer.lineForRow(5)).toMatch /^\s+$/ - expect(buffer.lineForRow(5).length).toBe 6 - expect(editSession.getCursorBufferPosition()).toEqual [5, 6] - - describe "when 'softTabs' is false", -> - it "moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentaion", -> - convertToHardTabs(buffer) - editSession.setSoftTabs(false) - buffer.insert([5, 0], "\t\n") - editSession.setCursorBufferPosition [5, 0] - editSession.indent(autoIndent: true) - expect(buffer.lineForRow(5)).toMatch /^\t\t\t$/ - expect(editSession.getCursorBufferPosition()).toEqual [5, 3] - - describe "when the line's indent level is greater than the suggested level of indentation", -> - describe "when 'softTabs' is true (the default)", -> - it "moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", -> - buffer.insert([7, 0], " \n") - editSession.setCursorBufferPosition [7, 2] - editSession.indent(autoIndent: true) - expect(buffer.lineForRow(7)).toMatch /^\s+$/ - expect(buffer.lineForRow(7).length).toBe 8 - expect(editSession.getCursorBufferPosition()).toEqual [7, 8] - - describe "when 'softTabs' is false", -> - it "moves the cursor to the end of the leading whitespace and inserts \t into the buffer", -> - convertToHardTabs(buffer) - editSession.setSoftTabs(false) - buffer.insert([7, 0], "\t\t\t\n") - editSession.setCursorBufferPosition [7, 1] - editSession.indent(autoIndent: true) - expect(buffer.lineForRow(7)).toMatch /^\t\t\t\t$/ - expect(editSession.getCursorBufferPosition()).toEqual [7, 4] - - describe "when the selection is not empty", -> - it "indents the selected lines", -> - editSession.setSelectedBufferRange([[0, 0], [10, 0]]) - selection = editSession.getSelection() - spyOn(selection, "indentSelectedRows") - editSession.indent() - expect(selection.indentSelectedRows).toHaveBeenCalled() - - describe "if editSession.softTabs is false", -> - it "inserts a tab character into the buffer", -> - editSession.setSoftTabs(false) - expect(buffer.lineForRow(0)).not.toMatch(/^\t/) - editSession.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t/) - expect(editSession.getCursorBufferPosition()).toEqual [0, 1] - expect(editSession.getCursorScreenPosition()).toEqual [0, editSession.getTabLength()] - - editSession.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t\t/) - expect(editSession.getCursorBufferPosition()).toEqual [0, 2] - expect(editSession.getCursorScreenPosition()).toEqual [0, editSession.getTabLength() * 2] - - describe "pasteboard operations", -> - beforeEach -> - editSession.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - - describe ".cutSelectedText()", -> - it "removes the selected text from the buffer and places it on the pasteboard", -> - editSession.cutSelectedText() - expect(buffer.lineForRow(0)).toBe "var = function () {" - expect(buffer.lineForRow(1)).toBe " var = function(items) {" - - expect(clipboard.readText()).toBe 'quicksort\nsort' - - describe ".cutToEndOfLine()", -> - describe "when soft wrap is on", -> - it "cuts up to the end of the line", -> - editSession.setSoftWrap(true) - editSession.setEditorWidthInChars(10) - editSession.setCursorScreenPosition([2, 2]) - editSession.cutToEndOfLine() - expect(editSession.lineForScreenRow(2).text).toBe '= () {' - - describe "when soft wrap is off", -> - describe "when nothing is selected", -> - it "cuts up to the end of the line", -> - editSession.setCursorBufferPosition([2, 20]) - editSession.addCursorAtBufferPosition([3, 20]) - editSession.cutToEndOfLine() - expect(buffer.lineForRow(2)).toBe ' if (items.length' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.pasteboard.read()[0]).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' - - describe "when text is selected", -> - it "only cuts the selected text, not to the end of the line", -> - editSession.setSelectedBufferRanges([[[2,20], [2, 30]], [[3, 20], [3, 20]]]) - - editSession.cutToEndOfLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.pasteboard.read()[0]).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' - - describe ".copySelectedText()", -> - it "copies selected text onto the clipboard", -> - editSession.copySelectedText() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - expect(clipboard.readText()).toBe 'quicksort\nsort' - - describe ".pasteText()", -> - it "pastes text into the buffer", -> - atom.pasteboard.write('first') - editSession.pasteText() - expect(editSession.buffer.lineForRow(0)).toBe "var first = function () {" - expect(buffer.lineForRow(1)).toBe " var first = function(items) {" - - describe ".indentSelectedRows()", -> - describe "when nothing is selected", -> - describe "when softTabs is enabled", -> - it "indents line and retains selection", -> - editSession.setSelectedBufferRange([[0,3], [0,3]]) - editSession.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - expect(editSession.getSelectedBufferRange()).toEqual [[0, 3 + editSession.getTabLength()], [0, 3 + editSession.getTabLength()]] - - describe "when softTabs is disabled", -> - it "indents line and retains selection", -> - convertToHardTabs(buffer) - editSession.setSoftTabs(false) - editSession.setSelectedBufferRange([[0,3], [0,3]]) - editSession.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - expect(editSession.getSelectedBufferRange()).toEqual [[0, 3 + 1], [0, 3 + 1]] - - describe "when one line is selected", -> - describe "when softTabs is enabled", -> - it "indents line and retains selection", -> - editSession.setSelectedBufferRange([[0,4], [0,14]]) - editSession.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "#{editSession.getTabText()}var quicksort = function () {" - expect(editSession.getSelectedBufferRange()).toEqual [[0, 4 + editSession.getTabLength()], [0, 14 + editSession.getTabLength()]] - - describe "when softTabs is disabled", -> - it "indents line and retains selection", -> - convertToHardTabs(buffer) - editSession.setSoftTabs(false) - editSession.setSelectedBufferRange([[0,4], [0,14]]) - editSession.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - expect(editSession.getSelectedBufferRange()).toEqual [[0, 4 + 1], [0, 14 + 1]] - - describe "when multiple lines are selected", -> - describe "when softTabs is enabled", -> - it "indents selected lines (that are not empty) and retains selection", -> - editSession.setSelectedBufferRange([[9,1], [11,15]]) - editSession.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe " };" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editSession.getSelectedBufferRange()).toEqual [[9, 1 + editSession.getTabLength()], [11, 15 + editSession.getTabLength()]] - - it "does not indent the last row if the selection ends at column 0", -> - editSession.setSelectedBufferRange([[9,1], [11,0]]) - editSession.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe " };" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editSession.getSelectedBufferRange()).toEqual [[9, 1 + editSession.getTabLength()], [11, 0]] - - describe "when softTabs is disabled", -> - it "indents selected lines (that are not empty) and retains selection", -> - convertToHardTabs(buffer) - editSession.setSoftTabs(false) - editSession.setSelectedBufferRange([[9,1], [11,15]]) - editSession.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe "\t\t};" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe "\t\treturn sort(Array.apply(this, arguments));" - expect(editSession.getSelectedBufferRange()).toEqual [[9, 1 + 1], [11, 15 + 1]] - - describe ".outdentSelectedRows()", -> - describe "when nothing is selected", -> - it "outdents line and retains selection", -> - editSession.setSelectedBufferRange([[1,3], [1,3]]) - editSession.outdentSelectedRows() - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editSession.getSelectedBufferRange()).toEqual [[1, 3 - editSession.getTabLength()], [1, 3 - editSession.getTabLength()]] - - it "outdents when indent is less than a tab length", -> - editSession.insertText(' ') - editSession.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents a single hard tab when indent is multiple hard tabs and and the session is using soft tabs", -> - editSession.insertText('\t\t') - editSession.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - editSession.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents when a mix of hard tabs and soft tabs are used", -> - editSession.insertText('\t ') - editSession.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - editSession.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - editSession.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - describe "when one line is selected", -> - it "outdents line and retains editSession", -> - editSession.setSelectedBufferRange([[1,4], [1,14]]) - editSession.outdentSelectedRows() - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editSession.getSelectedBufferRange()).toEqual [[1, 4 - editSession.getTabLength()], [1, 14 - editSession.getTabLength()]] - - describe "when multiple lines are selected", -> - it "outdents selected lines and retains editSession", -> - editSession.setSelectedBufferRange([[0,1], [3,15]]) - editSession.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [3, 15 - editSession.getTabLength()]] - - it "does not outdent the last line of the selection if it ends at column 0", -> - editSession.setSelectedBufferRange([[0,1], [3,0]]) - editSession.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [3, 0]] - - describe ".toggleLineCommentsInSelection()", -> - it "toggles comments on the selected lines", -> - editSession.setSelectedBufferRange([[4, 5], [7, 5]]) - editSession.toggleLineCommentsInSelection() - - expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " // current = items.shift();" - expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " // }" - expect(editSession.getSelectedBufferRange()).toEqual [[4, 8], [7, 8]] - - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " current = items.shift();" - expect(buffer.lineForRow(6)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " }" - - it "does not comment the last line of a non-empty selection if it ends at column 0", -> - editSession.setSelectedBufferRange([[4, 5], [7, 0]]) - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " // current = items.shift();" - expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " }" - - it "uncomments lines if all lines match the comment regex", -> - editSession.setSelectedBufferRange([[0, 0], [0, 1]]) - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {" - - editSession.setSelectedBufferRange([[0, 0], [2, Infinity]]) - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "// // var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe "// if (items.length <= 1) return items;" - - editSession.setSelectedBufferRange([[0, 0], [2, Infinity]]) - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - - editSession.setSelectedBufferRange([[0, 0], [0, Infinity]]) - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "uncomments commented lines separated by an empty line", -> - editSession.setSelectedBufferRange([[0, 0], [1, Infinity]]) - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {" - - buffer.insert([0, Infinity], '\n') - - editSession.setSelectedBufferRange([[0, 0], [2, Infinity]]) - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "" - expect(buffer.lineForRow(2)).toBe " var sort = function(items) {" - - it "preserves selection emptiness", -> - editSession.setCursorBufferPosition([4, 0]) - editSession.toggleLineCommentsInSelection() - expect(editSession.getSelection().isEmpty()).toBeTruthy() - - it "does not explode if the current language mode has no comment regex", -> - editSession.destroy() - editSession = project.openSync(null, autoIndent: false) - editSession.setSelectedBufferRange([[4, 5], [4, 5]]) - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" - - it "uncomments when the line lacks the trailing whitespace in the comment regex", -> - editSession.setCursorBufferPosition([10, 0]) - editSession.toggleLineCommentsInSelection() - - expect(buffer.lineForRow(10)).toBe "// " - expect(editSession.getSelectedBufferRange()).toEqual [[10, 3], [10, 3]] - editSession.backspace() - expect(buffer.lineForRow(10)).toBe "//" - - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(10)).toBe "" - expect(editSession.getSelectedBufferRange()).toEqual [[10, 0], [10, 0]] - - it "uncomments when the line has leading whitespace", -> - editSession.setCursorBufferPosition([10, 0]) - editSession.toggleLineCommentsInSelection() - - expect(buffer.lineForRow(10)).toBe "// " - editSession.moveCursorToBeginningOfLine() - editSession.insertText(" ") - editSession.setSelectedBufferRange([[10, 0], [10, 0]]) - editSession.toggleLineCommentsInSelection() - expect(buffer.lineForRow(10)).toBe " " - - describe ".undo() and .redo()", -> - it "undoes/redoes the last change", -> - editSession.insertText("foo") - editSession.undo() - expect(buffer.lineForRow(0)).not.toContain "foo" - - editSession.redo() - expect(buffer.lineForRow(0)).toContain "foo" - - it "batches the undo / redo of changes caused by multiple cursors", -> - editSession.setCursorScreenPosition([0, 0]) - editSession.addCursorAtScreenPosition([1, 0]) - - editSession.insertText("foo") - editSession.backspace() - - expect(buffer.lineForRow(0)).toContain "fovar" - expect(buffer.lineForRow(1)).toContain "fo " - - editSession.undo() - - expect(buffer.lineForRow(0)).toContain "foo" - expect(buffer.lineForRow(1)).toContain "foo" - - editSession.redo() - - expect(buffer.lineForRow(0)).not.toContain "foo" - expect(buffer.lineForRow(0)).toContain "fovar" - - it "restores the selected ranges after undo and redo", -> - editSession.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) - editSession.delete() - editSession.delete() - - selections = editSession.getSelections() - expect(buffer.lineForRow(1)).toBe ' var = function( {' - - expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 17], [1, 17]]] - - editSession.undo() - expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] - - editSession.undo() - expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 10]], [[1, 22], [1, 27]]] - - editSession.redo() - expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] - - xit "restores folds after undo and redo", -> - editSession.foldBufferRow(1) - editSession.setSelectedBufferRange([[1, 0], [10, Infinity]], preserveFolds: true) - expect(editSession.isFoldedAtBufferRow(1)).toBeTruthy() - - editSession.insertText """ - \ // testing - function foo() { - return 1 + 2; - } - """ - expect(editSession.isFoldedAtBufferRow(1)).toBeFalsy() - editSession.foldBufferRow(2) - - editSession.undo() - expect(editSession.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editSession.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editSession.isFoldedAtBufferRow(10)).toBeFalsy() - - editSession.redo() - expect(editSession.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editSession.isFoldedAtBufferRow(2)).toBeTruthy() - - describe "begin/commitTransaction()", -> - it "restores the selection when the transaction is undone/redone", -> - buffer.setText('1234') - editSession.setSelectedBufferRange([[0, 1], [0, 3]]) - editSession.beginTransaction() - - editSession.delete() - editSession.moveCursorToEndOfLine() - editSession.insertText('5') - expect(buffer.getText()).toBe '145' - - editSession.commitTransaction() - - editSession.undo() - expect(buffer.getText()).toBe '1234' - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] - - editSession.redo() - expect(buffer.getText()).toBe '145' - expect(editSession.getSelectedBufferRange()).toEqual [[0, 3], [0, 3]] - - describe "when the buffer is changed (via its direct api, rather than via than edit session)", -> - it "moves the cursor so it is in the same relative position of the buffer", -> - expect(editSession.getCursorScreenPosition()).toEqual [0, 0] - editSession.addCursorAtScreenPosition([0, 5]) - editSession.addCursorAtScreenPosition([1, 0]) - [cursor1, cursor2, cursor3] = editSession.getCursors() - - buffer.insert([0, 1], 'abc') - - expect(cursor1.getScreenPosition()).toEqual [0, 0] - expect(cursor2.getScreenPosition()).toEqual [0, 8] - expect(cursor3.getScreenPosition()).toEqual [1, 0] - - it "does not destroy cursors or selections when a change encompasses them", -> - cursor = editSession.getCursor() - cursor.setBufferPosition [3, 3] - editSession.buffer.delete([[3, 1], [3, 5]]) - expect(cursor.getBufferPosition()).toEqual [3, 1] - expect(editSession.getCursors().indexOf(cursor)).not.toBe -1 - - selection = editSession.getLastSelection() - selection.setBufferRange [[3, 5], [3, 10]] - editSession.buffer.delete [[3, 3], [3, 8]] - expect(selection.getBufferRange()).toEqual [[3, 3], [3, 5]] - expect(editSession.getSelections().indexOf(selection)).not.toBe -1 - - it "merges cursors when the change causes them to overlap", -> - editSession.setCursorScreenPosition([0, 0]) - editSession.addCursorAtScreenPosition([0, 2]) - editSession.addCursorAtScreenPosition([1, 2]) - - [cursor1, cursor2, cursor3] = editSession.getCursors() - expect(editSession.getCursors().length).toBe 3 - - buffer.delete([[0, 0], [0, 2]]) - - expect(editSession.getCursors().length).toBe 2 - expect(editSession.getCursors()).toEqual [cursor1, cursor3] - expect(cursor1.getBufferPosition()).toEqual [0,0] - expect(cursor3.getBufferPosition()).toEqual [1,2] - - describe ".deleteLine()", -> - it "deletes the first line when the cursor is there", -> - editSession.getCursor().moveToTop() - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line1) - editSession.deleteLine() - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "deletes the last line when the cursor is there", -> - count = buffer.getLineCount() - secondToLastLine = buffer.lineForRow(count - 2) - expect(buffer.lineForRow(count - 1)).not.toBe(secondToLastLine) - editSession.getCursor().moveToBottom() - editSession.deleteLine() - newCount = buffer.getLineCount() - expect(buffer.lineForRow(newCount - 1)).toBe(secondToLastLine) - expect(newCount).toBe(count - 1) - - it "deletes whole lines when partial lines are selected", -> - editSession.setSelectedBufferRange([[0, 2], [1, 2]]) - line2 = buffer.lineForRow(2) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line2) - expect(buffer.lineForRow(1)).not.toBe(line2) - editSession.deleteLine() - expect(buffer.lineForRow(0)).toBe(line2) - expect(buffer.getLineCount()).toBe(count - 2) - - it "only deletes first line if only newline is selected on second line", -> - editSession.setSelectedBufferRange([[0, 2], [1, 0]]) - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line1) - editSession.deleteLine() - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "deletes the entire region when invoke on a folded region", -> - editSession.foldBufferRow(1) - editSession.getCursor().moveToTop() - editSession.getCursor().moveDown() - expect(buffer.getLineCount()).toBe(13) - editSession.deleteLine() - expect(buffer.getLineCount()).toBe(4) - - it "deletes the entire file from the bottom up", -> - count = buffer.getLineCount() - expect(count).toBeGreaterThan(0) - for line in [0...count] - editSession.getCursor().moveToBottom() - editSession.deleteLine() - expect(buffer.getLineCount()).toBe(1) - expect(buffer.getText()).toBe('') - - it "deletes the entire file from the top down", -> - count = buffer.getLineCount() - expect(count).toBeGreaterThan(0) - for line in [0...count] - editSession.getCursor().moveToTop() - editSession.deleteLine() - expect(buffer.getLineCount()).toBe(1) - expect(buffer.getText()).toBe('') - - describe "when soft wrap is enabled", -> - it "deletes the entire line that the cursor is on", -> - editSession.setSoftWrap(true) - editSession.setEditorWidthInChars(10) - editSession.setCursorBufferPosition([6]) - - line7 = buffer.lineForRow(7) - count = buffer.getLineCount() - expect(buffer.lineForRow(6)).not.toBe(line7) - editSession.deleteLine() - expect(buffer.lineForRow(6)).toBe(line7) - expect(buffer.getLineCount()).toBe(count - 1) - - describe "when the line being deleted preceeds a fold, and the command is undone", -> - it "restores the line and preserves the fold", -> - editSession.setCursorBufferPosition([4]) - editSession.foldCurrentRow() - expect(editSession.isFoldedAtScreenRow(4)).toBeTruthy() - editSession.setCursorBufferPosition([3]) - editSession.deleteLine() - expect(editSession.isFoldedAtScreenRow(3)).toBeTruthy() - expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' - editSession.undo() - expect(editSession.isFoldedAtScreenRow(4)).toBeTruthy() - expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - - describe ".replaceSelectedText(options, fn)", -> - describe "when no text is selected", -> - it "inserts the text returned from the function at the cursor position", -> - editSession.replaceSelectedText {}, -> '123' - expect(buffer.lineForRow(0)).toBe '123var quicksort = function () {' - - editSession.replaceSelectedText {selectWordIfEmpty: true}, -> 'var' - editSession.setCursorBufferPosition([0]) - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - - editSession.setCursorBufferPosition([10]) - editSession.replaceSelectedText null, -> '' - expect(buffer.lineForRow(10)).toBe '' - - describe "when text is selected", -> - it "replaces the selected text with the text returned from the function", -> - editSession.setSelectedBufferRange([[0, 1], [0, 3]]) - editSession.replaceSelectedText {}, -> 'ia' - expect(buffer.lineForRow(0)).toBe 'via quicksort = function () {' - - describe ".transpose()", -> - it "swaps two characters", -> - editSession.buffer.setText("abc") - editSession.setCursorScreenPosition([0, 1]) - editSession.transpose() - expect(editSession.lineForBufferRow(0)).toBe 'bac' - - it "reverses a selection", -> - editSession.buffer.setText("xabcz") - editSession.setSelectedBufferRange([[0, 1], [0, 4]]) - editSession.transpose() - expect(editSession.lineForBufferRow(0)).toBe 'xcbaz' - - describe ".upperCase()", -> - describe "when there is no selection", -> - it "upper cases the current word", -> - editSession.buffer.setText("aBc") - editSession.setCursorScreenPosition([0, 1]) - editSession.upperCase() - expect(editSession.lineForBufferRow(0)).toBe 'ABC' - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [0, 1]] - - describe "when there is a selection", -> - it "upper cases the current selection", -> - editSession.buffer.setText("abc") - editSession.setSelectedBufferRange([[0,0], [0,2]]) - editSession.upperCase() - expect(editSession.lineForBufferRow(0)).toBe 'ABc' - expect(editSession.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - - describe ".lowerCase()", -> - describe "when there is no selection", -> - it "lower cases the current word", -> - editSession.buffer.setText("aBC") - editSession.setCursorScreenPosition([0, 1]) - editSession.lowerCase() - expect(editSession.lineForBufferRow(0)).toBe 'abc' - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [0, 1]] - - describe "when there is a selection", -> - it "lower cases the current selection", -> - editSession.buffer.setText("ABC") - editSession.setSelectedBufferRange([[0,0], [0,2]]) - editSession.lowerCase() - expect(editSession.lineForBufferRow(0)).toBe 'abC' - expect(editSession.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - - describe "soft-tabs detection", -> - it "assigns soft / hard tabs based on the contents of the buffer, or uses the default if unknown", -> - editSession = project.openSync('sample.js', softTabs: false) - expect(editSession.getSoftTabs()).toBeTruthy() - - editSession = project.openSync('sample-with-tabs.coffee', softTabs: true) - expect(editSession.getSoftTabs()).toBeFalsy() - - editSession = project.openSync(null, softTabs: false) - expect(editSession.getSoftTabs()).toBeFalsy() - - describe ".indentLevelForLine(line)", -> - it "returns the indent level when the line has only leading whitespace", -> - expect(editSession.indentLevelForLine(" hello")).toBe(2) - expect(editSession.indentLevelForLine(" hello")).toBe(1.5) - - it "returns the indent level when the line has only leading tabs", -> - expect(editSession.indentLevelForLine("\t\thello")).toBe(2) - - it "returns the indent level when the line has mixed leading whitespace and tabs", -> - expect(editSession.indentLevelForLine("\t hello")).toBe(2) - expect(editSession.indentLevelForLine(" \thello")).toBe(2) - expect(editSession.indentLevelForLine(" \t hello")).toBe(2.5) - expect(editSession.indentLevelForLine(" \t \thello")).toBe(3.5) - - describe "when the buffer is reloaded", -> - it "preserves the current cursor position", -> - editSession.setCursorScreenPosition([0, 1]) - editSession.buffer.reload() - expect(editSession.getCursorScreenPosition()).toEqual [0,1] - - describe "when a better-matched grammar is added to syntax", -> - it "switches to the better-matched grammar and re-tokenizes the buffer", -> - editSession.destroy() - jsGrammar = atom.syntax.selectGrammar('a.js') - atom.syntax.removeGrammar(jsGrammar) - - editSession = project.openSync('sample.js', autoIndent: false) - expect(editSession.getGrammar()).toBe atom.syntax.nullGrammar - expect(editSession.lineForScreenRow(0).tokens.length).toBe 1 - - atom.syntax.addGrammar(jsGrammar) - expect(editSession.getGrammar()).toBe jsGrammar - expect(editSession.lineForScreenRow(0).tokens.length).toBeGreaterThan 1 - - describe "auto-indent", -> - copyText = (text, {startColumn}={}) -> - startColumn ?= 0 - editSession.setCursorBufferPosition([0, 0]) - editSession.insertText(text) - numberOfNewlines = text.match(/\n/g)?.length - endColumn = text.match(/[^\n]*$/)[0]?.length - editSession.getSelection().setBufferRange([[0,startColumn], [numberOfNewlines,endColumn]]) - editSession.cutSelectedText() - - describe "editor.autoIndent", -> - describe "when editor.autoIndent is false (default)", -> - describe "when `indent` is triggered", -> - it "does not auto-indent the line", -> - editSession.setCursorBufferPosition([1, 30]) - editSession.insertText("\n ") - expect(editSession.lineForBufferRow(2)).toBe " " - - atom.config.set("editor.autoIndent", false) - editSession.indent() - expect(editSession.lineForBufferRow(2)).toBe " " - - describe "when editor.autoIndent is true", -> - beforeEach -> - atom.config.set("editor.autoIndent", true) - - describe "when `indent` is triggered", -> - it "auto-indents the line", -> - editSession.setCursorBufferPosition([1, 30]) - editSession.insertText("\n ") - expect(editSession.lineForBufferRow(2)).toBe " " - - atom.config.set("editor.autoIndent", true) - editSession.indent() - expect(editSession.lineForBufferRow(2)).toBe " " - - describe "when a newline is added", -> - describe "when the line preceding the newline adds a new level of indentation", -> - it "indents the newline to one additional level of indentation beyond the preceding line", -> - editSession.setCursorBufferPosition([1, Infinity]) - editSession.insertText('\n') - expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1 - - describe "when the line preceding the newline does't add a level of indentation", -> - it "indents the new line to the same level a as the preceding line", -> - editSession.setCursorBufferPosition([5, 13]) - editSession.insertText('\n') - expect(editSession.indentationForBufferRow(6)).toBe editSession.indentationForBufferRow(5) - - describe "when the line preceding the newline is a comment", -> - it "maintains the indent of the commented line", -> - editSession.setCursorBufferPosition([0, 0]) - editSession.insertText(' //') - editSession.setCursorBufferPosition([0, Infinity]) - editSession.insertText('\n') - expect(editSession.indentationForBufferRow(1)).toBe 2 - - it "does not indent the line preceding the newline", -> - editSession.setCursorBufferPosition([2, 0]) - editSession.insertText(' var this-line-should-be-indented-more\n') - expect(editSession.indentationForBufferRow(1)).toBe 1 - - atom.config.set("editor.autoIndent", true) - editSession.setCursorBufferPosition([2, Infinity]) - editSession.insertText('\n') - expect(editSession.indentationForBufferRow(1)).toBe 1 - expect(editSession.indentationForBufferRow(2)).toBe 1 - - describe "when inserted text matches a decrease indent pattern", -> - describe "when the preceding line matches an increase indent pattern", -> - it "decreases the indentation to match that of the preceding line", -> - editSession.setCursorBufferPosition([1, Infinity]) - editSession.insertText('\n') - expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1 - editSession.insertText('}') - expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) - - describe "when the preceding line doesn't match an increase indent pattern", -> - it "decreases the indentation to be one level below that of the preceding line", -> - editSession.setCursorBufferPosition([3, Infinity]) - editSession.insertText('\n ') - expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - editSession.insertText('}') - expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - 1 - - it "doesn't break when decreasing the indentation on a row that has no indentation", -> - editSession.setCursorBufferPosition([12, Infinity]) - editSession.insertText("\n}; # too many closing brackets!") - expect(editSession.lineForBufferRow(13)).toBe "}; # too many closing brackets!" - - describe "when inserted text does not match a decrease indent pattern", -> - it "does not the indentation", -> - editSession.setCursorBufferPosition([12, 0]) - editSession.insertText(' ') - expect(editSession.lineForBufferRow(12)).toBe ' };' - editSession.insertText('\t\t') - expect(editSession.lineForBufferRow(12)).toBe ' \t\t};' - - describe "when the current line does not match a decrease indent pattern", -> - it "leaves the line unchanged", -> - editSession.setCursorBufferPosition([2, 4]) - expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1 - editSession.insertText('foo') - expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1 - - describe "editor.normalizeIndentOnPaste", -> - beforeEach -> - atom.config.set('editor.normalizeIndentOnPaste', true) - - it "does not normalize the indentation level of the text when editor.normalizeIndentOnPaste is false", -> - copyText(" function() {\nvar cool = 1;\n }\n") - atom.config.set('editor.normalizeIndentOnPaste', false) - editSession.setCursorBufferPosition([5, 2]) - editSession.pasteText() - expect(editSession.lineForBufferRow(5)).toBe " function() {" - expect(editSession.lineForBufferRow(6)).toBe "var cool = 1;" - expect(editSession.lineForBufferRow(7)).toBe " }" - - describe "when the inserted text contains no newlines", -> - it "does not adjust the indentation level of the text", -> - editSession.setCursorBufferPosition([5, 2]) - editSession.insertText("foo", indentBasis: 5) - expect(editSession.lineForBufferRow(5)).toBe " foo current = items.shift();" - - it "does not adjust the whitespace if there are preceding characters", -> - copyText(" foo") - editSession.setCursorBufferPosition([5, 30]) - editSession.pasteText() - - expect(editSession.lineForBufferRow(5)).toBe " current = items.shift(); foo" - - describe "when the inserted text contains newlines", -> - describe "when the cursor is preceded only by whitespace characters", -> - it "normalizes indented lines to the cursor's current indentation level", -> - copyText(" while (true) {\n foo();\n }\n", {startColumn: 2}) - editSession.setCursorBufferPosition([3, 4]) - editSession.pasteText() - - expect(editSession.lineForBufferRow(3)).toBe " while (true) {" - expect(editSession.lineForBufferRow(4)).toBe " foo();" - expect(editSession.lineForBufferRow(5)).toBe " }" - expect(editSession.lineForBufferRow(6)).toBe "var pivot = items.shift(), current, left = [], right = [];" - - describe "when the cursor is preceded by non-whitespace characters", -> - it "normalizes the indentation level of all lines based on the level of the existing first line", -> - copyText(" while (true) {\n foo();\n }\n", {startColumn: 0}) - editSession.setCursorBufferPosition([1, Infinity]) - editSession.pasteText() - - expect(editSession.lineForBufferRow(1)).toBe " var sort = function(items) {while (true) {" - expect(editSession.lineForBufferRow(2)).toBe " foo();" - expect(editSession.lineForBufferRow(3)).toBe " }" - expect(editSession.lineForBufferRow(4)).toBe "" - - it "autoIndentSelectedRows auto-indents the selection", -> - editSession.setCursorBufferPosition([2, 0]) - editSession.insertText("function() {\ninside=true\n}\n i=1\n") - editSession.getSelection().setBufferRange([[2,0], [6,0]]) - editSession.autoIndentSelectedRows() - - expect(editSession.lineForBufferRow(2)).toBe " function() {" - expect(editSession.lineForBufferRow(3)).toBe " inside=true" - expect(editSession.lineForBufferRow(4)).toBe " }" - expect(editSession.lineForBufferRow(5)).toBe " i=1" - - describe ".destroy()", -> - it "destroys all markers associated with the edit session", -> - expect(buffer.getMarkerCount()).toBeGreaterThan 0 - editSession.destroy() - expect(buffer.getMarkerCount()).toBe 0 - - describe ".joinLine()", -> - describe "when no text is selected", -> - describe "when the line below isn't empty", -> - it "joins the line below with the current line separated by a space and moves the cursor to the start of line that was moved up", -> - editSession.joinLine() - expect(editSession.lineForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' - expect(editSession.getCursorBufferPosition()).toEqual [0, 30] - - describe "when the line below is empty", -> - it "deletes the line below and moves the cursor to the end of the line", -> - editSession.setCursorBufferPosition([9]) - editSession.joinLine() - expect(editSession.lineForBufferRow(9)).toBe ' };' - expect(editSession.lineForBufferRow(10)).toBe ' return sort(Array.apply(this, arguments));' - expect(editSession.getCursorBufferPosition()).toEqual [9, 4] - - describe "when the cursor is on the last row", -> - it "does nothing", -> - editSession.setCursorBufferPosition([Infinity, Infinity]) - editSession.joinLine() - expect(editSession.lineForBufferRow(12)).toBe '};' - - describe "when text is selected", -> - describe "when the selection does not span multiple lines", -> - it "joins the line below with the current line separated by a space and retains the selected text", -> - editSession.setSelectedBufferRange([[0, 1], [0, 3]]) - editSession.joinLine() - expect(editSession.lineForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] - - describe "when the selection spans multiple lines", -> - it "joins all selected lines separated by a space and retains the selected text", -> - editSession.setSelectedBufferRange([[9, 3], [12, 1]]) - editSession.joinLine() - expect(editSession.lineForBufferRow(9)).toBe ' }; return sort(Array.apply(this, arguments)); };' - expect(editSession.getSelectedBufferRange()).toEqual [[9, 3], [9, 49]] - - describe ".shouldPromptToSave()", -> - it "returns false when an edit session's buffer is in use by more than one session", -> - expect(editSession.shouldPromptToSave()).toBeFalsy() - buffer.setText('changed') - expect(editSession.shouldPromptToSave()).toBeTruthy() - editSession2 = project.openSync('sample.js', autoIndent: false) - expect(editSession.shouldPromptToSave()).toBeFalsy() - editSession2.destroy() - expect(editSession.shouldPromptToSave()).toBeTruthy() - - describe "when the edit session contains surrogate pair characters", -> - it "correctly backspaces over them", -> - editSession.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') - editSession.moveCursorToBottom() - editSession.backspace() - expect(editSession.getText()).toBe '\uD835\uDF97\uD835\uDF97' - editSession.backspace() - expect(editSession.getText()).toBe '\uD835\uDF97' - editSession.backspace() - expect(editSession.getText()).toBe '' - - it "correctly deletes over them", -> - editSession.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') - editSession.moveCursorToTop() - editSession.delete() - expect(editSession.getText()).toBe '\uD835\uDF97\uD835\uDF97' - editSession.delete() - expect(editSession.getText()).toBe '\uD835\uDF97' - editSession.delete() - expect(editSession.getText()).toBe '' - - it "correctly moves over them", -> - editSession.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97\n') - editSession.moveCursorToTop() - editSession.moveCursorRight() - expect(editSession.getCursorBufferPosition()).toEqual [0, 2] - editSession.moveCursorRight() - expect(editSession.getCursorBufferPosition()).toEqual [0, 4] - editSession.moveCursorRight() - expect(editSession.getCursorBufferPosition()).toEqual [0, 6] - editSession.moveCursorRight() - expect(editSession.getCursorBufferPosition()).toEqual [1, 0] - editSession.moveCursorLeft() - expect(editSession.getCursorBufferPosition()).toEqual [0, 6] - editSession.moveCursorLeft() - expect(editSession.getCursorBufferPosition()).toEqual [0, 4] - editSession.moveCursorLeft() - expect(editSession.getCursorBufferPosition()).toEqual [0, 2] - editSession.moveCursorLeft() - expect(editSession.getCursorBufferPosition()).toEqual [0, 0] diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index e2c8f5817..07a9eff91 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1,2821 +1,2661 @@ -{_, $, $$, fs, Editor, Range, RootView} = require 'atom' -path = require 'path' -temp = require 'temp' +clipboard = require 'clipboard' describe "Editor", -> - [buffer, editor, editSession, cachedLineHeight, cachedCharWidth] = [] + [buffer, editor, lineLengths] = [] - beforeEach -> - atom.activatePackage('language-text', sync: true) - atom.activatePackage('language-javascript', sync: true) - editSession = project.openSync('sample.js') - buffer = editSession.buffer - editor = new Editor(editSession) - editor.lineOverdraw = 2 - editor.isFocused = true - editor.enableKeymap() - editor.calculateHeightInLines = -> - Math.ceil(@height() / @lineHeight) - editor.attachToDom = ({ heightInLines, widthInChars } = {}) -> - heightInLines ?= @getBuffer().getLineCount() - @height(getLineHeight() * heightInLines) - @width(getCharWidth() * widthInChars) if widthInChars - $('#jasmine-content').append(this) - - getLineHeight = -> - return cachedLineHeight if cachedLineHeight? - calcDimensions() - cachedLineHeight - - getCharWidth = -> - return cachedCharWidth if cachedCharWidth? - calcDimensions() - cachedCharWidth - - calcDimensions = -> - editorForMeasurement = new Editor(editSession: project.openSync('sample.js')) - editorForMeasurement.attachToDom() - cachedLineHeight = editorForMeasurement.lineHeight - cachedCharWidth = editorForMeasurement.charWidth - editorForMeasurement.remove() - - describe "construction", -> - it "throws an error if no edit session is given", -> - expect(-> new Editor).toThrow() - - describe "when the editor is attached to the dom", -> - it "calculates line height and char width and updates the pixel position of the cursor", -> - expect(editor.lineHeight).toBeNull() - expect(editor.charWidth).toBeNull() - editor.setCursorScreenPosition(row: 2, column: 2) - - editor.attachToDom() - - expect(editor.lineHeight).not.toBeNull() - expect(editor.charWidth).not.toBeNull() - expect(editor.find('.cursor').offset()).toEqual pagePixelPositionForPoint(editor, [2, 2]) - - it "is focused", -> - editor.attachToDom() - expect(editor).toMatchSelector ":has(:focus)" - - describe "when the editor receives focus", -> - it "focuses the hidden input", -> - editor.attachToDom() - editor.focus() - expect(editor).not.toMatchSelector ':focus' - expect(editor.hiddenInput).toMatchSelector ':focus' - - it "does not scroll the editor (regression)", -> - editor.attachToDom(heightInLines: 2) - editor.selectAll() - editor.hiddenInput.blur() - editor.focus() - - expect(editor.hiddenInput).toMatchSelector ':focus' - expect($(editor[0]).scrollTop()).toBe 0 - expect($(editor.scrollView[0]).scrollTop()).toBe 0 - - editor.moveCursorToBottom() - editor.hiddenInput.blur() - editor.scrollTop(0) - editor.focus() - - expect(editor.hiddenInput).toMatchSelector ':focus' - expect($(editor[0]).scrollTop()).toBe 0 - expect($(editor.scrollView[0]).scrollTop()).toBe 0 - - describe "when the hidden input is focused / unfocused", -> - it "assigns the isFocused flag on the editor and also adds/removes the .focused css class", -> - editor.attachToDom() - editor.isFocused = false - editor.hiddenInput.focus() - expect(editor.isFocused).toBeTruthy() - - editor.hiddenInput.focusout() - expect(editor.isFocused).toBeFalsy() - - describe "when the activeEditSession's file is modified on disk", -> - it "triggers an alert", -> - filePath = path.join(temp.dir, 'atom-changed-file.txt') - fs.writeFileSync(filePath, "") - editSession = project.openSync(filePath) - editor.edit(editSession) - editor.insertText("now the buffer is modified") - - fileChangeHandler = jasmine.createSpy('fileChange') - editSession.buffer.file.on 'contents-changed', fileChangeHandler - - spyOn(atom, "confirm") - - fs.writeFileSync(filePath, "a file change") - - waitsFor "file to trigger contents-changed event", -> - fileChangeHandler.callCount > 0 - - runs -> - expect(atom.confirm).toHaveBeenCalled() - - describe ".remove()", -> - it "destroys the edit session", -> - editor.remove() - expect(editor.activeEditSession.destroyed).toBeTruthy() - - describe ".edit(editSession)", -> - [newEditSession, newBuffer] = [] + convertToHardTabs = (buffer) -> + buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t")) + describe "with an initial line option", -> beforeEach -> - newEditSession = project.openSync('two-hundred.txt') - newBuffer = newEditSession.buffer + editor = project.openSync('sample.js', initialLine: 2) + buffer = editor.buffer - it "updates the rendered lines, cursors, selections, scroll position, and event subscriptions to match the given edit session", -> - editor.attachToDom(heightInLines: 5, widthInChars: 30) - editor.setCursorBufferPosition([6, 13]) - editor.scrollToBottom() - editor.scrollLeft(150) - previousScrollHeight = editor.verticalScrollbar.prop('scrollHeight') - previousScrollTop = editor.scrollTop() - previousScrollLeft = editor.scrollLeft() + it "opens the file and positions the cursor on line 2", -> + expect(editor.getCursor().getBufferPosition().row).toEqual 2 - newEditSession.setScrollTop(900) - newEditSession.setSelectedBufferRange([[40, 0], [43, 1]]) - - editor.edit(newEditSession) - { firstRenderedScreenRow, lastRenderedScreenRow } = editor - expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe newBuffer.lineForRow(firstRenderedScreenRow) - expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe newBuffer.lineForRow(editor.lastRenderedScreenRow) - expect(editor.scrollTop()).toBe 900 - expect(editor.scrollLeft()).toBe 0 - expect(editor.getSelectionView().regions[0].position().top).toBe 40 * editor.lineHeight - editor.insertText("hello") - expect(editor.lineElementForScreenRow(40).text()).toBe "hello3" - - editor.edit(editSession) - { firstRenderedScreenRow, lastRenderedScreenRow } = editor - expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe buffer.lineForRow(firstRenderedScreenRow) - expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe buffer.lineForRow(editor.lastRenderedScreenRow) - expect(editor.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight - expect(editor.scrollTop()).toBe previousScrollTop - expect(editor.scrollLeft()).toBe previousScrollLeft - expect(editor.getCursorView().position()).toEqual { top: 6 * editor.lineHeight, left: 13 * editor.charWidth } - editor.insertText("goodbye") - expect(editor.lineElementForScreenRow(6).text()).toMatch /^ currentgoodbye/ - - it "triggers alert if edit session's buffer goes into conflict with changes on disk", -> - filePath = path.join(temp.dir, 'atom-changed-file.txt') - fs.writeFileSync(filePath, "") - tempEditSession = project.openSync(filePath) - editor.edit(tempEditSession) - tempEditSession.insertText("a buffer change") - - spyOn(atom, "confirm") - - contentsConflictedHandler = jasmine.createSpy("contentsConflictedHandler") - tempEditSession.on 'contents-conflicted', contentsConflictedHandler - fs.writeFileSync(filePath, "a file change") - waitsFor -> - contentsConflictedHandler.callCount > 0 - - runs -> - expect(atom.confirm).toHaveBeenCalled() - - describe ".scrollTop(n)", -> + describe "with default options", -> beforeEach -> - editor.attachToDom(heightInLines: 5) - expect(editor.verticalScrollbar.scrollTop()).toBe 0 - - describe "when called with a scroll top argument", -> - it "sets the scrollTop of the vertical scrollbar and sets scrollTop on the line numbers and lines", -> - editor.scrollTop(100) - expect(editor.verticalScrollbar.scrollTop()).toBe 100 - expect(editor.scrollView.scrollTop()).toBe 0 - expect(editor.renderedLines.css('top')).toBe "-100px" - expect(editor.gutter.lineNumbers.css('top')).toBe "-100px" - - editor.scrollTop(120) - expect(editor.verticalScrollbar.scrollTop()).toBe 120 - expect(editor.scrollView.scrollTop()).toBe 0 - expect(editor.renderedLines.css('top')).toBe "-120px" - expect(editor.gutter.lineNumbers.css('top')).toBe "-120px" - - 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.renderedLines, 'css') - spyOn(editor.gutter.lineNumbers, 'css') - - editor.scrollTop(100) - expect(editor.verticalScrollbar.scrollTop).not.toHaveBeenCalled() - expect(editor.renderedLines.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.renderedLines.css('top')).toBe "-100px" - expect(editor.gutter.lineNumbers.css('top')).toBe "-100px" - - 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 - - it "sets the new scroll top position on the active edit session", -> - expect(editor.activeEditSession.getScrollTop()).toBe 0 - editor.scrollTop(123) - expect(editor.activeEditSession.getScrollTop()).toBe 123 - - describe ".scrollHorizontally(pixelPosition)", -> - it "sets the new scroll left position on the active edit session", -> - editor.attachToDom(heightInLines: 5) - setEditorWidthInChars(editor, 5) - expect(editor.activeEditSession.getScrollLeft()).toBe 0 - editor.scrollHorizontally(left: 50) - expect(editor.activeEditSession.getScrollLeft()).toBeGreaterThan 0 - expect(editor.activeEditSession.getScrollLeft()).toBe editor.scrollLeft() - - describe "editor:attached event", -> - it 'only triggers an editor:attached event when it is first added to the DOM', -> - openHandler = jasmine.createSpy('openHandler') - editor.on 'editor:attached', openHandler - - editor.attachToDom() - expect(openHandler).toHaveBeenCalled() - [event, eventEditor] = openHandler.argsForCall[0] - expect(eventEditor).toBe editor - - openHandler.reset() - editor.attachToDom() - expect(openHandler).not.toHaveBeenCalled() - - describe "editor:path-changed event", -> - filePath = null - - beforeEach -> - filePath = path.join(temp.dir, 'something.txt') - fs.writeFileSync(filePath, filePath) - - afterEach -> - fs.removeSync(filePath) if fs.existsSync(filePath) - - it "emits event when buffer's path is changed", -> - eventHandler = jasmine.createSpy('eventHandler') - editor.on 'editor:path-changed', eventHandler - editor.getBuffer().saveAs(filePath) - expect(eventHandler).toHaveBeenCalled() - - it "emits event when editor receives a new buffer", -> - eventHandler = jasmine.createSpy('eventHandler') - editor.on 'editor:path-changed', eventHandler - editor.edit(project.openSync(filePath)) - expect(eventHandler).toHaveBeenCalled() - - it "stops listening to events on previously set buffers", -> - eventHandler = jasmine.createSpy('eventHandler') - oldBuffer = editor.getBuffer() - editor.on 'editor:path-changed', eventHandler - - editor.edit(project.openSync(filePath)) - expect(eventHandler).toHaveBeenCalled() - - eventHandler.reset() - oldBuffer.saveAs(path.join(temp.dir, 'atom-bad.txt')) - expect(eventHandler).not.toHaveBeenCalled() - - eventHandler.reset() - editor.getBuffer().saveAs(path.join(temp.dir, 'atom-new.txt')) - expect(eventHandler).toHaveBeenCalled() - - it "loads the grammar for the new path", -> - expect(editor.getGrammar().name).toBe 'JavaScript' - editor.getBuffer().saveAs(filePath) - expect(editor.getGrammar().name).toBe 'Plain Text' - - describe "font family", -> - beforeEach -> - expect(editor.css('font-family')).toBe 'Courier' - - it "when there is no config in fontFamily don't set it", -> - atom.config.set('editor.fontFamily', null) - expect(editor.css('font-family')).toBe '' - - describe "when the font family changes", -> - [fontFamily] = [] - - beforeEach -> - if process.platform is 'darwin' - fontFamily = "PCMyungjo" - else - fontFamily = "Consolas" - - it "updates the font family of editors and recalculates dimensions critical to cursor positioning", -> - editor.attachToDom(12) - lineHeightBefore = editor.lineHeight - charWidthBefore = editor.charWidth - editor.setCursorScreenPosition [5, 6] - - atom.config.set("editor.fontFamily", fontFamily) - expect(editor.css('font-family')).toBe fontFamily - expect(editor.charWidth).not.toBe charWidthBefore - expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth } - - newEditor = new Editor(editor.activeEditSession.copy()) - newEditor.attachToDom() - expect(newEditor.css('font-family')).toBe fontFamily - - describe "font size", -> - beforeEach -> - expect(editor.css('font-size')).not.toBe "20px" - expect(editor.css('font-size')).not.toBe "10px" - - it "sets the initial font size based on the value from config", -> - expect(editor.css('font-size')).toBe "#{atom.config.get('editor.fontSize')}px" - - describe "when the font size changes", -> - it "updates the font sizes of editors and recalculates dimensions critical to cursor positioning", -> - atom.config.set("editor.fontSize", 10) - editor.attachToDom() - lineHeightBefore = editor.lineHeight - charWidthBefore = editor.charWidth - editor.setCursorScreenPosition [5, 6] - - atom.config.set("editor.fontSize", 30) - expect(editor.css('font-size')).toBe '30px' - expect(editor.lineHeight).toBeGreaterThan lineHeightBefore - expect(editor.charWidth).toBeGreaterThan charWidthBefore - expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth } - expect(editor.renderedLines.outerHeight()).toBe buffer.getLineCount() * editor.lineHeight - expect(editor.verticalScrollbarContent.height()).toBe buffer.getLineCount() * editor.lineHeight - - newEditor = new Editor(editor.activeEditSession.copy()) - editor.remove() - newEditor.attachToDom() - expect(newEditor.css('font-size')).toBe '30px' - - it "updates the position and size of selection regions", -> - atom.config.set("editor.fontSize", 10) - editor.setSelectedBufferRange([[5, 2], [5, 7]]) - editor.attachToDom() - - atom.config.set("editor.fontSize", 30) - selectionRegion = editor.find('.region') - expect(selectionRegion.position().top).toBe 5 * editor.lineHeight - expect(selectionRegion.position().left).toBe 2 * editor.charWidth - expect(selectionRegion.height()).toBe editor.lineHeight - expect(selectionRegion.width()).toBe 5 * editor.charWidth - - it "updates lines if there are unrendered lines", -> - editor.attachToDom(heightInLines: 5) - originalLineCount = editor.renderedLines.find(".line").length - expect(originalLineCount).toBeGreaterThan 0 - - atom.config.set("editor.fontSize", 10) - expect(editor.renderedLines.find(".line").length).toBeGreaterThan originalLineCount - - describe "when the font size changes while editor is detached", -> - it "redraws the editor according to the new font size when it is reattached", -> - editor.setCursorScreenPosition([4, 2]) - editor.attachToDom() - initialLineHeight = editor.lineHeight - initialCharWidth = editor.charWidth - initialCursorPosition = editor.getCursorView().position() - initialScrollbarHeight = editor.verticalScrollbarContent.height() - editor.detach() - - atom.config.set("editor.fontSize", 10) - expect(editor.lineHeight).toBe initialLineHeight - expect(editor.charWidth).toBe initialCharWidth - - editor.attachToDom() - expect(editor.lineHeight).not.toBe initialLineHeight - expect(editor.charWidth).not.toBe initialCharWidth - expect(editor.getCursorView().position()).not.toEqual initialCursorPosition - expect(editor.verticalScrollbarContent.height()).not.toBe initialScrollbarHeight - - describe "mouse events", -> - beforeEach -> - editor.attachToDom() - editor.css(position: 'absolute', top: 10, left: 10, width: 400) - - describe "single-click", -> - it "re-positions the cursor to the clicked row / column", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 10]) - expect(editor.getCursorScreenPosition()).toEqual(row: 3, column: 10) - - describe "when the lines are scrolled to the right", -> - it "re-positions the cursor on the clicked location", -> - setEditorWidthInChars(editor, 30) - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 30]) # scrolls lines to the right - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 50]) - expect(editor.getCursorBufferPosition()).toEqual(row: 3, column: 50) - - describe "when the editor is using a variable-width font", -> - beforeEach -> - editor.setFontFamily('sans-serif') - - it "positions the cursor to the clicked row and column", -> - {top, left} = editor.pixelOffsetForScreenPosition([3, 30]) - editor.renderedLines.trigger mousedownEvent(pageX: left, pageY: top) - expect(editor.getCursorScreenPosition()).toEqual [3, 30] - - describe "double-click", -> - it "selects the word under the cursor, and expands the selection wordwise in either direction on a subsequent shift-click", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [8, 24], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [8, 24], originalEvent: {detail: 2}) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "concat" - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [8, 7], shiftKey: true) - editor.renderedLines.trigger 'mouseup' - - expect(editor.getSelectedText()).toBe "return sort(left).concat" - - it "stops selecting by word when the selection is emptied", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 8], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 8], originalEvent: {detail: 2}) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "quicksort" - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 10]) - editor.renderedLines.trigger 'mouseup' - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 12], originalEvent: {detail: 1}, shiftKey: true) - expect(editor.getSelectedBufferRange()).toEqual [[3, 10], [3, 12]] - - describe "when clicking between a word and a non-word", -> - it "selects the word", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 21], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 21], originalEvent: {detail: 2}) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "function" - - editor.setCursorBufferPosition([0, 0]) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 22], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 22], originalEvent: {detail: 2}) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "items" - - editor.setCursorBufferPosition([0, 0]) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 28], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 28], originalEvent: {detail: 2}) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "{" - - describe "triple/quardruple/etc-click", -> - it "selects the line under the cursor", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - - # Triple click - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 2}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 3}) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe " var sort = function(items) {\n" - - # Quad click - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 2}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 3}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 4}) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe " if (items.length <= 1) return items;\n" - - it "expands the selection linewise in either direction on a subsequent shift-click, but stops selecting linewise once the selection is emptied", -> - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 8], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 8], originalEvent: {detail: 2}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 8], originalEvent: {detail: 3}) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [5, 0]] - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 1}, shiftKey: true) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [5, 0]] - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 8], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelection().isEmpty()).toBeTruthy() - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 8], originalEvent: {detail: 1}, shiftKey: true) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[2, 8], [3, 8]] - - describe "shift-click", -> - it "selects from the cursor's current location to the clicked location", -> - editor.setCursorScreenPosition([4, 7]) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true) - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - describe "shift-double-click", -> - it "expands the selection on the first click and ignores the second click", -> - editor.setCursorScreenPosition([4, 7]) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 }) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 }) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - describe "shift-triple-click", -> - it "expands the selection on the first click and ignores the second click", -> - editor.setCursorScreenPosition([4, 7]) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 }) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 }) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 3 }) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - describe "meta-click", -> - it "places an additional cursor", -> - editor.attachToDom() - setEditorHeightInLines(editor, 5) - editor.setCursorBufferPosition([3, 0]) - editor.scrollTop(editor.lineHeight * 6) - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [6, 0], metaKey: true) - expect(editor.scrollTop()).toBe editor.lineHeight * (6 - editor.vScrollMargin) - - [cursor1, cursor2] = editor.getCursorViews() - expect(cursor1.position()).toEqual(top: 3 * editor.lineHeight, left: 0) - expect(cursor1.getBufferPosition()).toEqual [3, 0] - expect(cursor2.position()).toEqual(top: 6 * editor.lineHeight, left: 0) - expect(cursor2.getBufferPosition()).toEqual [6, 0] - - describe "click and drag", -> - it "creates a selection from the initial click to mouse cursor's location ", -> - editor.attachToDom() - editor.css(position: 'absolute', top: 10, left: 10) - - # start - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 10]) - - # moving changes selection - $(document).trigger mousemoveEvent(editor: editor, point: [5, 27]) - - range = editor.getSelection().getScreenRange() - expect(range.start).toEqual({row: 4, column: 10}) - expect(range.end).toEqual({row: 5, column: 27}) - expect(editor.getCursorScreenPosition()).toEqual(row: 5, column: 27) - - # mouse up may occur outside of editor, but still need to halt selection - $(document).trigger 'mouseup' - - # moving after mouse up should not change selection - editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [8, 8]) - - range = editor.getSelection().getScreenRange() - expect(range.start).toEqual({row: 4, column: 10}) - expect(range.end).toEqual({row: 5, column: 27}) - expect(editor.getCursorScreenPosition()).toEqual(row: 5, column: 27) - - it "selects and scrolls if the mouse is dragged outside of the editor itself", -> - editor.vScrollMargin = 0 - editor.attachToDom(heightInLines: 5) - editor.scrollToBottom() - - spyOn(window, 'setInterval').andCallFake -> - - # start - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [12, 0]) - originalScrollTop = editor.scrollTop() - - # moving changes selection - $(document).trigger mousemoveEvent(editor: editor, pageX: 0, pageY: -1) - expect(editor.scrollTop()).toBe originalScrollTop - editor.lineHeight - - # every mouse move selects more text - for x in [0..10] - $(document).trigger mousemoveEvent(editor: editor, pageX: 0, pageY: -1) - - expect(editor.scrollTop()).toBe 0 - - it "ignores non left-click and drags", -> - editor.attachToDom() - editor.css(position: 'absolute', top: 10, left: 10) - - event = mousedownEvent(editor: editor, point: [4, 10]) - event.originalEvent.which = 2 - editor.renderedLines.trigger(event) - $(document).trigger mousemoveEvent(editor: editor, point: [5, 27]) - $(document).trigger 'mouseup' - - range = editor.getSelection().getScreenRange() - expect(range.start).toEqual({row: 4, column: 10}) - expect(range.end).toEqual({row: 4, column: 10}) - - it "ignores ctrl-click and drags", -> - editor.attachToDom() - editor.css(position: 'absolute', top: 10, left: 10) - - event = mousedownEvent(editor: editor, point: [4, 10]) - event.ctrlKey = true - editor.renderedLines.trigger(event) - $(document).trigger mousemoveEvent(editor: editor, point: [5, 27]) - $(document).trigger 'mouseup' - - range = editor.getSelection().getScreenRange() - expect(range.start).toEqual({row: 4, column: 10}) - expect(range.end).toEqual({row: 4, column: 10}) - - describe "double-click and drag", -> - it "selects the word under the cursor, then continues to select by word in either direction as the mouse is dragged", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 8], originalEvent: {detail: 1}) - editor.renderedLines.trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 8], originalEvent: {detail: 2}) - expect(editor.getSelectedText()).toBe "quicksort" - - editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [1, 8]) - expect(editor.getSelectedBufferRange()).toEqual [[0, 4], [1, 10]] - expect(editor.getCursorBufferPosition()).toEqual [1, 10] - - editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [0, 1]) - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 13]] - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 13]] - - # shift-clicking still selects by word, but does not preserve the initial range - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 25], originalEvent: {detail: 1}, shiftKey: true) - editor.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[0, 13], [5, 27]] - - describe "triple-click and drag", -> - it "expands the initial selection linewise in either direction", -> - editor.attachToDom() - - # triple click - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 7], originalEvent: {detail: 1}) - $(document).trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 7], originalEvent: {detail: 2}) - $(document).trigger 'mouseup' - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 7], originalEvent: {detail: 3}) - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [5, 0]] - - # moving changes selection linewise - editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [5, 27]) - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [6, 0]] - expect(editor.getCursorBufferPosition()).toEqual [6, 0] - - # moving changes selection linewise - editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [2, 27]) - expect(editor.getSelectedBufferRange()).toEqual [[2, 0], [5, 0]] - expect(editor.getCursorBufferPosition()).toEqual [2, 0] - - # mouse up may occur outside of editor, but still need to halt selection - $(document).trigger 'mouseup' - - describe "meta-click and drag", -> - it "adds an additional selection", -> - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 10]) - editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [5, 27]) - editor.renderedLines.trigger 'mouseup' - - editor.renderedLines.trigger mousedownEvent(editor: editor, point: [6, 10], metaKey: true) - editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [8, 27], metaKey: true) - editor.renderedLines.trigger 'mouseup' - - selections = editor.getSelections() - expect(selections.length).toBe 2 - [selection1, selection2] = selections - expect(selection1.getScreenRange()).toEqual [[4, 10], [5, 27]] - expect(selection2.getScreenRange()).toEqual [[6, 10], [8, 27]] - - describe "when text input events are triggered on the hidden input element", -> - it "inserts the typed character at the cursor position, both in the buffer and the pre element", -> - editor.attachToDom() - editor.setCursorScreenPosition(row: 1, column: 6) - - expect(buffer.lineForRow(1).charAt(6)).not.toBe 'q' - - editor.hiddenInput.textInput 'q' - - expect(buffer.lineForRow(1).charAt(6)).toBe 'q' - expect(editor.getCursorScreenPosition()).toEqual(row: 1, column: 7) - expect(editor.renderedLines.find('.line:eq(1)')).toHaveText buffer.lineForRow(1) - - describe "selection rendering", -> - [charWidth, lineHeight, selection, selectionView] = [] - - beforeEach -> - editor.attachToDom() - editor.width(500) - { charWidth, lineHeight } = editor - selection = editor.getSelection() - selectionView = editor.getSelectionView() - - describe "when a selection is added", -> - it "adds a selection view for it with the proper regions", -> - editor.activeEditSession.addSelectionForBufferRange([[2, 7], [2, 25]]) - selectionViews = editor.getSelectionViews() - expect(selectionViews.length).toBe 2 - expect(selectionViews[1].regions.length).toBe 1 - region = selectionViews[1].regions[0] - expect(region.position().top).toBeCloseTo(2 * lineHeight) - expect(region.position().left).toBeCloseTo(7 * charWidth) - expect(region.height()).toBeCloseTo lineHeight - expect(region.width()).toBeCloseTo((25 - 7) * charWidth) - - describe "when a selection changes", -> - describe "when the selection is within a single line", -> - it "covers the selection's range with a single region", -> - selection.setBufferRange(new Range({row: 2, column: 7}, {row: 2, column: 25})) - - expect(selectionView.regions.length).toBe 1 - region = selectionView.regions[0] - expect(region.position().top).toBeCloseTo(2 * lineHeight) - expect(region.position().left).toBeCloseTo(7 * charWidth) - expect(region.height()).toBeCloseTo lineHeight - expect(region.width()).toBeCloseTo((25 - 7) * charWidth) - - describe "when the selection spans 2 lines", -> - it "covers the selection's range with 2 regions", -> - selection.setBufferRange(new Range({row: 2, column: 7}, {row: 3, column: 25})) - - expect(selectionView.regions.length).toBe 2 - - region1 = selectionView.regions[0] - expect(region1.position().top).toBeCloseTo(2 * lineHeight) - expect(region1.position().left).toBeCloseTo(7 * charWidth) - expect(region1.height()).toBeCloseTo lineHeight - - expect(region1.width()).toBeCloseTo(editor.renderedLines.outerWidth() - region1.position().left) - region2 = selectionView.regions[1] - expect(region2.position().top).toBeCloseTo(3 * lineHeight) - expect(region2.position().left).toBeCloseTo(0) - expect(region2.height()).toBeCloseTo lineHeight - expect(region2.width()).toBeCloseTo(25 * charWidth) - - describe "when the selection spans more than 2 lines", -> - it "covers the selection's range with 3 regions", -> - selection.setBufferRange(new Range({row: 2, column: 7}, {row: 6, column: 25})) - - expect(selectionView.regions.length).toBe 3 - - region1 = selectionView.regions[0] - expect(region1.position().top).toBeCloseTo(2 * lineHeight) - expect(region1.position().left).toBeCloseTo(7 * charWidth) - expect(region1.height()).toBeCloseTo lineHeight - - expect(region1.width()).toBeCloseTo(editor.renderedLines.outerWidth() - region1.position().left) - region2 = selectionView.regions[1] - expect(region2.position().top).toBeCloseTo(3 * lineHeight) - expect(region2.position().left).toBeCloseTo(0) - expect(region2.height()).toBeCloseTo(3 * lineHeight) - expect(region2.width()).toBeCloseTo(editor.renderedLines.outerWidth()) - - # resizes with the editor - expect(editor.width()).toBeLessThan(800) - editor.width(800) - editor.resize() # call to trigger the resize event. - - region2 = selectionView.regions[1] - expect(region2.width()).toBe(editor.renderedLines.outerWidth()) - - region3 = selectionView.regions[2] - expect(region3.position().top).toBeCloseTo(6 * lineHeight) - expect(region3.position().left).toBeCloseTo(0) - expect(region3.height()).toBeCloseTo lineHeight - expect(region3.width()).toBeCloseTo(25 * charWidth) - - it "clears previously drawn regions before creating new ones", -> - selection.setBufferRange(new Range({row: 2, column: 7}, {row: 4, column: 25})) - expect(selectionView.regions.length).toBe 3 - expect(selectionView.find('.region').length).toBe 3 - - selectionView.updateDisplay() - expect(selectionView.regions.length).toBe 3 - expect(selectionView.find('.region').length).toBe 3 - - describe "when a selection merges with another selection", -> - it "removes the merged selection view", -> - editSession = editor.activeEditSession - editSession.setCursorScreenPosition([4, 10]) - editSession.selectToScreenPosition([5, 27]) - editSession.addCursorAtScreenPosition([3, 10]) - editSession.selectToScreenPosition([6, 27]) - - expect(editor.getSelectionViews().length).toBe 1 - expect(editor.find('.region').length).toBe 3 - - describe "when a selection is added and removed before the display is updated", -> - it "does not attempt to render the selection", -> - # don't update display until we request it - jasmine.unspy(editor, 'requestDisplayUpdate') - spyOn(editor, 'requestDisplayUpdate') - - editSession = editor.activeEditSession - selection = editSession.addSelectionForBufferRange([[3, 0], [3, 4]]) - selection.destroy() - editor.updateDisplay() - expect(editor.getSelectionViews().length).toBe 1 - - describe "when the selection is created with the selectAll event", -> - it "does not scroll to the end of the buffer", -> - editor.height(150) - editor.selectAll() - expect(editor.scrollTop()).toBe 0 - - # regression: does not scroll the scroll view when the editor is refocused - editor.hiddenInput.blur() - editor.hiddenInput.focus() - expect(editor.scrollTop()).toBe 0 - expect(editor.scrollView.scrollTop()).toBe 0 - - # does autoscroll when the selection is cleared - editor.moveCursorDown() - expect(editor.scrollTop()).toBeGreaterThan(0) - - describe "selection autoscrolling and highlighting when setting selected buffer range", -> - beforeEach -> - setEditorHeightInLines(editor, 4) - - describe "if autoscroll is true", -> - it "centers the viewport on the selection if its vertical center is currently offscreen", -> - editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) - expect(editor.scrollTop()).toBe 0 - - editor.setSelectedBufferRange([[6, 0], [8, 0]], autoscroll: true) - expect(editor.scrollTop()).toBe 5 * editor.lineHeight - - it "highlights the selection if autoscroll is true", -> - editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) - expect(editor.getSelectionView()).toHaveClass 'highlighted' - advanceClock(1000) - expect(editor.getSelectionView()).not.toHaveClass 'highlighted' - - editor.setSelectedBufferRange([[3, 0], [5, 0]], autoscroll: true) - expect(editor.getSelectionView()).toHaveClass 'highlighted' - - advanceClock(500) - spyOn(editor.getSelectionView(), 'removeClass').andCallThrough() - editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) - expect(editor.getSelectionView().removeClass).toHaveBeenCalledWith('highlighted') - expect(editor.getSelectionView()).toHaveClass 'highlighted' - - advanceClock(500) - expect(editor.getSelectionView()).toHaveClass 'highlighted' - - describe "if autoscroll is false", -> - it "does not scroll to the selection or the cursor", -> - editor.scrollToBottom() - scrollTopBefore = editor.scrollTop() - editor.setSelectedBufferRange([[0, 0], [1, 0]], autoscroll: false) - expect(editor.scrollTop()).toBe scrollTopBefore - - describe "if autoscroll is not specified", -> - it "autoscrolls to the cursor as normal", -> - editor.scrollToBottom() - editor.setSelectedBufferRange([[0, 0], [1, 0]]) - expect(editor.scrollTop()).toBe 0 - - describe "cursor rendering", -> - describe "when the cursor moves", -> - charWidth = null - - beforeEach -> - editor.attachToDom() - editor.vScrollMargin = 3 - editor.hScrollMargin = 5 - {charWidth} = editor - - it "repositions the cursor's view on screen", -> - editor.setCursorScreenPosition(row: 2, column: 2) - expect(editor.getCursorView().position()).toEqual(top: 2 * editor.lineHeight, left: 2 * editor.charWidth) - - it "hides the cursor when the selection is non-empty, and shows it otherwise", -> - cursorView = editor.getCursorView() - expect(editor.getSelection().isEmpty()).toBeTruthy() - expect(cursorView).toBeVisible() - - editor.setSelectedBufferRange([[0, 0], [3, 0]]) - expect(editor.getSelection().isEmpty()).toBeFalsy() - expect(cursorView).toBeHidden() - - editor.setCursorBufferPosition([1, 3]) - expect(editor.getSelection().isEmpty()).toBeTruthy() - expect(cursorView).toBeVisible() - - it "moves the hiddenInput to the same position with cursor's view", -> - editor.setCursorScreenPosition(row: 2, column: 2) - expect(editor.getCursorView().offset()).toEqual(editor.hiddenInput.offset()) - - describe "when the editor is using a variable-width font", -> - beforeEach -> - editor.setFontFamily('sans-serif') - - describe "on #darwin or #linux", -> - it "correctly positions the cursor", -> - editor.setCursorBufferPosition([3, 30]) - expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 178} - editor.setCursorBufferPosition([3, Infinity]) - expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 353} - - describe "on #win32", -> - it "correctly positions the cursor", -> - editor.setCursorBufferPosition([3, 30]) - expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 175} - editor.setCursorBufferPosition([3, Infinity]) - expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 346} - - describe "autoscrolling", -> - it "only autoscrolls when the last cursor is moved", -> - editor.setCursorBufferPosition([11,0]) - editor.addCursorAtBufferPosition([6,50]) + atom.activatePackage('language-javascript', sync: true) + editor = project.openSync('sample.js', autoIndent: false) + buffer = editor.buffer + lineLengths = buffer.getLines().map (line) -> line.length + + describe "@deserialize(state)", -> + it "restores selections and folds based on markers in the buffer", -> + editor.setSelectedBufferRange([[1, 2], [3, 4]]) + editor.addSelectionForBufferRange([[5, 6], [7, 5]], isReversed: true) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + + editor2 = deserialize(editor.serialize()) + + expect(editor2.id).toBe editor.id + expect(editor2.getBuffer().getPath()).toBe editor.getBuffer().getPath() + expect(editor2.getSelectedBufferRanges()).toEqual [[[1, 2], [3, 4]], [[5, 6], [7, 5]]] + expect(editor2.getSelection(1).isReversed()).toBeTruthy() + expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() + + describe ".copy()", -> + it "returns a different edit session with the same initial state", -> + editor.setSelectedBufferRange([[1, 2], [3, 4]]) + editor.addSelectionForBufferRange([[5, 6], [7, 8]], isReversed: true) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + + editor2 = editor.copy() + expect(editor2.id).not.toBe editor.id + expect(editor2.getSelectedBufferRanges()).toEqual editor.getSelectedBufferRanges() + expect(editor2.getSelection(1).isReversed()).toBeTruthy() + expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() + + # editor2 can now diverge from its origin edit session + editor2.getSelection().setBufferRange([[2, 1], [4, 3]]) + expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges() + editor2.unfoldBufferRow(4) + expect(editor2.isFoldedAtBufferRow(4)).not.toBe editor.isFoldedAtBufferRow(4) + + describe "config defaults", -> + it "uses the `editor.tabLength`, `editor.softWrap`, and `editor.softTabs` config values", -> + atom.config.set('editor.tabLength', 4) + atom.config.set('editor.softWrap', true) + atom.config.set('editor.softTabs', false) + editor1 = project.openSync('a') + expect(editor1.getTabLength()).toBe 4 + expect(editor1.getSoftWrap()).toBe true + expect(editor1.getSoftTabs()).toBe false + + atom.config.set('editor.tabLength', 100) + atom.config.set('editor.softWrap', false) + atom.config.set('editor.softTabs', true) + editor2 = project.openSync('b') + expect(editor2.getTabLength()).toBe 100 + expect(editor2.getSoftWrap()).toBe false + expect(editor2.getSoftTabs()).toBe true + + describe "title", -> + describe ".getTitle()", -> + it "uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", -> + expect(editor.getTitle()).toBe 'sample.js' + buffer.setPath(undefined) + expect(editor.getTitle()).toBe 'untitled' + + describe ".getLongTitle()", -> + it "appends the name of the containing directory to the basename of the file", -> + expect(editor.getLongTitle()).toBe 'sample.js - fixtures' + buffer.setPath(undefined) + expect(editor.getLongTitle()).toBe 'untitled' + + it "emits 'title-changed' events when the underlying buffer path", -> + titleChangedHandler = jasmine.createSpy("titleChangedHandler") + editor.on 'title-changed', titleChangedHandler + + buffer.setPath('/foo/bar/baz.txt') + buffer.setPath(undefined) + expect(titleChangedHandler.callCount).toBe 2 + + describe "cursor", -> + describe ".getCursor()", -> + it "returns the most recently created cursor", -> + editor.addCursorAtScreenPosition([1, 0]) + lastCursor = editor.addCursorAtScreenPosition([2, 0]) + expect(editor.getCursor()).toBe lastCursor + + describe ".setCursorScreenPosition(screenPosition)", -> + it "clears a goal column established by vertical movement", -> + # set a goal column by moving down + editor.setCursorScreenPosition(row: 3, column: lineLengths[3]) + editor.moveCursorDown() + expect(editor.getCursorScreenPosition().column).not.toBe 6 + + # clear the goal column by explicitly setting the cursor position + editor.setCursorScreenPosition([4,6]) + expect(editor.getCursorScreenPosition().column).toBe 6 + + editor.moveCursorDown() + expect(editor.getCursorScreenPosition().column).toBe 6 + + it "merges multiple cursors", -> + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 1]) + [cursor1, cursor2] = editor.getCursors() + editor.setCursorScreenPosition([4, 7]) + expect(editor.getCursors().length).toBe 1 + expect(editor.getCursors()).toEqual [cursor1] + expect(editor.getCursorScreenPosition()).toEqual [4, 7] + + describe "when soft-wrap is enabled and code is folded", -> + beforeEach -> + editor.setSoftWrap(true) + editor.setEditorWidthInChars(50) + editor.createFold(2, 3) + + it "positions the cursor at the buffer position that corresponds to the given screen position", -> + editor.setCursorScreenPosition([9, 0]) + expect(editor.getCursorBufferPosition()).toEqual [8, 11] + + describe ".moveCursorUp()", -> + it "moves the cursor up", -> + editor.setCursorScreenPosition([2, 2]) + editor.moveCursorUp() + expect(editor.getCursorScreenPosition()).toEqual [1, 2] + + it "retains the goal column across lines of differing length", -> + expect(lineLengths[6]).toBeGreaterThan(32) + editor.setCursorScreenPosition(row: 6, column: 32) + + editor.moveCursorUp() + expect(editor.getCursorScreenPosition().column).toBe lineLengths[5] + + editor.moveCursorUp() + expect(editor.getCursorScreenPosition().column).toBe lineLengths[4] + + editor.moveCursorUp() + expect(editor.getCursorScreenPosition().column).toBe 32 + + describe "when the cursor is on the first line", -> + it "moves the cursor to the beginning of the line, but retains the goal column", -> + editor.setCursorScreenPosition([0, 4]) + editor.moveCursorUp() + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + editor.moveCursorDown() + expect(editor.getCursorScreenPosition()).toEqual([1, 4]) + + describe "when there is a selection", -> + beforeEach -> + editor.setSelectedBufferRange([[4, 9],[5, 10]]) + + it "moves above the selection", -> + cursor = editor.getCursor() + editor.moveCursorUp() + expect(cursor.getBufferPosition()).toEqual [3, 9] + + it "merges cursors when they overlap", -> + editor.addCursorAtScreenPosition([1, 0]) [cursor1, cursor2] = editor.getCursors() - spyOn(editor, 'scrollToPixelPosition') - cursor1.setScreenPosition([10, 10]) - expect(editor.scrollToPixelPosition).not.toHaveBeenCalled() - - cursor2.setScreenPosition([11, 11]) - expect(editor.scrollToPixelPosition).toHaveBeenCalled() + editor.moveCursorUp() + expect(editor.getCursors()).toEqual [cursor1] + expect(cursor1.getBufferPosition()).toEqual [0,0] + + describe ".moveCursorDown()", -> + it "moves the cursor down", -> + editor.setCursorScreenPosition([2, 2]) + editor.moveCursorDown() + expect(editor.getCursorScreenPosition()).toEqual [3, 2] + + it "retains the goal column across lines of differing length", -> + editor.setCursorScreenPosition(row: 3, column: lineLengths[3]) + + editor.moveCursorDown() + expect(editor.getCursorScreenPosition().column).toBe lineLengths[4] + + editor.moveCursorDown() + expect(editor.getCursorScreenPosition().column).toBe lineLengths[5] + + editor.moveCursorDown() + expect(editor.getCursorScreenPosition().column).toBe lineLengths[3] + + describe "when the cursor is on the last line", -> + it "moves the cursor to the end of line, but retains the goal column when moving back up", -> + lastLineIndex = buffer.getLines().length - 1 + lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + editor.setCursorScreenPosition(row: lastLineIndex, column: editor.getTabLength()) + editor.moveCursorDown() + expect(editor.getCursorScreenPosition()).toEqual(row: lastLineIndex, column: lastLine.length) + + editor.moveCursorUp() + expect(editor.getCursorScreenPosition().column).toBe editor.getTabLength() + + it "retains a goal column of 0 when moving back up", -> + lastLineIndex = buffer.getLines().length - 1 + lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + editor.setCursorScreenPosition(row: lastLineIndex, column: 0) + editor.moveCursorDown() + editor.moveCursorUp() + expect(editor.getCursorScreenPosition().column).toBe 0 + + describe "when there is a selection", -> + beforeEach -> + editor.setSelectedBufferRange([[4, 9],[5, 10]]) + + it "moves below the selection", -> + cursor = editor.getCursor() + editor.moveCursorDown() + expect(cursor.getBufferPosition()).toEqual [6, 10] + + it "merges cursors when they overlap", -> + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([11, 2]) + [cursor1, cursor2] = editor.getCursors() + + editor.moveCursorDown() + expect(editor.getCursors()).toEqual [cursor1] + expect(cursor1.getBufferPosition()).toEqual [12,2] + + describe ".moveCursorLeft()", -> + it "moves the cursor by one column to the left", -> + editor.setCursorScreenPosition([1, 8]) + editor.moveCursorLeft() + expect(editor.getCursorScreenPosition()).toEqual [1, 7] + + describe "when the cursor is in the first column", -> + describe "when there is a previous line", -> + it "wraps to the end of the previous line", -> + editor.setCursorScreenPosition(row: 1, column: 0) + editor.moveCursorLeft() + expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: buffer.lineForRow(0).length) + + describe "when the cursor is on the first line", -> + it "remains in the same position (0,0)", -> + editor.setCursorScreenPosition(row: 0, column: 0) + editor.moveCursorLeft() + expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) + + describe "when softTabs is enabled and the cursor is preceded by leading whitespace", -> + it "skips tabLength worth of whitespace at a time", -> + editor.setCursorBufferPosition([5, 6]) + + editor.moveCursorLeft() + expect(editor.getCursorBufferPosition()).toEqual [5, 4] + + describe "when there is a selection", -> + beforeEach -> + editor.setSelectedBufferRange([[5, 22],[5, 27]]) + + it "moves to the left of the selection", -> + cursor = editor.getCursor() + editor.moveCursorLeft() + expect(cursor.getBufferPosition()).toEqual [5, 22] + + editor.moveCursorLeft() + expect(cursor.getBufferPosition()).toEqual [5, 21] + + it "merges cursors when they overlap", -> + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 1]) + + [cursor1, cursor2] = editor.getCursors() + editor.moveCursorLeft() + expect(editor.getCursors()).toEqual [cursor1] + expect(cursor1.getBufferPosition()).toEqual [0,0] + + describe ".moveCursorRight()", -> + it "moves the cursor by one column to the right", -> + editor.setCursorScreenPosition([3, 3]) + editor.moveCursorRight() + expect(editor.getCursorScreenPosition()).toEqual [3, 4] + + describe "when the cursor is on the last column of a line", -> + describe "when there is a subsequent line", -> + it "wraps to the beginning of the next line", -> + editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) + editor.moveCursorRight() + expect(editor.getCursorScreenPosition()).toEqual [1, 0] + + describe "when the cursor is on the last line", -> + it "remains in the same position", -> + lastLineIndex = buffer.getLines().length - 1 + lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + lastPosition = { row: lastLineIndex, column: lastLine.length } + editor.setCursorScreenPosition(lastPosition) + editor.moveCursorRight() + + expect(editor.getCursorScreenPosition()).toEqual(lastPosition) + + describe "when there is a selection", -> + beforeEach -> + editor.setSelectedBufferRange([[5, 22],[5, 27]]) + + it "moves to the left of the selection", -> + cursor = editor.getCursor() + editor.moveCursorRight() + expect(cursor.getBufferPosition()).toEqual [5, 27] + + editor.moveCursorRight() + expect(cursor.getBufferPosition()).toEqual [5, 28] + + it "merges cursors when they overlap", -> + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([12, 1]) + [cursor1, cursor2] = editor.getCursors() + + editor.moveCursorRight() + expect(editor.getCursors()).toEqual [cursor1] + expect(cursor1.getBufferPosition()).toEqual [12,2] + + describe ".moveCursorToTop()", -> + it "moves the cursor to the top of the buffer", -> + editor.setCursorScreenPosition [11,1] + editor.addCursorAtScreenPosition [12,0] + editor.moveCursorToTop() + expect(editor.getCursors().length).toBe 1 + expect(editor.getCursorBufferPosition()).toEqual [0,0] + + describe ".moveCursorToBottom()", -> + it "moves the cusor to the bottom of the buffer", -> + editor.setCursorScreenPosition [0,0] + editor.addCursorAtScreenPosition [1,0] + editor.moveCursorToBottom() + expect(editor.getCursors().length).toBe 1 + expect(editor.getCursorBufferPosition()).toEqual [12,2] + + describe ".moveCursorToBeginningOfLine()", -> + describe "when soft wrap is on", -> + it "moves cursor to the beginning of the screen line", -> + editor.setSoftWrap(true) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveCursorToBeginningOfLine() + cursor = editor.getCursor() + expect(cursor.getScreenPosition()).toEqual [1, 0] + + describe "when soft wrap is off", -> + it "moves cursor to the beginning of then line", -> + editor.setCursorScreenPosition [0,5] + editor.addCursorAtScreenPosition [1,7] + editor.moveCursorToBeginningOfLine() + expect(editor.getCursors().length).toBe 2 + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [0,0] + expect(cursor2.getBufferPosition()).toEqual [1,0] + + describe ".moveCursorToEndOfLine()", -> + describe "when soft wrap is on", -> + it "moves cursor to the beginning of the screen line", -> + editor.setSoftWrap(true) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveCursorToEndOfLine() + cursor = editor.getCursor() + expect(cursor.getScreenPosition()).toEqual [1, 9] + + describe "when soft wrap is off", -> + it "moves cursor to the end of line", -> + editor.setCursorScreenPosition [0,0] + editor.addCursorAtScreenPosition [1,0] + editor.moveCursorToEndOfLine() + expect(editor.getCursors().length).toBe 2 + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [0,29] + expect(cursor2.getBufferPosition()).toEqual [1,30] + + describe ".moveCursorToFirstCharacterOfLine()", -> + describe "when soft wrap is on", -> + it "moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", -> + editor.setSoftWrap(true) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition [2,5] + editor.addCursorAtScreenPosition [8,7] + + editor.moveCursorToFirstCharacterOfLine() + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getScreenPosition()).toEqual [2,0] + expect(cursor2.getScreenPosition()).toEqual [8,4] + + editor.moveCursorToFirstCharacterOfLine() + expect(cursor1.getScreenPosition()).toEqual [2,0] + expect(cursor2.getScreenPosition()).toEqual [8,0] + + describe "when soft wrap is off", -> + it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> + editor.setCursorScreenPosition [0,5] + editor.addCursorAtScreenPosition [1,7] + + editor.moveCursorToFirstCharacterOfLine() + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [0,0] + expect(cursor2.getBufferPosition()).toEqual [1,2] + + editor.moveCursorToFirstCharacterOfLine() + expect(cursor1.getBufferPosition()).toEqual [0,0] + expect(cursor2.getBufferPosition()).toEqual [1,0] + + describe ".moveCursorToBeginningOfWord()", -> + it "moves the cursor to the beginning of the word", -> + editor.setCursorBufferPosition [0, 8] + editor.addCursorAtBufferPosition [1, 12] + editor.addCursorAtBufferPosition [3, 0] + [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveCursorToBeginningOfWord() + + expect(cursor1.getBufferPosition()).toEqual [0, 4] + expect(cursor2.getBufferPosition()).toEqual [1, 11] + expect(cursor3.getBufferPosition()).toEqual [2, 39] + + it "does not fail at position [0, 0]", -> + editor.setCursorBufferPosition([0, 0]) + editor.moveCursorToBeginningOfWord() + + it "treats lines with only whitespace as a word", -> + editor.setCursorBufferPosition([11, 0]) + editor.moveCursorToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual [10, 0] + + it "works when the current line is blank", -> + editor.setCursorBufferPosition([10, 0]) + editor.moveCursorToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual [9, 2] + + describe ".moveCursorToPreviousWordBoundary()", -> + it "moves the cursor to the previous word boundary", -> + editor.setCursorBufferPosition [0, 8] + editor.addCursorAtBufferPosition [2, 0] + editor.addCursorAtBufferPosition [2, 4] + editor.addCursorAtBufferPosition [3, 14] + [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.moveCursorToPreviousWordBoundary() + + expect(cursor1.getBufferPosition()).toEqual [0, 4] + expect(cursor2.getBufferPosition()).toEqual [1, 30] + expect(cursor3.getBufferPosition()).toEqual [2, 0] + expect(cursor4.getBufferPosition()).toEqual [3, 13] + + describe ".moveCursorToNextWordBoundary()", -> + it "moves the cursor to the previous word boundary", -> + editor.setCursorBufferPosition [0, 8] + editor.addCursorAtBufferPosition [2, 40] + editor.addCursorAtBufferPosition [3, 0] + editor.addCursorAtBufferPosition [3, 30] + [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.moveCursorToNextWordBoundary() + + expect(cursor1.getBufferPosition()).toEqual [0, 13] + expect(cursor2.getBufferPosition()).toEqual [3, 0] + expect(cursor3.getBufferPosition()).toEqual [3, 4] + expect(cursor4.getBufferPosition()).toEqual [3, 31] + + describe ".moveCursorToEndOfWord()", -> + it "moves the cursor to the end of the word", -> + editor.setCursorBufferPosition [0, 6] + editor.addCursorAtBufferPosition [1, 10] + editor.addCursorAtBufferPosition [2, 40] + [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveCursorToEndOfWord() + + expect(cursor1.getBufferPosition()).toEqual [0, 13] + expect(cursor2.getBufferPosition()).toEqual [1, 12] + expect(cursor3.getBufferPosition()).toEqual [3, 7] + + it "does not blow up when there is no next word", -> + editor.setCursorBufferPosition [Infinity, Infinity] + endPosition = editor.getCursorBufferPosition() + editor.moveCursorToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual endPosition + + it "treats lines with only whitespace as a word", -> + editor.setCursorBufferPosition([9, 4]) + editor.moveCursorToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual [10, 0] + + it "works when the current line is blank", -> + editor.setCursorBufferPosition([10, 0]) + editor.moveCursorToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual [11, 8] + + describe ".moveCursorToBeginningOfNextWord()", -> + it "moves the cursor before the first character of the next word", -> + editor.setCursorBufferPosition [0,6] + editor.addCursorAtBufferPosition [1,11] + editor.addCursorAtBufferPosition [2,0] + [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveCursorToBeginningOfNextWord() + + expect(cursor1.getBufferPosition()).toEqual [0, 14] + expect(cursor2.getBufferPosition()).toEqual [1, 13] + expect(cursor3.getBufferPosition()).toEqual [2, 4] + + # When the cursor is on whitespace + editor.setText("ab cde- ") + editor.setCursorBufferPosition [0,2] + cursor = editor.getCursor() + editor.moveCursorToBeginningOfNextWord() + + expect(cursor.getBufferPosition()).toEqual [0, 3] + + it "does not blow up when there is no next word", -> + editor.setCursorBufferPosition [Infinity, Infinity] + endPosition = editor.getCursorBufferPosition() + editor.moveCursorToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual endPosition + + it "treats lines with only whitespace as a word", -> + editor.setCursorBufferPosition([9, 4]) + editor.moveCursorToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual [10, 0] + + it "works when the current line is blank", -> + editor.setCursorBufferPosition([10, 0]) + editor.moveCursorToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual [11, 9] + + describe ".getCurrentParagraphBufferRange()", -> + it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", -> + buffer.setText """ + I am the first paragraph, + bordered by the beginning of + the file + #{' '} + + I am the second paragraph + with blank lines above and below + me. + + I am the last paragraph, + bordered by the end of the file. + """ + + # in a paragraph + editor.setCursorBufferPosition([1, 7]) + expect(editor.getCurrentParagraphBufferRange()).toEqual [[0, 0], [2, 8]] + + editor.setCursorBufferPosition([7, 1]) + expect(editor.getCurrentParagraphBufferRange()).toEqual [[5, 0], [7, 3]] + + editor.setCursorBufferPosition([9, 10]) + expect(editor.getCurrentParagraphBufferRange()).toEqual [[9, 0], [10, 32]] + + # between paragraphs + editor.setCursorBufferPosition([3, 1]) + expect(editor.getCurrentParagraphBufferRange()).toBeUndefined() + + describe "cursor-moved events", -> + cursorMovedHandler = null - it "does not autoscroll if the 'autoscroll' option is false", -> - editor.setCursorBufferPosition([11,0]) - spyOn(editor, 'scrollToPixelPosition') - editor.setCursorScreenPosition([10, 10], autoscroll: false) - expect(editor.scrollToPixelPosition).not.toHaveBeenCalled() - - it "autoscrolls to cursor if autoscroll is true, even if the position does not change", -> - spyOn(editor, 'scrollToPixelPosition') - editor.setCursorScreenPosition([4, 10], autoscroll: false) - editor.setCursorScreenPosition([4, 10]) - expect(editor.scrollToPixelPosition).toHaveBeenCalled() - editor.scrollToPixelPosition.reset() - - editor.setCursorBufferPosition([4, 10]) - expect(editor.scrollToPixelPosition).toHaveBeenCalled() - - it "does not autoscroll the cursor based on a buffer change, unless the buffer change was initiated by the cursor", -> - lastVisibleRow = editor.getLastVisibleScreenRow() - editor.addCursorAtBufferPosition([lastVisibleRow, 0]) - spyOn(editor, 'scrollToPixelPosition') - buffer.insert([lastVisibleRow, 0], "\n\n") - expect(editor.scrollToPixelPosition).not.toHaveBeenCalled() - editor.insertText('\n\n') - expect(editor.scrollToPixelPosition.callCount).toBe 1 - - it "autoscrolls on undo/redo", -> - spyOn(editor, 'scrollToPixelPosition') - editor.insertText('\n\n') - expect(editor.scrollToPixelPosition.callCount).toBe 1 - editor.undo() - expect(editor.scrollToPixelPosition.callCount).toBe 2 - editor.redo() - expect(editor.scrollToPixelPosition.callCount).toBe 3 - - describe "when the last cursor exceeds the upper or lower scroll margins", -> - describe "when the editor is taller than twice the vertical scroll margin", -> - it "sets the scrollTop so the cursor remains within the scroll margin", -> - setEditorHeightInLines(editor, 10) - - _.times 6, -> editor.moveCursorDown() - expect(editor.scrollTop()).toBe(0) - - editor.moveCursorDown() - expect(editor.scrollTop()).toBe(editor.lineHeight) - - editor.moveCursorDown() - expect(editor.scrollTop()).toBe(editor.lineHeight * 2) - - _.times 3, -> editor.moveCursorUp() - - editor.moveCursorUp() - expect(editor.scrollTop()).toBe(editor.lineHeight) - - editor.moveCursorUp() - expect(editor.scrollTop()).toBe(0) - - describe "when the editor is shorter than twice the vertical scroll margin", -> - it "sets the scrollTop based on a reduced scroll margin, which prevents a jerky tug-of-war between upper and lower scroll margins", -> - setEditorHeightInLines(editor, 5) - - _.times 3, -> editor.moveCursorDown() - - expect(editor.scrollTop()).toBe(editor.lineHeight) - - editor.moveCursorUp() - expect(editor.renderedLines.css('top')).toBe "0px" - - describe "when the last cursor exceeds the right or left scroll margins", -> - describe "when soft-wrap is disabled", -> - describe "when the editor is wider than twice the horizontal scroll margin", -> - it "sets the scrollView's scrollLeft so the cursor remains within the scroll margin", -> - setEditorWidthInChars(editor, 30) - - # moving right - editor.setCursorScreenPosition([2, 24]) - expect(editor.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 25]) - expect(editor.scrollLeft()).toBe charWidth - - editor.setCursorScreenPosition([2, 28]) - expect(editor.scrollLeft()).toBe charWidth * 4 - - # moving left - editor.setCursorScreenPosition([2, 9]) - expect(editor.scrollLeft()).toBe charWidth * 4 - - editor.setCursorScreenPosition([2, 8]) - expect(editor.scrollLeft()).toBe charWidth * 3 - - editor.setCursorScreenPosition([2, 5]) - expect(editor.scrollLeft()).toBe 0 - - describe "when the editor is narrower than twice the horizontal scroll margin", -> - it "sets the scrollView's scrollLeft based on a reduced horizontal scroll margin, to prevent a jerky tug-of-war between right and left scroll margins", -> - editor.hScrollMargin = 6 - setEditorWidthInChars(editor, 7) - - editor.setCursorScreenPosition([2, 3]) - window.advanceClock() - expect(editor.scrollLeft()).toBe(0) - - editor.setCursorScreenPosition([2, 4]) - window.advanceClock() - expect(editor.scrollLeft()).toBe(charWidth) - - editor.setCursorScreenPosition([2, 3]) - window.advanceClock() - expect(editor.scrollLeft()).toBe(0) - - describe "when soft-wrap is enabled", -> - beforeEach -> - editSession.setSoftWrap(true) - - it "does not scroll the buffer horizontally", -> - editor.width(charWidth * 30) - - # moving right - editor.setCursorScreenPosition([2, 24]) - expect(editor.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 25]) - expect(editor.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 28]) - expect(editor.scrollLeft()).toBe 0 - - # moving left - editor.setCursorScreenPosition([2, 9]) - expect(editor.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 8]) - expect(editor.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 5]) - expect(editor.scrollLeft()).toBe 0 - - describe "when editor:toggle-soft-wrap is toggled", -> - describe "when the text exceeds the editor width and the scroll-view is horizontally scrolled", -> - it "wraps the text and renders properly", -> - editor.attachToDom(heightInLines: 30, widthInChars: 30) - editor.setWidthInChars(100) - editor.setText("Fashion axe umami jean shorts retro hashtag carles mumblecore. Photo booth skateboard Austin gentrify occupy ethical. Food truck gastropub keffiyeh, squid deep v pinterest literally sustainable salvia scenester messenger bag. Neutra messenger bag flexitarian four loko, shoreditch VHS pop-up tumblr seitan synth master cleanse. Marfa selvage ugh, raw denim authentic try-hard mcsweeney's trust fund fashion axe actually polaroid viral sriracha. Banh mi marfa plaid single-origin coffee. Pickled mumblecore lomo ugh bespoke.") - editor.scrollLeft(editor.charWidth * 30) - editor.trigger "editor:toggle-soft-wrap" - expect(editor.scrollLeft()).toBe 0 - expect(editor.activeEditSession.getSoftWrapColumn()).not.toBe 100 - - describe "text rendering", -> - describe "when all lines in the buffer are visible on screen", -> - beforeEach -> - editor.attachToDom() - expect(editor.trueHeight()).toBeCloseTo buffer.getLineCount() * editor.lineHeight - - it "creates a line element for each line in the buffer with the html-escaped text of the line", -> - expect(editor.renderedLines.find('.line').length).toEqual(buffer.getLineCount()) - expect(buffer.lineForRow(2)).toContain('<') - expect(editor.renderedLines.find('.line:eq(2)').html()).toContain '<' - - # renders empty lines with a non breaking space - expect(buffer.lineForRow(10)).toBe '' - expect(editor.renderedLines.find('.line:eq(10)').html()).toBe ' ' - - it "syntax highlights code based on the file type", -> - line0 = editor.renderedLines.find('.line:first') - span0 = line0.children('span:eq(0)') - expect(span0).toMatchSelector '.source.js' - expect(span0.children('span:eq(0)')).toMatchSelector '.storage.modifier.js' - expect(span0.children('span:eq(0)').text()).toBe 'var' - - span0_1 = span0.children('span:eq(1)') - expect(span0_1).toMatchSelector '.meta.function.js' - expect(span0_1.text()).toBe 'quicksort = function ()' - expect(span0_1.children('span:eq(0)')).toMatchSelector '.entity.name.function.js' - expect(span0_1.children('span:eq(0)').text()).toBe "quicksort" - expect(span0_1.children('span:eq(1)')).toMatchSelector '.keyword.operator.js' - expect(span0_1.children('span:eq(1)').text()).toBe "=" - expect(span0_1.children('span:eq(2)')).toMatchSelector '.storage.type.function.js' - expect(span0_1.children('span:eq(2)').text()).toBe "function" - expect(span0_1.children('span:eq(3)')).toMatchSelector '.punctuation.definition.parameters.begin.js' - expect(span0_1.children('span:eq(3)').text()).toBe "(" - expect(span0_1.children('span:eq(4)')).toMatchSelector '.punctuation.definition.parameters.end.js' - expect(span0_1.children('span:eq(4)').text()).toBe ")" - - expect(span0.children('span:eq(2)')).toMatchSelector '.meta.brace.curly.js' - expect(span0.children('span:eq(2)').text()).toBe "{" - - line12 = editor.renderedLines.find('.line:eq(11)').children('span:eq(0)') - expect(line12.children('span:eq(1)')).toMatchSelector '.keyword' - - it "wraps hard tabs in a span", -> - editor.setText('\t<- hard tab') - line0 = editor.renderedLines.find('.line:first') - span0_0 = line0.children('span:eq(0)').children('span:eq(0)') - expect(span0_0).toMatchSelector '.hard-tab' - expect(span0_0.text()).toBe ' ' - - it "wraps leading whitespace in a span", -> - line1 = editor.renderedLines.find('.line:eq(1)') - span0_0 = line1.children('span:eq(0)').children('span:eq(0)') - expect(span0_0).toMatchSelector '.leading-whitespace' - expect(span0_0.text()).toBe ' ' - - describe "when the line has trailing whitespace", -> - it "wraps trailing whitespace in a span", -> - editor.setText('trailing whitespace -> ') - line0 = editor.renderedLines.find('.line:first') - span0_last = line0.children('span:eq(0)').children('span:last') - expect(span0_last).toMatchSelector '.trailing-whitespace' - expect(span0_last.text()).toBe ' ' - - describe "when lines are updated in the buffer", -> - it "syntax highlights the updated lines", -> - expect(editor.renderedLines.find('.line:eq(0) > span:first > span:first')).toMatchSelector '.storage.modifier.js' - buffer.insert([0, 0], "q") - expect(editor.renderedLines.find('.line:eq(0) > span:first > span:first')).not.toMatchSelector '.storage.modifier.js' - - # verify that re-highlighting can occur below the changed line - buffer.insert([5,0], "/* */") - buffer.insert([1,0], "/*") - expect(editor.renderedLines.find('.line:eq(2) > span:first > span:first')).toMatchSelector '.comment' - - describe "when some lines at the end of the buffer are not visible on screen", -> - beforeEach -> - editor.attachToDom(heightInLines: 5.5) - - it "only renders the visible lines plus the overdrawn lines, setting the padding-bottom of the lines element to account for the missing lines", -> - expect(editor.renderedLines.find('.line').length).toBe 8 - expectedPaddingBottom = (buffer.getLineCount() - 8) * editor.lineHeight - expect(editor.renderedLines.css('padding-bottom')).toBe "#{expectedPaddingBottom}px" - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - it "renders additional lines when the editor is resized", -> - setEditorHeightInLines(editor, 10) - $(window).trigger 'resize' - - expect(editor.renderedLines.find('.line').length).toBe 12 - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(11) - - it "renders correctly when scrolling after text is added to the buffer", -> - editor.insertText("1\n") - _.times 4, -> editor.moveCursorDown() - expect(editor.renderedLines.find('.line:eq(2)').text()).toBe editor.lineForBufferRow(2) - expect(editor.renderedLines.find('.line:eq(7)').text()).toBe editor.lineForBufferRow(7) - - it "renders correctly when scrolling after text is removed from buffer", -> - editor.getBuffer().delete([[0,0],[1,0]]) - expect(editor.renderedLines.find('.line:eq(0)').text()).toBe editor.lineForBufferRow(0) - expect(editor.renderedLines.find('.line:eq(5)').text()).toBe editor.lineForBufferRow(5) - - editor.scrollTop(3 * editor.lineHeight) - expect(editor.renderedLines.find('.line:first').text()).toBe editor.lineForBufferRow(1) - expect(editor.renderedLines.find('.line:last').text()).toBe editor.lineForBufferRow(10) - - describe "when creating and destroying folds that are longer than the visible lines", -> - describe "when the cursor precedes the fold when it is destroyed", -> - it "renders lines and line numbers correctly", -> - scrollHeightBeforeFold = editor.scrollView.prop('scrollHeight') - fold = editor.createFold(1, 9) - fold.destroy() - expect(editor.scrollView.prop('scrollHeight')).toBe scrollHeightBeforeFold - - expect(editor.renderedLines.find('.line').length).toBe 8 - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - expect(editor.gutter.find('.line-number').length).toBe 8 - expect(editor.gutter.find('.line-number:last').intValue()).toBe 8 - - editor.scrollTop(4 * editor.lineHeight) - expect(editor.renderedLines.find('.line').length).toBe 10 - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(11) - - describe "when the cursor follows the fold when it is destroyed", -> - it "renders lines and line numbers correctly", -> - fold = editor.createFold(1, 9) - editor.setCursorBufferPosition([10, 0]) - fold.destroy() - - expect(editor.renderedLines.find('.line').length).toBe 8 - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(12) - - expect(editor.gutter.find('.line-number').length).toBe 8 - expect(editor.gutter.find('.line-number:last').text()).toBe '13' - - editor.scrollTop(4 * editor.lineHeight) - - expect(editor.renderedLines.find('.line').length).toBe 10 - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(11) - - describe "when scrolling vertically", -> - describe "when scrolling less than the editor's height", -> - it "draws new lines and removes old lines when the last visible line will exceed the last rendered line", -> - expect(editor.renderedLines.find('.line').length).toBe 8 - - editor.scrollTop(editor.lineHeight * 1.5) - expect(editor.renderedLines.find('.line').length).toBe 8 - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - editor.scrollTop(editor.lineHeight * 3.5) # first visible row will be 3, last will be 8 - expect(editor.renderedLines.find('.line').length).toBe 10 - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(1) - expect(editor.renderedLines.find('.line:last').html()).toBe ' ' # line 10 is blank - expect(editor.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editor.gutter.find('.line-number:last').intValue()).toBe 11 - - # here we don't scroll far enough to trigger additional rendering - editor.scrollTop(editor.lineHeight * 5.5) # first visible row will be 5, last will be 10 - expect(editor.renderedLines.find('.line').length).toBe 10 - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(1) - expect(editor.renderedLines.find('.line:last').html()).toBe ' ' # line 10 is blank - expect(editor.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editor.gutter.find('.line-number:last').intValue()).toBe 11 - - editor.scrollTop(editor.lineHeight * 7.5) # first visible row is 7, last will be 12 - expect(editor.renderedLines.find('.line').length).toBe 8 - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(5) - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(12) - - editor.scrollTop(editor.lineHeight * 3.5) # first visible row will be 3, last will be 8 - expect(editor.renderedLines.find('.line').length).toBe 10 - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(1) - expect(editor.renderedLines.find('.line:last').html()).toBe ' ' # line 10 is blank - - editor.scrollTop(0) - expect(editor.renderedLines.find('.line').length).toBe 8 - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - describe "when scrolling more than the editors height", -> - it "removes lines that are offscreen and not in range of the overdraw and builds lines that become visible", -> - editor.scrollTop(editor.layerHeight - editor.scrollView.height()) - expect(editor.renderedLines.find('.line').length).toBe 8 - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(5) - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(12) - - editor.verticalScrollbar.scrollBottom(0) - editor.verticalScrollbar.trigger 'scroll' - expect(editor.renderedLines.find('.line').length).toBe 8 - expect(editor.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editor.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - it "adjusts the vertical padding of the lines element to account for non-rendered lines", -> - editor.scrollTop(editor.lineHeight * 3) - firstVisibleBufferRow = 3 - expectedPaddingTop = (firstVisibleBufferRow - editor.lineOverdraw) * editor.lineHeight - expect(editor.renderedLines.css('padding-top')).toBe "#{expectedPaddingTop}px" - - lastVisibleBufferRow = Math.ceil(3 + 5.5) # scroll top in lines + height in lines - lastOverdrawnRow = lastVisibleBufferRow + editor.lineOverdraw - expectedPaddingBottom = ((buffer.getLineCount() - lastOverdrawnRow) * editor.lineHeight) - expect(editor.renderedLines.css('padding-bottom')).toBe "#{expectedPaddingBottom}px" - - editor.scrollToBottom() - # scrolled to bottom, first visible row is 5 and first rendered row is 3 - firstVisibleBufferRow = Math.floor(buffer.getLineCount() - 5.5) - firstOverdrawnBufferRow = firstVisibleBufferRow - editor.lineOverdraw - expectedPaddingTop = firstOverdrawnBufferRow * editor.lineHeight - expect(editor.renderedLines.css('padding-top')).toBe "#{expectedPaddingTop}px" - expect(editor.renderedLines.css('padding-bottom')).toBe "0px" - - describe "when lines are added", -> - beforeEach -> - editor.attachToDom(heightInLines: 5) - - describe "when the change precedes the first rendered row", -> - it "inserts and removes rendered lines to account for upstream change", -> - editor.scrollToBottom() - expect(editor.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - buffer.change([[1,0], [3,0]], "1\n2\n3\n") - expect(editor.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editor.renderedLines.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.scrollToBottom() - - expect(editor.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editor.renderedLines.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.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - describe "when the change 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.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) - - 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.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) - - it "increases the width of the rendered lines element to be either the width of the longest line or the width of the scrollView (whichever is longer)", -> - maxLineLength = editor.getMaxScreenLineLength() - setEditorWidthInChars(editor, maxLineLength) - widthBefore = editor.renderedLines.width() - expect(widthBefore).toBe editor.scrollView.width() + 20 - buffer.change([[12,0], [12,0]], [1..maxLineLength*2].join('')) - expect(editor.renderedLines.width()).toBeGreaterThan widthBefore - - describe "when lines are removed", -> - beforeEach -> - editor.attachToDom(heightInLines: 5) - - it "sets the rendered screen line's width to either the max line length or the scollView's width (whichever is greater)", -> - maxLineLength = editor.getMaxScreenLineLength() - setEditorWidthInChars(editor, maxLineLength) - buffer.change([[12,0], [12,0]], [1..maxLineLength*2].join('')) - expect(editor.renderedLines.width()).toBeGreaterThan editor.scrollView.width() - widthBefore = editor.renderedLines.width() - buffer.delete([[12, 0], [12, Infinity]]) - expect(editor.renderedLines.width()).toBe editor.scrollView.width() + 20 - - describe "when the change the precedes the first rendered row", -> - it "removes rendered lines to account for upstream change", -> - editor.scrollToBottom() - expect(editor.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - buffer.change([[1,0], [2,0]], "") - expect(editor.renderedLines.find(".line").length).toBe 6 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(11) - - describe "when the change straddles the first rendered row", -> - it "renders the correct rows", -> - editor.scrollToBottom() - expect(editor.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - buffer.change([[7,0], [11,0]], "1\n2\n") - expect(editor.renderedLines.find(".line").length).toBe 5 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(10) - - describe "when the change straddles the last rendered row", -> - it "renders the correct rows", -> - buffer.change([[2,0], [7,0]], "") - expect(editor.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) - - describe "when the change the follows the last rendered row", -> - it "does not change the rendered lines", -> - buffer.change([[10,0], [12,0]], "") - expect(editor.renderedLines.find(".line").length).toBe 7 - expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) - expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) - - describe "when the last line is removed when the editor is scrolled to the bottom", -> - it "reduces the editor's scrollTop (due to the reduced total scroll height) and renders the correct screen lines", -> - editor.setCursorScreenPosition([Infinity, Infinity]) - editor.insertText('\n\n\n') - editor.scrollToBottom() - - expect(buffer.getLineCount()).toBe 16 - - initialScrollTop = editor.scrollTop() - expect(editor.firstRenderedScreenRow).toBe 9 - expect(editor.lastRenderedScreenRow).toBe 15 - - editor.backspace() - - expect(editor.scrollTop()).toBeLessThan initialScrollTop - expect(editor.firstRenderedScreenRow).toBe 9 - expect(editor.lastRenderedScreenRow).toBe 14 - - expect(editor.find('.line').length).toBe 6 - - editor.backspace() - expect(editor.firstRenderedScreenRow).toBe 9 - expect(editor.lastRenderedScreenRow).toBe 13 - - expect(editor.find('.line').length).toBe 5 - - editor.backspace() - expect(editor.firstRenderedScreenRow).toBe 6 - expect(editor.lastRenderedScreenRow).toBe 12 - - expect(editor.find('.line').length).toBe 7 - - describe "when folding leaves less then a screen worth of text (regression)", -> - it "renders lines properly", -> - editor.lineOverdraw = 1 - editor.attachToDom(heightInLines: 5) - editor.activeEditSession.foldBufferRow(4) - editor.activeEditSession.foldBufferRow(0) - - expect(editor.renderedLines.find('.line').length).toBe 1 - expect(editor.renderedLines.find('.line').text()).toBe buffer.lineForRow(0) - - describe "when folding leaves fewer screen lines than the first rendered screen line (regression)", -> - it "clears all screen lines and does not throw any exceptions", -> - editor.lineOverdraw = 1 - editor.attachToDom(heightInLines: 5) - editor.scrollToBottom() - editor.activeEditSession.foldBufferRow(0) - expect(editor.renderedLines.find('.line').length).toBe 1 - expect(editor.renderedLines.find('.line').text()).toBe buffer.lineForRow(0) - - describe "when autoscrolling at the end of the document", -> - it "renders lines properly", -> - editor.edit(project.openSync('two-hundred.txt')) - editor.attachToDom(heightInLines: 5.5) - - expect(editor.renderedLines.find('.line').length).toBe 8 - - editor.moveCursorToBottom() - - expect(editor.renderedLines.find('.line').length).toBe 8 - - describe "when line has a character that could push it to be too tall (regression)", -> - it "does renders the line at a consistent height", -> - editor.attachToDom() - buffer.insert([0, 0], "–") - expect(editor.find('.line:eq(0)').outerHeight()).toBe editor.find('.line:eq(1)').outerHeight() - - describe "when editor.showInvisibles config is set to true", -> - it "displays spaces, tabs, and newlines using visible non-empty values", -> - editor.setText " a line with tabs\tand spaces " - editor.attachToDom() - - expect(atom.config.get("editor.showInvisibles")).toBeFalsy() - expect(editor.renderedLines.find('.line').text()).toBe " a line with tabs and spaces " - - atom.config.set("editor.showInvisibles", true) - space = editor.invisibles?.space - expect(space).toBeTruthy() - tab = editor.invisibles?.tab - expect(tab).toBeTruthy() - eol = editor.invisibles?.eol - expect(eol).toBeTruthy() - expect(editor.renderedLines.find('.line').text()).toBe "#{space}a line with tabs#{tab} and spaces#{space}#{eol}" - - atom.config.set("editor.showInvisibles", false) - expect(editor.renderedLines.find('.line').text()).toBe " a line with tabs and spaces " - - it "displays newlines as their own token outside of the other tokens scope", -> - editor.setShowInvisibles(true) - editor.attachToDom() - editor.setText "var" - expect(editor.find('.line').html()).toBe 'var¬' - - it "allows invisible glyphs to be customized via the editor.invisibles config", -> - editor.setText(" \t ") - editor.attachToDom() - atom.config.set("editor.showInvisibles", true) - atom.config.set("editor.invisibles", eol: ";", space: "_", tab: "tab") - expect(editor.find(".line:first").text()).toBe "_tab _;" - - it "displays trailing carriage return using a visible non-empty value", -> - editor.setText "a line that ends with a carriage return\r\n" - editor.attachToDom() - - expect(atom.config.get("editor.showInvisibles")).toBeFalsy() - expect(editor.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return" - - atom.config.set("editor.showInvisibles", true) - cr = editor.invisibles?.cr - expect(cr).toBeTruthy() - eol = editor.invisibles?.eol - expect(eol).toBeTruthy() - expect(editor.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return#{cr}#{eol}" - - describe "when wrapping is on", -> beforeEach -> - editSession.setSoftWrap(true) + editor.foldBufferRow(4) + editor.setSelectedBufferRange([[8, 1], [9, 0]]) + cursorMovedHandler = jasmine.createSpy("cursorMovedHandler") + editor.on 'cursor-moved', cursorMovedHandler + + describe "when the position of the cursor changes", -> + it "emits a cursor-moved event", -> + buffer.insert([9, 0], '...') + expect(cursorMovedHandler).toHaveBeenCalledWith( + oldBufferPosition: [9, 0] + oldScreenPosition: [6, 0] + newBufferPosition: [9, 3] + newScreenPosition: [6, 3] + textChanged: true + ) + + describe "when the position of the associated selection's tail changes, but not the cursor's position", -> + it "does not emit a cursor-moved event", -> + buffer.insert([8, 0], '...') + expect(cursorMovedHandler).not.toHaveBeenCalled() + + describe "addCursorAtScreenPosition(screenPosition)", -> + describe "when a cursor already exists at the position", -> + it "returns the existing cursor", -> + cursor1 = editor.addCursorAtScreenPosition([0,2]) + cursor2 = editor.addCursorAtScreenPosition([0,2]) + expect(cursor2.marker).toBe cursor1.marker + + describe "addCursorAtBufferPosition(bufferPosition)", -> + describe "when a cursor already exists at the position", -> + it "returns the existing cursor", -> + cursor1 = editor.addCursorAtBufferPosition([1,4]) + cursor2 = editor.addCursorAtBufferPosition([1,4]) + expect(cursor2.marker).toBe cursor1.marker + + describe "selection", -> + selection = null - it "doesn't show the end of line invisible at the end of lines broken due to wrapping", -> - editor.setText "a line that wraps" - editor.attachToDom() - editor.setWidthInChars(6) - atom.config.set "editor.showInvisibles", true - space = editor.invisibles?.space - expect(space).toBeTruthy() - eol = editor.invisibles?.eol - expect(eol).toBeTruthy() - expect(editor.renderedLines.find('.line:first').text()).toBe "a line#{space}" - expect(editor.renderedLines.find('.line:last').text()).toBe "wraps#{eol}" - - it "displays trailing carriage return using a visible non-empty value", -> - editor.setText "a line that\r\n" - editor.attachToDom() - editor.setWidthInChars(6) - atom.config.set "editor.showInvisibles", true - space = editor.invisibles?.space - expect(space).toBeTruthy() - cr = editor.invisibles?.cr - expect(cr).toBeTruthy() - eol = editor.invisibles?.eol - expect(eol).toBeTruthy() - expect(editor.renderedLines.find('.line:first').text()).toBe "a line#{space}" - expect(editor.renderedLines.find('.line:eq(1)').text()).toBe "that#{cr}#{eol}" - expect(editor.renderedLines.find('.line:last').text()).toBe "#{eol}" - - describe "when editor.showIndentGuide is set to true", -> - it "adds an indent-guide class to each leading whitespace span", -> - editor.attachToDom() - - expect(atom.config.get("editor.showIndentGuide")).toBeFalsy() - atom.config.set("editor.showIndentGuide", true) - expect(editor.showIndentGuide).toBeTruthy() - - expect(editor.renderedLines.find('.line:eq(0) .indent-guide').length).toBe 0 - - expect(editor.renderedLines.find('.line:eq(1) .indent-guide').length).toBe 1 - expect(editor.renderedLines.find('.line:eq(1) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(2) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(2) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(3) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(3) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(4) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(4) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(5) .indent-guide').length).toBe 3 - expect(editor.renderedLines.find('.line:eq(5) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(6) .indent-guide').length).toBe 3 - expect(editor.renderedLines.find('.line:eq(6) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(7) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(7) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(8) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(8) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(9) .indent-guide').length).toBe 1 - expect(editor.renderedLines.find('.line:eq(9) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(11) .indent-guide').length).toBe 1 - expect(editor.renderedLines.find('.line:eq(11) .indent-guide').text()).toBe ' ' - - expect(editor.renderedLines.find('.line:eq(12) .indent-guide').length).toBe 0 - - describe "when the indentation level on a line before an empty line is changed", -> - it "updates the indent guide on the empty line", -> - editor.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - editor.setCursorBufferPosition([9]) - editor.indentSelectedRows() - - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - describe "when the indentation level on a line after an empty line is changed", -> - it "updates the indent guide on the empty line", -> - editor.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - editor.setCursorBufferPosition([11]) - editor.indentSelectedRows() - - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - describe "when a line contains only whitespace", -> - it "displays an indent guide on the line", -> - editor.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - editor.setCursorBufferPosition([10]) - editor.indent() - editor.indent() - expect(editor.getCursorBufferPosition()).toEqual [10, 4] - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - it "uses the highest indent guide level from the next or previous non-empty line", -> - editor.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - editor.setCursorBufferPosition([1, Infinity]) - editor.insertNewline() - expect(editor.getCursorBufferPosition()).toEqual [2, 0] - expect(editor.renderedLines.find('.line:eq(2) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(2) .indent-guide').text()).toBe ' ' - - describe "when the line has leading and trailing whitespace", -> - it "does not display the indent guide in the trailing whitespace", -> - editor.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - editor.insertText("/*\n * \n*/") - expect(editor.renderedLines.find('.line:eq(1) .indent-guide').length).toBe 1 - expect(editor.renderedLines.find('.line:eq(1) .indent-guide')).toHaveClass('leading-whitespace') - - describe "when the line is empty and end of show invisibles are enabled", -> - it "renders the indent guides interleaved with the end of line invisibles", -> - editor.attachToDom() - atom.config.set("editor.showIndentGuide", true) - atom.config.set("editor.showInvisibles", true) - eol = editor.invisibles?.eol - - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe "#{eol} " - expect(editor.renderedLines.find('.line:eq(10) .invisible-character').text()).toBe eol - - editor.setCursorBufferPosition([9]) - editor.indent() - - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 - expect(editor.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe "#{eol} " - expect(editor.renderedLines.find('.line:eq(10) .invisible-character').text()).toBe eol - - describe "when soft-wrap is enabled", -> - beforeEach -> - jasmine.unspy(window, 'setTimeout') - editSession.setSoftWrap(true) - editor.attachToDom() - setEditorHeightInLines(editor, 20) - setEditorWidthInChars(editor, 50) - expect(editor.activeEditSession.getSoftWrapColumn()).toBe 50 - - it "wraps lines that are too long to fit within the editor's width, adjusting cursor positioning accordingly", -> - expect(editor.renderedLines.find('.line').length).toBe 16 - expect(editor.renderedLines.find('.line:eq(3)').text()).toBe " var pivot = items.shift(), current, left = [], " - expect(editor.renderedLines.find('.line:eq(4)').text()).toBe "right = [];" - - editor.setCursorBufferPosition([3, 51], wrapAtSoftNewlines: true) - expect(editor.find('.cursor').offset()).toEqual(editor.renderedLines.find('.line:eq(4)').offset()) - - editor.setCursorBufferPosition([4, 0]) - expect(editor.find('.cursor').offset()).toEqual(editor.renderedLines.find('.line:eq(5)').offset()) - - editor.getSelection().setBufferRange(new Range([6, 30], [6, 55])) - [region1, region2] = editor.getSelectionView().regions - expect(region1.offset().top).toBeCloseTo(editor.renderedLines.find('.line:eq(7)').offset().top) - expect(region2.offset().top).toBeCloseTo(editor.renderedLines.find('.line:eq(8)').offset().top) - - it "handles changes to wrapped lines correctly", -> - buffer.insert([6, 28], '1234567') - expect(editor.renderedLines.find('.line:eq(7)').text()).toBe ' current < pivot ? left1234567.push(current) ' - expect(editor.renderedLines.find('.line:eq(8)').text()).toBe ': right.push(current);' - expect(editor.renderedLines.find('.line:eq(9)').text()).toBe ' }' - - it "changes the max line length and repositions the cursor when the window size changes", -> - editor.setCursorBufferPosition([3, 60]) - setEditorWidthInChars(editor, 40) - expect(editor.renderedLines.find('.line').length).toBe 19 - expect(editor.renderedLines.find('.line:eq(4)').text()).toBe "left = [], right = [];" - expect(editor.renderedLines.find('.line:eq(5)').text()).toBe " while(items.length > 0) {" - expect(editor.bufferPositionForScreenPosition(editor.getCursorScreenPosition())).toEqual [3, 60] - - it "does not wrap the lines of any newly assigned buffers", -> - otherEditSession = project.openSync() - otherEditSession.buffer.setText([1..100].join('')) - editor.edit(otherEditSession) - expect(editor.renderedLines.find('.line').length).toBe(1) - - it "unwraps lines when softwrap is disabled", -> - editor.toggleSoftWrap() - expect(editor.renderedLines.find('.line:eq(3)').text()).toBe ' var pivot = items.shift(), current, left = [], right = [];' - - it "allows the cursor to move down to the last line", -> - _.times editor.getLastScreenRow(), -> editor.moveCursorDown() - expect(editor.getCursorScreenPosition()).toEqual [editor.getLastScreenRow(), 0] - editor.moveCursorDown() - expect(editor.getCursorScreenPosition()).toEqual [editor.getLastScreenRow(), 2] - - it "allows the cursor to move up to a shorter soft wrapped line", -> - editor.setCursorScreenPosition([11, 15]) - editor.moveCursorUp() - expect(editor.getCursorScreenPosition()).toEqual [10, 10] - editor.moveCursorUp() - editor.moveCursorUp() - expect(editor.getCursorScreenPosition()).toEqual [8, 15] - - it "it allows the cursor to wrap when moving horizontally past the beginning / end of a wrapped line", -> - editor.setCursorScreenPosition([11, 0]) - editor.moveCursorLeft() - expect(editor.getCursorScreenPosition()).toEqual [10, 10] - - editor.moveCursorRight() - expect(editor.getCursorScreenPosition()).toEqual [11, 0] - - it "calls .setWidthInChars() when the editor is attached because now its dimensions are available to calculate it", -> - otherEditor = new Editor(editSession: project.openSync('sample.js')) - spyOn(otherEditor, 'setWidthInChars') - - otherEditor.activeEditSession.setSoftWrap(true) - expect(otherEditor.setWidthInChars).not.toHaveBeenCalled() - - otherEditor.simulateDomAttachment() - expect(otherEditor.setWidthInChars).toHaveBeenCalled() - otherEditor.remove() - - describe "when the editor's width changes", -> - it "updates the width in characters on the edit session", -> - previousSoftWrapColumn = editSession.getSoftWrapColumn() - - spyOn(editor, 'setWidthInChars').andCallThrough() - editor.width(editor.width() / 2) - - waitsFor -> - editor.setWidthInChars.callCount > 0 - - runs -> - expect(editSession.getSoftWrapColumn()).toBeLessThan previousSoftWrapColumn - - describe "gutter rendering", -> - beforeEach -> - editor.attachToDom(heightInLines: 5.5) - - it "creates a line number element for each visible line with   padding to the left of the number", -> - expect(editor.gutter.find('.line-number').length).toBe 8 - expect(editor.find('.line-number:first').html()).toBe " 1" - expect(editor.gutter.find('.line-number:last').html()).toBe " 8" - - # here we don't scroll far enough to trigger additional rendering - editor.scrollTop(editor.lineHeight * 1.5) - expect(editor.renderedLines.find('.line').length).toBe 8 - expect(editor.gutter.find('.line-number:first').html()).toBe " 1" - expect(editor.gutter.find('.line-number:last').html()).toBe " 8" - - editor.scrollTop(editor.lineHeight * 3.5) - expect(editor.renderedLines.find('.line').length).toBe 10 - expect(editor.gutter.find('.line-number:first').html()).toBe " 2" - expect(editor.gutter.find('.line-number:last').html()).toBe "11" - - describe "when lines are inserted", -> - it "re-renders the correct line number range in the gutter", -> - editor.scrollTop(3 * editor.lineHeight) - expect(editor.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editor.gutter.find('.line-number:last').intValue()).toBe 11 - - buffer.insert([6, 0], '\n') - - expect(editor.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editor.gutter.find('.line-number:last').intValue()).toBe 11 - - it "re-renders the correct line number range when there are folds", -> - editor.activeEditSession.foldBufferRow(1) - expect(editor.gutter.find('.line-number-1')).toHaveClass 'fold' - - buffer.insert([0, 0], '\n') - - expect(editor.gutter.find('.line-number-2')).toHaveClass 'fold' - - describe "when wrapping is on", -> - it "renders a • instead of line number for wrapped portions of lines", -> - editSession.setSoftWrap(true) - editor.setWidthInChars(50) - expect(editor.gutter.find('.line-number').length).toEqual(8) - expect(editor.gutter.find('.line-number:eq(3)').intValue()).toBe 4 - expect(editor.gutter.find('.line-number:eq(4)').html()).toBe ' â€¢' - expect(editor.gutter.find('.line-number:eq(5)').intValue()).toBe 5 - - describe "when there are folds", -> - 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)').intValue()).toBe 4 - expect(editor.gutter.find('.line-number:eq(4)').intValue()).toBe 7 - - buffer.insert([4,0], "\n\n") - expect(editor.gutter.find('.line-number:eq(3)').intValue()).toBe 4 - expect(editor.gutter.find('.line-number:eq(4)').intValue()).toBe 9 - - buffer.delete([[3,0], [6,0]]) - expect(editor.gutter.find('.line-number:eq(3)').intValue()).toBe 4 - expect(editor.gutter.find('.line-number:eq(4)').intValue()).toBe 6 - - it "redraws gutter numbers when lines are unfolded", -> - setEditorHeightInLines(editor, 20) - fold = editor.createFold(2, 12) - expect(editor.gutter.find('.line-number').length).toBe 3 - - fold.destroy() - expect(editor.gutter.find('.line-number').length).toBe 13 - - it "styles folded line numbers", -> - editor.createFold(3, 5) - expect(editor.gutter.find('.line-number.fold').length).toBe 1 - expect(editor.gutter.find('.line-number.fold:eq(0)').intValue()).toBe 4 - - describe "when the scrollView is scrolled to the right", -> - it "adds a drop shadow to the gutter", -> - editor.attachToDom() - editor.width(100) - - expect(editor.gutter).not.toHaveClass('drop-shadow') - - editor.scrollLeft(10) - editor.scrollView.trigger('scroll') - - expect(editor.gutter).toHaveClass('drop-shadow') - - editor.scrollLeft(0) - editor.scrollView.trigger('scroll') - - expect(editor.gutter).not.toHaveClass('drop-shadow') - - describe "when the editor is scrolled vertically", -> - it "adjusts the padding-top to account for non-rendered line numbers", -> - editor.scrollTop(editor.lineHeight * 3.5) - expect(editor.gutter.lineNumbers.css('padding-top')).toBe "#{editor.lineHeight * 1}px" - expect(editor.gutter.lineNumbers.css('padding-bottom')).toBe "#{editor.lineHeight * 2}px" - expect(editor.renderedLines.find('.line').length).toBe 10 - expect(editor.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editor.gutter.find('.line-number:last').intValue()).toBe 11 - - describe "when the switching from an edit session for a long buffer to an edit session for a short buffer", -> - it "updates the line numbers to reflect the shorter buffer", -> - emptyEditSession = project.openSync(null) - editor.edit(emptyEditSession) - expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 1 - - editor.edit(editSession) - expect(editor.gutter.lineNumbers.find('.line-number').length).toBeGreaterThan 1 - - editor.edit(emptyEditSession) - expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 1 - - describe "when the editor is mini", -> - it "hides the gutter", -> - miniEditor = new Editor(mini: true) - miniEditor.attachToDom() - expect(miniEditor.gutter).toBeHidden() - - it "doesn't highlight the only line", -> - miniEditor = new Editor(mini: true) - miniEditor.attachToDom() - expect(miniEditor.getCursorBufferPosition().row).toBe 0 - expect(miniEditor.find('.line.cursor-line').length).toBe 0 - - it "doesn't show the end of line invisible", -> - atom.config.set "editor.showInvisibles", true - miniEditor = new Editor(mini: true) - miniEditor.attachToDom() - space = miniEditor.invisibles?.space - expect(space).toBeTruthy() - tab = miniEditor.invisibles?.tab - expect(tab).toBeTruthy() - miniEditor.setText(" a line with tabs\tand spaces ") - expect(miniEditor.renderedLines.find('.line').text()).toBe "#{space}a line with tabs#{tab} and spaces#{space}" - - it "doesn't show the indent guide", -> - atom.config.set "editor.showIndentGuide", true - miniEditor = new Editor(mini: true) - miniEditor.attachToDom() - miniEditor.setText(" and indented line") - expect(miniEditor.renderedLines.find('.indent-guide').length).toBe 0 - - - it "lets you set the grammar", -> - miniEditor = new Editor(mini: true) - miniEditor.setText("var something") - previousTokens = miniEditor.lineForScreenRow(0).tokens - miniEditor.setGrammar(atom.syntax.selectGrammar('something.js')) - expect(miniEditor.getGrammar().name).toBe "JavaScript" - expect(previousTokens).not.toEqual miniEditor.lineForScreenRow(0).tokens - - # doesn't allow regular editors to set grammars - expect(-> editor.setGrammar()).toThrow() - - describe "when the editor.showLineNumbers config is false", -> - it "doesn't render any line numbers", -> - expect(editor.gutter.lineNumbers).toBeVisible() - atom.config.set("editor.showLineNumbers", false) - expect(editor.gutter.lineNumbers).not.toBeVisible() - - describe "using gutter's api", -> - it "can get all the line number elements", -> - elements = editor.gutter.getLineNumberElements() - len = editor.gutter.lastScreenRow - editor.gutter.firstScreenRow + 1 - expect(elements).toHaveLength(len) - - it "can get a single line number element", -> - element = editor.gutter.getLineNumberElement(3) - expect(element).toBeTruthy() - - it "returns falsy when there is no line element", -> - expect(editor.gutter.getLineNumberElement(42)).toHaveLength 0 - - it "can add and remove classes to all the line numbers", -> - wasAdded = editor.gutter.addClassToAllLines('heyok') - expect(wasAdded).toBe true - - elements = editor.gutter.getLineNumberElementsForClass('heyok') - expect($(elements)).toHaveClass('heyok') - - editor.gutter.removeClassFromAllLines('heyok') - expect($(editor.gutter.getLineNumberElements())).not.toHaveClass('heyok') - - it "can add and remove classes from a single line number", -> - wasAdded = editor.gutter.addClassToLine(3, 'heyok') - expect(wasAdded).toBe true - - element = editor.gutter.getLineNumberElement(2) - expect($(element)).not.toHaveClass('heyok') - - it "can fetch line numbers by their class", -> - editor.gutter.addClassToLine(1, 'heyok') - editor.gutter.addClassToLine(3, 'heyok') - - elements = editor.gutter.getLineNumberElementsForClass('heyok') - expect(elements.length).toBe 2 - - expect($(elements[0])).toHaveClass 'line-number-1' - expect($(elements[0])).toHaveClass 'heyok' - - expect($(elements[1])).toHaveClass 'line-number-3' - expect($(elements[1])).toHaveClass 'heyok' - - describe "gutter line highlighting", -> - beforeEach -> - editor.attachToDom(heightInLines: 5.5) - - describe "when there is no wrapping", -> - it "highlights the line where the initial cursor position is", -> - expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 - expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 1 - - it "updates the highlighted line when the cursor position changes", -> - editor.setCursorBufferPosition([1,0]) - expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 - expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 2 - - describe "when there is wrapping", -> beforeEach -> - editor.attachToDom(30) - editSession.setSoftWrap(true) - setEditorWidthInChars(editor, 20) + selection = editor.getSelection() - it "highlights the line where the initial cursor position is", -> - expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 - expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 1 + describe ".selectUp/Down/Left/Right()", -> + it "expands each selection to its cursor's new location", -> + editor.setSelectedBufferRanges([[[0,9], [0,13]], [[3,16], [3,21]]]) + [selection1, selection2] = editor.getSelections() - it "updates the highlighted line when the cursor position changes", -> - editor.setCursorBufferPosition([1,0]) - expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 - expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 2 - - describe "when the selection spans multiple lines", -> - beforeEach -> - editor.attachToDom(30) - - it "highlights the foreground of the gutter", -> - editor.getSelection().setBufferRange(new Range([0,0],[2,2])) - expect(editor.getSelection().isSingleScreenLine()).toBe false - expect(editor.find('.line-number.cursor-line').length).toBe 3 - - it "doesn't highlight the background of the gutter", -> - editor.getSelection().setBufferRange(new Range([0,0],[2,0])) - expect(editor.getSelection().isSingleScreenLine()).toBe false - expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 0 - - it "doesn't highlight the last line if it ends at the beginning of a line", -> - editor.getSelection().setBufferRange(new Range([0,0],[1,0])) - expect(editor.getSelection().isSingleScreenLine()).toBe false - expect(editor.find('.line-number.cursor-line').length).toBe 1 - expect(editor.find('.line-number.cursor-line').intValue()).toBe 1 - - it "when a newline is deleted with backspace, the line number of the new cursor position is highlighted", -> - editor.setCursorScreenPosition([1,0]) - editor.backspace() - expect(editor.find('.line-number.cursor-line').length).toBe 1 - expect(editor.find('.line-number.cursor-line').intValue()).toBe 1 - - describe "line highlighting", -> - beforeEach -> - editor.attachToDom(30) - - describe "when there is no wrapping", -> - it "highlights the line where the initial cursor position is", -> - expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editor.find('.line.cursor-line').length).toBe 1 - expect(editor.find('.line.cursor-line').text()).toBe buffer.lineForRow(0) - - it "updates the highlighted line when the cursor position changes", -> - editor.setCursorBufferPosition([1,0]) - expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editor.find('.line.cursor-line').length).toBe 1 - expect(editor.find('.line.cursor-line').text()).toBe buffer.lineForRow(1) - - it "when a newline is deleted with backspace, the line of the new cursor position is highlighted", -> - editor.setCursorScreenPosition([1,0]) - editor.backspace() - expect(editor.find('.line.cursor-line').length).toBe 1 - - describe "when there is wrapping", -> - beforeEach -> - editSession.setSoftWrap(true) - setEditorWidthInChars(editor, 20) - - it "highlights the line where the initial cursor position is", -> - expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editor.find('.line.cursor-line').length).toBe 1 - expect(editor.find('.line.cursor-line').text()).toBe 'var quicksort = ' - - it "updates the highlighted line when the cursor position changes", -> - editor.setCursorBufferPosition([1,0]) - expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editor.find('.line.cursor-line').length).toBe 1 - expect(editor.find('.line.cursor-line').text()).toBe ' var sort = ' - - describe "when there is a non-empty selection", -> - it "does not highlight the line", -> - editor.setSelectedBufferRange([[1, 0], [1, 1]]) - expect(editor.find('.line.cursor-line').length).toBe 0 - - describe "folding", -> - beforeEach -> - editSession = project.openSync('two-hundred.txt') - buffer = editSession.buffer - editor.edit(editSession) - editor.attachToDom() - - describe "when a fold-selection event is triggered", -> - it "folds the lines covered by the selection into a single line with a fold class and marker", -> - editor.getSelection().setBufferRange(new Range([4, 29], [7, 4])) - editor.trigger 'editor:fold-selection' - - expect(editor.renderedLines.find('.line:eq(4)')).toHaveClass('fold') - expect(editor.renderedLines.find('.line:eq(4) > .fold-marker')).toExist() - expect(editor.renderedLines.find('.line:eq(5)').text()).toBe '8' - - expect(editor.getSelection().isEmpty()).toBeTruthy() - expect(editor.getCursorScreenPosition()).toEqual [5, 0] - - it "keeps the gutter line and the editor line the same heights (regression)", -> - editor.getSelection().setBufferRange(new Range([4, 29], [7, 4])) - editor.trigger 'editor:fold-selection' - - expect(editor.gutter.find('.line-number:eq(4)').height()).toBe editor.renderedLines.find('.line:eq(4)').height() - - describe "when a fold placeholder line is clicked", -> - it "removes the associated fold and places the cursor at its beginning", -> - editor.setCursorBufferPosition([3,0]) - editSession.createFold(3, 5) - - foldLine = editor.find('.line.fold') - expect(foldLine).toExist() - foldLine.mousedown() - - expect(editor.find('.fold')).not.toExist() - expect(editor.find('.fold-marker')).not.toExist() - expect(editor.renderedLines.find('.line:eq(4)').text()).toMatch /4-+/ - expect(editor.renderedLines.find('.line:eq(5)').text()).toMatch /5/ - - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - - describe "when the unfold-current-row 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.setCursorBufferPosition([3,0]) - editor.trigger 'editor:fold-current-row' - - editor.setCursorBufferPosition([3,0]) - editor.trigger 'editor:unfold-current-row' - - expect(editor.find('.fold')).not.toExist() - expect(editor.renderedLines.find('.line:eq(4)').text()).toMatch /4-+/ - expect(editor.renderedLines.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 'fold-selected' class to the fold's line element and hides the cursor if it is on the fold line", -> - editor.createFold(2, 4) - - editor.setSelectedBufferRange([[1, 0], [2, 0]], preserveFolds: true, isReversed: true) - expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') - - editor.setSelectedBufferRange([[1, 0], [1, 1]], preserveFolds: true) - expect(editor.lineElementForScreenRow(2)).not.toMatchSelector('.fold.fold-selected') - - editor.setSelectedBufferRange([[1, 0], [5, 0]], preserveFolds: true) - expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') - - editor.setCursorScreenPosition([3,0]) - expect(editor.lineElementForScreenRow(2)).not.toMatchSelector('.fold.fold-selected') - - editor.setCursorScreenPosition([2,0]) - expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') - expect(editor.find('.cursor')).toBeHidden() - - editor.setCursorScreenPosition([3,0]) - expect(editor.find('.cursor')).toBeVisible() - - 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 'fold-selected' class", -> - setEditorHeightInLines(editor, 5) - editor.resetDisplay() - - editor.createFold(2, 4) - editor.setSelectedBufferRange([[1, 0], [5, 0]], preserveFolds: true) - expect(editor.renderedLines.find('.fold.fold-selected')).toExist() - - editor.scrollToBottom() - expect(editor.renderedLines.find('.fold.fold-selected')).not.toExist() - - editor.scrollTop(0) - expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') - - describe "paging up and down", -> - beforeEach -> - editor.attachToDom() - - it "moves to the last line when page down is repeated from the first line", -> - rows = editor.getLineCount() - 1 - expect(rows).toBeGreaterThan(0) - row = editor.getCursor().getScreenPosition().row - expect(row).toBe(0) - while row < rows - editor.pageDown() - newRow = editor.getCursor().getScreenPosition().row - expect(newRow).toBeGreaterThan(row) - if (newRow <= row) - break - row = newRow - expect(row).toBe(rows) - expect(editor.getLastVisibleScreenRow()).toBe(rows) - - it "moves to the first line when page up is repeated from the last line", -> - editor.moveCursorToBottom() - row = editor.getCursor().getScreenPosition().row - expect(row).toBeGreaterThan(0) - while row > 0 - editor.pageUp() - newRow = editor.getCursor().getScreenPosition().row - expect(newRow).toBeLessThan(row) - if (newRow >= row) - break - row = newRow - expect(row).toBe(0) - expect(editor.getFirstVisibleScreenRow()).toBe(0) - - it "resets to original position when down is followed by up", -> - expect(editor.getCursor().getScreenPosition().row).toBe(0) - editor.pageDown() - expect(editor.getCursor().getScreenPosition().row).toBeGreaterThan(0) - editor.pageUp() - expect(editor.getCursor().getScreenPosition().row).toBe(0) - expect(editor.getFirstVisibleScreenRow()).toBe(0) - - describe ".checkoutHead()", -> - [filePath, originalPathText] = [] - - beforeEach -> - filePath = project.resolve('git/working-dir/file.txt') - originalPathText = fs.readFileSync(filePath, 'utf8') - editor.edit(project.openSync(filePath)) - - afterEach -> - fs.writeFileSync(filePath, originalPathText) - - it "restores the contents of the editor to the HEAD revision", -> - editor.setText('') - editor.getBuffer().save() - - fileChangeHandler = jasmine.createSpy('fileChange') - editor.getBuffer().file.on 'contents-changed', fileChangeHandler - - editor.checkoutHead() - - waitsFor "file to trigger contents-changed event", -> - fileChangeHandler.callCount > 0 - - runs -> - expect(editor.getText()).toBe(originalPathText) - - describe ".pixelPositionForBufferPosition(position)", -> - describe "when the editor is detached", -> - it "returns top and left values of 0", -> - expect(editor.isOnDom()).toBeFalsy() - expect(editor.pixelPositionForBufferPosition([2,7])).toEqual top: 0, left: 0 - - describe "when the editor is invisible", -> - it "returns top and left values of 0", -> - editor.attachToDom() - editor.hide() - expect(editor.isVisible()).toBeFalsy() - expect(editor.pixelPositionForBufferPosition([2,7])).toEqual top: 0, left: 0 - - describe "when the editor is attached and visible", -> - beforeEach -> - editor.attachToDom() - - it "returns the top and left pixel positions", -> - expect(editor.pixelPositionForBufferPosition([2,7])).toEqual top: 40, left: 70 - - it "caches the left position", -> - editor.renderedLines.css('font-size', '16px') - expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 - - # make characters smaller - editor.renderedLines.css('font-size', '15px') - - expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 - - describe "when clicking in the gutter", -> - beforeEach -> - editor.attachToDom() - - describe "when single clicking", -> - it "moves the cursor to the start of the selected line", -> - expect(editor.getCursorScreenPosition()).toEqual [0,0] - event = $.Event("mousedown") - event.pageY = editor.gutter.find(".line-number:eq(1)").offset().top - event.originalEvent = {detail: 1} - editor.gutter.find(".line-number:eq(1)").trigger event - expect(editor.getCursorScreenPosition()).toEqual [1,0] - - describe "when shift-clicking", -> - it "selects to the start of the selected line", -> - expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [0,0]] - event = $.Event("mousedown") - event.pageY = editor.gutter.find(".line-number:eq(1)").offset().top - event.originalEvent = {detail: 1} - event.shiftKey = true - editor.gutter.find(".line-number:eq(1)").trigger event - expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [2,0]] - - describe "when mousing down and then moving across multiple lines before mousing up", -> - describe "when selecting from top to bottom", -> - it "selects the lines", -> - mousedownEvent = $.Event("mousedown") - mousedownEvent.pageY = editor.gutter.find(".line-number:eq(1)").offset().top - mousedownEvent.originalEvent = {detail: 1} - editor.gutter.find(".line-number:eq(1)").trigger mousedownEvent - - mousemoveEvent = $.Event("mousemove") - mousemoveEvent.pageY = editor.gutter.find(".line-number:eq(5)").offset().top - mousemoveEvent.originalEvent = {detail: 1} - editor.gutter.find(".line-number:eq(5)").trigger mousemoveEvent - - $(document).trigger 'mouseup' - - expect(editor.getSelection().getScreenRange()).toEqual [[1,0], [6,0]] - - describe "when selecting from bottom to top", -> - it "selects the lines", -> - mousedownEvent = $.Event("mousedown") - mousedownEvent.pageY = editor.gutter.find(".line-number:eq(5)").offset().top - mousedownEvent.originalEvent = {detail: 1} - editor.gutter.find(".line-number:eq(5)").trigger mousedownEvent - - mousemoveEvent = $.Event("mousemove") - mousemoveEvent.pageY = editor.gutter.find(".line-number:eq(1)").offset().top - mousemoveEvent.originalEvent = {detail: 1} - editor.gutter.find(".line-number:eq(1)").trigger mousemoveEvent - - $(document).trigger 'mouseup' - - expect(editor.getSelection().getScreenRange()).toEqual [[1,0], [6,0]] - - describe "when clicking below the last line", -> - beforeEach -> - editor.attachToDom() - - it "move the cursor to the end of the file", -> - expect(editor.getCursorScreenPosition()).toEqual [0,0] - event = mousedownEvent(editor: editor, point: [Infinity, 10]) - editor.underlayer.trigger event - expect(editor.getCursorScreenPosition()).toEqual [12,2] - - it "selects to the end of the files when shift is pressed", -> - expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [0,0]] - event = mousedownEvent(editor: editor, point: [Infinity, 10], shiftKey: true) - editor.underlayer.trigger event - expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [12,2]] - - describe ".reloadGrammar()", -> - [filePath] = [] - - beforeEach -> - tmpdir = fs.absolute(temp.dir) - filePath = path.join(tmpdir, "grammar-change.txt") - fs.writeFileSync(filePath, "var i;") - - afterEach -> - fs.removeSync(filePath) if fs.existsSync(filePath) - - it "updates all the rendered lines when the grammar changes", -> - editor.edit(project.openSync(filePath)) - expect(editor.getGrammar().name).toBe 'Plain Text' - atom.syntax.setGrammarOverrideForPath(filePath, 'source.js') - editor.reloadGrammar() - expect(editor.getGrammar().name).toBe 'JavaScript' - - tokenizedBuffer = editor.activeEditSession.displayBuffer.tokenizedBuffer - line0 = tokenizedBuffer.lineForScreenRow(0) - expect(line0.tokens.length).toBe 3 - expect(line0.tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.modifier.js']) - - it "doesn't update the rendered lines when the grammar doesn't change", -> - expect(editor.getGrammar().name).toBe 'JavaScript' - spyOn(editor, 'updateDisplay').andCallThrough() - editor.reloadGrammar() - expect(editor.reloadGrammar()).toBeFalsy() - expect(editor.updateDisplay).not.toHaveBeenCalled() - expect(editor.getGrammar().name).toBe 'JavaScript' - - it "emits an editor:grammar-changed event when updated", -> - editor.edit(project.openSync(filePath)) - - eventHandler = jasmine.createSpy('eventHandler') - editor.on('editor:grammar-changed', eventHandler) - editor.reloadGrammar() - - expect(eventHandler).not.toHaveBeenCalled() - - atom.syntax.setGrammarOverrideForPath(filePath, 'source.js') - editor.reloadGrammar() - expect(eventHandler).toHaveBeenCalled() - - describe ".replaceSelectedText()", -> - it "doesn't call the replace function when the selection is empty", -> - replaced = false - edited = false - replacer = (text) -> - replaced = true - 'new' - - editor.moveCursorToTop() - edited = editor.replaceSelectedText(replacer) - expect(replaced).toBe false - expect(edited).toBe false - - it "returns true when transformed text is non-empty", -> - replaced = false - edited = false - replacer = (text) -> - replaced = true - 'new' - - editor.moveCursorToTop() - editor.selectToEndOfLine() - edited = editor.replaceSelectedText(replacer) - expect(replaced).toBe true - expect(edited).toBe true - - it "returns false when transformed text is null", -> - replaced = false - edited = false - replacer = (text) -> - replaced = true - null - - editor.moveCursorToTop() - editor.selectToEndOfLine() - edited = editor.replaceSelectedText(replacer) - expect(replaced).toBe true - expect(edited).toBe false - - it "returns false when transformed text is undefined", -> - replaced = false - edited = false - replacer = (text) -> - replaced = true - undefined - - editor.moveCursorToTop() - editor.selectToEndOfLine() - edited = editor.replaceSelectedText(replacer) - expect(replaced).toBe true - expect(edited).toBe false - - describe "when editor:copy-path is triggered", -> - it "copies the absolute path to the editor's file to the pasteboard", -> - editor.trigger 'editor:copy-path' - expect(atom.pasteboard.read()[0]).toBe editor.getPath() - - describe "when editor:move-line-up is triggered", -> - describe "when there is no selection", -> - it "moves the line where the cursor is up", -> - editor.setCursorBufferPosition([1,0]) - editor.trigger 'editor:move-line-up' - expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' - expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' - - it "moves the cursor to the new row and the same column", -> - editor.setCursorBufferPosition([1,2]) - editor.trigger 'editor:move-line-up' - expect(editor.getCursorBufferPosition()).toEqual [0,2] - - describe "where there is a selection", -> - describe "when the selection falls inside the line", -> - it "maintains the selection", -> - editor.setSelectedBufferRange([[1, 2], [1, 5]]) - expect(editor.getSelectedText()).toBe 'var' - editor.trigger 'editor:move-line-up' - expect(editor.getSelectedBufferRange()).toEqual [[0, 2], [0, 5]] - expect(editor.getSelectedText()).toBe 'var' - - describe "where there are multiple lines selected", -> - it "moves the selected lines up", -> - editor.setSelectedBufferRange([[2, 0], [3, Infinity]]) - editor.trigger 'editor:move-line-up' - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(3)).toBe ' var sort = function(items) {' - - it "maintains the selection", -> - editor.setSelectedBufferRange([[2, 0], [3, 62]]) - editor.trigger 'editor:move-line-up' - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [2, 62]] - - describe "when the last line is selected", -> - it "moves the selected line up", -> - editor.setSelectedBufferRange([[12, 0], [12, Infinity]]) - editor.trigger 'editor:move-line-up' - expect(buffer.lineForRow(11)).toBe '};' - expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));' - - describe "when the last two lines are selected", -> - it "moves the selected lines up", -> - editor.setSelectedBufferRange([[11, 0], [12, Infinity]]) - editor.trigger 'editor:move-line-up' - expect(buffer.lineForRow(10)).toBe ' return sort(Array.apply(this, arguments));' - expect(buffer.lineForRow(11)).toBe '};' - expect(buffer.lineForRow(12)).toBe '' - - describe "when the cursor is on the first line", -> - it "does not move the line", -> - editor.setCursorBufferPosition([0,0]) - originalText = editor.getText() - editor.trigger 'editor:move-line-up' - expect(editor.getText()).toBe originalText - - describe "when the cursor is on the trailing newline", -> - it "does not move the line", -> - editor.moveCursorToBottom() - editor.insertNewline() - editor.moveCursorToBottom() - originalText = editor.getText() - editor.trigger 'editor:move-line-up' - expect(editor.getText()).toBe originalText - - describe "when the cursor is on a folded line", -> - it "moves all lines in the fold up and preserves the fold", -> - editor.setCursorBufferPosition([4, 0]) - editor.foldCurrentRow() - editor.trigger 'editor:move-line-up' - expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' - expect(buffer.lineForRow(7)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(editor.getSelectedBufferRange()).toEqual [[3, 0], [3, 0]] - expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() - - describe "when the selection contains a folded and unfolded line", -> - it "moves the selected lines up and preserves the fold", -> - editor.setCursorBufferPosition([4, 0]) - editor.foldCurrentRow() - editor.setCursorBufferPosition([3, 4]) - editor.selectDown() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - editor.trigger 'editor:move-line-up' - expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' - expect(editor.getSelectedBufferRange()).toEqual [[2, 4], [3, 0]] - expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() - - describe "when an entire line is selected including the newline", -> - it "moves the selected line up", -> - editor.setCursorBufferPosition([1]) - editor.selectToEndOfLine() - editor.selectRight() - editor.trigger 'editor:move-line-up' - expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' - expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' - - describe "when editor:move-line-down is triggered", -> - describe "when there is no selection", -> - it "moves the line where the cursor is down", -> - editor.setCursorBufferPosition([0, 0]) - editor.trigger 'editor:move-line-down' - expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' - expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' - - it "moves the cursor to the new row and the same column", -> - editor.setCursorBufferPosition([0, 2]) - editor.trigger 'editor:move-line-down' - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - - describe "when the cursor is on the last line", -> - it "does not move the line", -> - editor.moveCursorToBottom() - editor.trigger 'editor:move-line-down' - expect(buffer.lineForRow(12)).toBe '};' - expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]] - - describe "when the cursor is on the second to last line", -> - it "moves the line down", -> - editor.setCursorBufferPosition([11, 0]) - editor.trigger 'editor:move-line-down' - expect(buffer.lineForRow(11)).toBe '};' - expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));' - expect(buffer.lineForRow(13)).toBeUndefined() - - describe "when the cursor is on the second to last line and the last line is empty", -> - it "does not move the line", -> - editor.moveCursorToBottom() - editor.insertNewline() - editor.setCursorBufferPosition([12, 2]) - editor.trigger 'editor:move-line-down' - expect(buffer.lineForRow(12)).toBe '};' - expect(buffer.lineForRow(13)).toBe '' - expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]] - - describe "where there is a selection", -> - describe "when the selection falls inside the line", -> - it "maintains the selection", -> - editor.setSelectedBufferRange([[1, 2], [1, 5]]) - expect(editor.getSelectedText()).toBe 'var' - editor.trigger 'editor:move-line-down' - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 5]] - expect(editor.getSelectedText()).toBe 'var' - - describe "where there are multiple lines selected", -> - it "moves the selected lines down", -> - editor.setSelectedBufferRange([[2, 0], [3, Infinity]]) - editor.trigger 'editor:move-line-down' - expect(buffer.lineForRow(2)).toBe ' while(items.length > 0) {' - expect(buffer.lineForRow(3)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(5)).toBe ' current = items.shift();' - - it "maintains the selection", -> - editor.setSelectedBufferRange([[2, 0], [3, 62]]) - editor.trigger 'editor:move-line-down' - expect(editor.getSelectedBufferRange()).toEqual [[3, 0], [4, 62]] - - describe "when the cursor is on a folded line", -> - it "moves all lines in the fold down and preserves the fold", -> - editor.setCursorBufferPosition([4, 0]) - editor.foldCurrentRow() - editor.trigger 'editor:move-line-down' - expect(buffer.lineForRow(4)).toBe ' return sort(left).concat(pivot).concat(sort(right));' - expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {' - expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 0]] - expect(editor.isFoldedAtScreenRow(5)).toBeTruthy() - - describe "when the selection contains a folded and unfolded line", -> - it "moves the selected lines down and preserves the fold", -> - editor.setCursorBufferPosition([4, 0]) - editor.foldCurrentRow() - editor.setCursorBufferPosition([3, 4]) - editor.selectDown() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - editor.trigger 'editor:move-line-down' - expect(buffer.lineForRow(3)).toBe ' return sort(left).concat(pivot).concat(sort(right));' - expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {' - expect(editor.getSelectedBufferRange()).toEqual [[4, 4], [5, 0]] - expect(editor.isFoldedAtScreenRow(5)).toBeTruthy() - - describe "when an entire line is selected including the newline", -> - it "moves the selected line down", -> - editor.setCursorBufferPosition([1]) - editor.selectToEndOfLine() editor.selectRight() - editor.trigger 'editor:move-line-down' - expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(2)).toBe ' var sort = function(items) {' + expect(selection1.getBufferRange()).toEqual [[0,9], [0,14]] + expect(selection2.getBufferRange()).toEqual [[3,16], [3,22]] - describe "when editor:duplicate-line is triggered", -> - describe "where there is no selection", -> - describe "when the cursor isn't on a folded line", -> - it "duplicates the current line below and moves the cursor down one row", -> - editor.setCursorBufferPosition([0, 5]) - editor.trigger 'editor:duplicate-line' - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' - expect(editor.getCursorBufferPosition()).toEqual [1, 5] + editor.selectLeft() + editor.selectLeft() + expect(selection1.getBufferRange()).toEqual [[0,9], [0,12]] + expect(selection2.getBufferRange()).toEqual [[3,16], [3,20]] - describe "when the cursor is on a folded line", -> - it "duplicates the entire fold before and moves the cursor to the new fold", -> + editor.selectDown() + expect(selection1.getBufferRange()).toEqual [[0,9], [1,12]] + expect(selection2.getBufferRange()).toEqual [[3,16], [4,20]] + + editor.selectUp() + expect(selection1.getBufferRange()).toEqual [[0,9], [0,12]] + expect(selection2.getBufferRange()).toEqual [[3,16], [3,20]] + + it "merges selections when they intersect when moving down", -> + editor.setSelectedBufferRanges([[[0,9], [0,13]], [[1,10], [1,20]], [[2,15], [3,25]]]) + [selection1, selection2, selection3] = editor.getSelections() + + editor.selectDown() + expect(editor.getSelections()).toEqual [selection1] + expect(selection1.getScreenRange()).toEqual([[0, 9], [4, 25]]) + expect(selection1.isReversed()).toBeFalsy() + + it "merges selections when they intersect when moving up", -> + editor.setSelectedBufferRanges([[[0,9], [0,13]], [[1,10], [1,20]]], isReversed: true) + [selection1, selection2] = editor.getSelections() + + editor.selectUp() + + expect(editor.getSelections().length).toBe 1 + expect(editor.getSelections()).toEqual [selection1] + expect(selection1.getScreenRange()).toEqual([[0, 0], [1, 20]]) + expect(selection1.isReversed()).toBeTruthy() + + it "merges selections when they intersect when moving left", -> + editor.setSelectedBufferRanges([[[0,9], [0,13]], [[0,14], [1,20]]], isReversed: true) + [selection1, selection2] = editor.getSelections() + + editor.selectLeft() + expect(editor.getSelections()).toEqual [selection1] + expect(selection1.getScreenRange()).toEqual([[0, 8], [1, 20]]) + expect(selection1.isReversed()).toBeTruthy() + + it "merges selections when they intersect when moving right", -> + editor.setSelectedBufferRanges([[[0,9], [0,13]], [[0,14], [1,20]]]) + [selection1, selection2] = editor.getSelections() + + editor.selectRight() + expect(editor.getSelections()).toEqual [selection1] + expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]) + expect(selection1.isReversed()).toBeFalsy() + + describe ".selectToScreenPosition(screenPosition)", -> + it "expands the last selection to the given position", -> + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + selections = editor.getSelections() + expect(selections.length).toBe 2 + [selection1, selection2] = selections + expect(selection1.getScreenRange()).toEqual [[3, 0], [4, 5]] + expect(selection2.getScreenRange()).toEqual [[5, 6], [6, 2]] + + it "merges selections if they intersect, maintaining the directionality of the last selection", -> + editor.setCursorScreenPosition([4, 10]) + editor.selectToScreenPosition([5, 27]) + editor.addCursorAtScreenPosition([3, 10]) + editor.selectToScreenPosition([6, 27]) + + selections = editor.getSelections() + expect(selections.length).toBe 1 + [selection1] = selections + expect(selection1.getScreenRange()).toEqual [[3, 10], [6, 27]] + expect(selection1.isReversed()).toBeFalsy() + + editor.addCursorAtScreenPosition([7, 4]) + editor.selectToScreenPosition([4, 11]) + + selections = editor.getSelections() + expect(selections.length).toBe 1 + [selection1] = selections + expect(selection1.getScreenRange()).toEqual [[3, 10], [7, 4]] + expect(selection1.isReversed()).toBeTruthy() + + describe ".selectToTop()", -> + it "selects text from cusor position to the top of the buffer", -> + editor.setCursorScreenPosition [11,2] + editor.addCursorAtScreenPosition [10,0] + editor.selectToTop() + expect(editor.getCursors().length).toBe 1 + expect(editor.getCursorBufferPosition()).toEqual [0,0] + expect(editor.getSelection().getBufferRange()).toEqual [[0,0], [11,2]] + expect(editor.getSelection().isReversed()).toBeTruthy() + + describe ".selectToBottom()", -> + it "selects text from cusor position to the bottom of the buffer", -> + editor.setCursorScreenPosition [10,0] + editor.addCursorAtScreenPosition [9,3] + editor.selectToBottom() + expect(editor.getCursors().length).toBe 1 + expect(editor.getCursorBufferPosition()).toEqual [12,2] + expect(editor.getSelection().getBufferRange()).toEqual [[9,3], [12,2]] + expect(editor.getSelection().isReversed()).toBeFalsy() + + describe ".selectAll()", -> + it "selects the entire buffer", -> + editor.selectAll() + expect(editor.getSelection().getBufferRange()).toEqual buffer.getRange() + + describe ".selectToBeginningOfLine()", -> + it "selects text from cusor position to beginning of line", -> + editor.setCursorScreenPosition [12,2] + editor.addCursorAtScreenPosition [11,3] + + editor.selectToBeginningOfLine() + + expect(editor.getCursors().length).toBe 2 + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [12,0] + expect(cursor2.getBufferPosition()).toEqual [11,0] + + expect(editor.getSelections().length).toBe 2 + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual [[12,0], [12,2]] + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual [[11,0], [11,3]] + expect(selection2.isReversed()).toBeTruthy() + + describe ".selectToEndOfLine()", -> + it "selects text from cusor position to end of line", -> + editor.setCursorScreenPosition [12,0] + editor.addCursorAtScreenPosition [11,3] + + editor.selectToEndOfLine() + + expect(editor.getCursors().length).toBe 2 + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [12,2] + expect(cursor2.getBufferPosition()).toEqual [11,44] + + expect(editor.getSelections().length).toBe 2 + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual [[12,0], [12,2]] + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual [[11,3], [11,44]] + expect(selection2.isReversed()).toBeFalsy() + + describe ".selectLine()", -> + it "selects the entire line (including newlines) at given row", -> + editor.setCursorScreenPosition([1, 2]) + editor.selectLine() + expect(editor.getSelectedBufferRange()).toEqual [[1,0], [2,0]] + expect(editor.getSelectedText()).toBe " var sort = function(items) {\n" + + editor.setCursorScreenPosition([12, 2]) + editor.selectLine() + expect(editor.getSelectedBufferRange()).toEqual [[12,0], [12,2]] + + describe ".selectToBeginningOfWord()", -> + it "selects text from cusor position to beginning of word", -> + editor.setCursorScreenPosition [0,13] + editor.addCursorAtScreenPosition [3,49] + + editor.selectToBeginningOfWord() + + expect(editor.getCursors().length).toBe 2 + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [0,4] + expect(cursor2.getBufferPosition()).toEqual [3,47] + + expect(editor.getSelections().length).toBe 2 + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual [[0,4], [0,13]] + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual [[3,47], [3,49]] + expect(selection2.isReversed()).toBeTruthy() + + describe ".selectToEndOfWord()", -> + it "selects text from cusor position to end of word", -> + editor.setCursorScreenPosition [0,4] + editor.addCursorAtScreenPosition [3,48] + + editor.selectToEndOfWord() + + expect(editor.getCursors().length).toBe 2 + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [0,13] + expect(cursor2.getBufferPosition()).toEqual [3,50] + + expect(editor.getSelections().length).toBe 2 + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual [[0,4], [0,13]] + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual [[3,48], [3,50]] + expect(selection2.isReversed()).toBeFalsy() + + describe ".selectToBeginningOfNextWord()", -> + it "selects text from cusor position to beginning of next word", -> + editor.setCursorScreenPosition [0,4] + editor.addCursorAtScreenPosition [3,48] + + editor.selectToBeginningOfNextWord() + + expect(editor.getCursors().length).toBe 2 + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [0,14] + expect(cursor2.getBufferPosition()).toEqual [3,51] + + expect(editor.getSelections().length).toBe 2 + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual [[0,4], [0,14]] + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual [[3,48], [3,51]] + expect(selection2.isReversed()).toBeFalsy() + + describe ".selectToPreviousWordBoundary()", -> + it "select to the previous word boundary", -> + editor.setCursorBufferPosition [0, 8] + editor.addCursorAtBufferPosition [2, 0] + editor.addCursorAtBufferPosition [3, 4] + editor.addCursorAtBufferPosition [3, 14] + + editor.selectToPreviousWordBoundary() + + expect(editor.getSelections().length).toBe 4 + [selection1, selection2, selection3, selection4] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual [[0,8], [0,4]] + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual [[2,0], [1,30]] + expect(selection2.isReversed()).toBeTruthy() + expect(selection3.getBufferRange()).toEqual [[3,4], [3,0]] + expect(selection3.isReversed()).toBeTruthy() + expect(selection4.getBufferRange()).toEqual [[3,14], [3,13]] + expect(selection4.isReversed()).toBeTruthy() + + describe ".selectToNextWordBoundary()", -> + it "select to the next word boundary", -> + editor.setCursorBufferPosition [0, 8] + editor.addCursorAtBufferPosition [2, 40] + editor.addCursorAtBufferPosition [4, 0] + editor.addCursorAtBufferPosition [3, 30] + + editor.selectToNextWordBoundary() + + expect(editor.getSelections().length).toBe 4 + [selection1, selection2, selection3, selection4] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual [[0,8], [0,13]] + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual [[2,40], [3,0]] + expect(selection2.isReversed()).toBeFalsy() + expect(selection3.getBufferRange()).toEqual [[4,0], [4,4]] + expect(selection3.isReversed()).toBeFalsy() + expect(selection4.getBufferRange()).toEqual [[3,30], [3,31]] + expect(selection4.isReversed()).toBeFalsy() + + describe ".selectWord()", -> + describe "when the cursor is inside a word", -> + it "selects the entire word", -> + editor.setCursorScreenPosition([0, 8]) + editor.selectWord() + expect(editor.getSelectedText()).toBe 'quicksort' + + describe "when the cursor is between two words", -> + it "selects the word the cursor is on", -> + editor.setCursorScreenPosition([0, 4]) + editor.selectWord() + expect(editor.getSelectedText()).toBe 'quicksort' + + editor.setCursorScreenPosition([0, 3]) + editor.selectWord() + expect(editor.getSelectedText()).toBe 'var' + + + describe "when the cursor is inside a region of whitespace", -> + it "selects the whitespace region", -> + editor.setCursorScreenPosition([5, 2]) + editor.selectWord() + expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] + + editor.setCursorScreenPosition([5, 0]) + editor.selectWord() + expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] + + describe "when the cursor is at the end of the text", -> + it "select the previous word", -> + editor.buffer.append 'word' + editor.moveCursorToBottom() + editor.selectWord() + expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]] + + describe ".selectToFirstCharacterOfLine()", -> + it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> + editor.setCursorScreenPosition [0,5] + editor.addCursorAtScreenPosition [1,7] + + editor.selectToFirstCharacterOfLine() + + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [0,0] + expect(cursor2.getBufferPosition()).toEqual [1,2] + + expect(editor.getSelections().length).toBe 2 + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual [[0,0], [0,5]] + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual [[1,2], [1,7]] + expect(selection2.isReversed()).toBeTruthy() + + editor.selectToFirstCharacterOfLine() + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual [[0,0], [0,5]] + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual [[1,0], [1,7]] + expect(selection2.isReversed()).toBeTruthy() + + describe ".setSelectedBufferRanges(ranges)", -> + it "clears existing selections and creates selections for each of the given ranges", -> + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[4, 4], [5, 5]]] + + editor.setSelectedBufferRanges([[[5, 5], [6, 6]]]) + expect(editor.getSelectedBufferRanges()).toEqual [[[5, 5], [6, 6]]] + + it "merges intersecting selections", -> + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] + + it "recyles existing selection instances", -> + selection = editor.getSelection() + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + + [selection1, selection2] = editor.getSelections() + expect(selection1).toBe selection + expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]] + + describe "when the preserveFolds option is false (the default)", -> + it "removes folds that contain the selections", -> + editor.setSelectedBufferRange([[0,0], [0,0]]) + editor.createFold(1, 4) + editor.createFold(2, 3) + editor.createFold(6, 8) + editor.createFold(10, 11) + + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) + expect(editor.lineForScreenRow(1).fold).toBeUndefined() + expect(editor.lineForScreenRow(2).fold).toBeUndefined() + expect(editor.lineForScreenRow(6).fold).toBeUndefined() + expect(editor.lineForScreenRow(10).fold).toBeDefined() + + describe "when the preserve folds option is true", -> + it "does not remove folds that contain the selections", -> + editor.setSelectedBufferRange([[0,0], [0,0]]) + editor.createFold(1, 4) + editor.createFold(6, 8) + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], preserveFolds: true) + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + + describe ".selectMarker(marker)", -> + describe "if the marker is valid", -> + it "selects the marker's range and returns the selected range", -> + marker = editor.markBufferRange([[0, 1], [3, 3]]) + expect(editor.selectMarker(marker)).toEqual [[0, 1], [3, 3]] + expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 3]] + + describe "if the marker is invalid", -> + it "does not change the selection and returns a falsy value", -> + marker = editor.markBufferRange([[0, 1], [3, 3]]) + marker.destroy() + expect(editor.selectMarker(marker)).toBeFalsy() + expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 0]] + + describe ".addSelectionBelow()", -> + describe "when the selection is non-empty", -> + it "selects the same region of the line below current selections if possible", -> + editor.setSelectedBufferRange([[3, 16], [3, 21]]) + editor.addSelectionForBufferRange([[3, 25], [3, 34]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 16], [3, 21]] + [[3, 25], [3, 34]] + [[4, 16], [4, 21]] + [[4, 25], [4, 29]] + ] + for cursor in editor.getCursors() + expect(cursor.isVisible()).toBeFalsy() + + it "skips lines that are too short to create a non-empty selection", -> + editor.setSelectedBufferRange([[3, 31], [3, 38]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 31], [3, 38]] + [[6, 31], [6, 38]] + ] + + it "honors the original selection's range (goal range) when adding across shorter lines", -> + editor.setSelectedBufferRange([[3, 22], [3, 38]]) + editor.addSelectionBelow() + editor.addSelectionBelow() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 38]] + [[4, 22], [4, 29]] + [[5, 22], [5, 30]] + [[6, 22], [6, 38]] + ] + + it "clears selection goal ranges when the selection changes", -> + editor.setSelectedBufferRange([[3, 22], [3, 38]]) + editor.addSelectionBelow() + editor.selectLeft() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 37]] + [[4, 22], [4, 29]] + [[5, 22], [5, 28]] + ] + + # goal range from previous add selection is honored next time + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 37]] + [[4, 22], [4, 29]] + [[5, 22], [5, 30]] # select to end of line 5 because line 4's goal range was reset by line 3 previously + [[6, 22], [6, 28]] + ] + + describe "when the selection is empty", -> + it "does not skip lines that are shorter than the current column", -> + editor.setCursorBufferPosition([3, 36]) + editor.addSelectionBelow() + editor.addSelectionBelow() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 36], [3, 36]] + [[4, 29], [4, 29]] + [[5, 30], [5, 30]] + [[6, 36], [6, 36]] + ] + + it "skips empty lines when the column is non-zero", -> + editor.setCursorBufferPosition([9, 4]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[9, 4], [9, 4]] + [[11, 4], [11, 4]] + ] + + it "does not skip empty lines when the column is zero", -> + editor.setCursorBufferPosition([9, 0]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[9, 0], [9, 0]] + [[10, 0], [10, 0]] + ] + + describe ".addSelectionAbove()", -> + describe "when the selection is non-empty", -> + it "selects the same region of the line above current selections if possible", -> + editor.setSelectedBufferRange([[3, 16], [3, 21]]) + editor.addSelectionForBufferRange([[3, 37], [3, 44]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[2, 16], [2, 21]] + [[2, 37], [2, 40]] + [[3, 16], [3, 21]] + [[3, 37], [3, 44]] + ] + for cursor in editor.getCursors() + expect(cursor.isVisible()).toBeFalsy() + + it "skips lines that are too short to create a non-empty selection", -> + editor.setSelectedBufferRange([[6, 31], [6, 38]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 31], [3, 38]] + [[6, 31], [6, 38]] + ] + + it "honors the original selection's range (goal range) when adding across shorter lines", -> + editor.setSelectedBufferRange([[6, 22], [6, 38]]) + editor.addSelectionAbove() + editor.addSelectionAbove() + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 38]] + [[4, 22], [4, 29]] + [[5, 22], [5, 30]] + [[6, 22], [6, 38]] + ] + + describe "when the selection is empty", -> + it "does not skip lines that are shorter than the current column", -> + editor.setCursorBufferPosition([6, 36]) + editor.addSelectionAbove() + editor.addSelectionAbove() + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 36], [3, 36]] + [[4, 29], [4, 29]] + [[5, 30], [5, 30]] + [[6, 36], [6, 36]] + ] + + it "skips empty lines when the column is non-zero", -> + editor.setCursorBufferPosition([11, 4]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[9, 4], [9, 4]] + [[11, 4], [11, 4]] + ] + + it "does not skip empty lines when the column is zero", -> + editor.setCursorBufferPosition([10, 0]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual [ + [[9, 0], [9, 0]] + [[10, 0], [10, 0]] + ] + + describe ".consolidateSelections()", -> + it "destroys all selections but the most recent, returning true if any selections were destroyed", -> + editor.setSelectedBufferRange([[3, 16], [3, 21]]) + selection1 = editor.getSelection() + selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]]) + selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]]) + + expect(editor.getSelections()).toEqual [selection1, selection2, selection3] + expect(editor.consolidateSelections()).toBeTruthy() + expect(editor.getSelections()).toEqual [selection3] + expect(selection3.isEmpty()).toBeFalsy() + expect(editor.consolidateSelections()).toBeFalsy() + expect(editor.getSelections()).toEqual [selection3] + + describe "when the cursor is moved while there is a selection", -> + makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] + + it "clears the selection", -> + makeSelection() + editor.moveCursorDown() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveCursorUp() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveCursorLeft() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveCursorRight() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.setCursorScreenPosition([3, 3]) + expect(selection.isEmpty()).toBeTruthy() + + it "does not share selections between different edit sessions for the same buffer", -> + editor2 = project.openSync('sample.js') + editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) + editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]) + expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges() + + describe "buffer manipulation", -> + describe ".insertText(text)", -> + describe "when there are multiple empty selections", -> + describe "when the cursors are on the same line", -> + it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> + editor.setCursorScreenPosition([1, 2]) + editor.addCursorAtScreenPosition([1, 5]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe ' xxxvarxxx sort = function(items) {' + [cursor1, cursor2] = editor.getCursors() + + expect(cursor1.getBufferPosition()).toEqual [1, 5] + expect(cursor2.getBufferPosition()).toEqual [1, 11] + + describe "when the cursors are on different lines", -> + it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> + editor.setCursorScreenPosition([1, 2]) + editor.addCursorAtScreenPosition([2, 4]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe ' xxxvar sort = function(items) {' + expect(buffer.lineForRow(2)).toBe ' xxxif (items.length <= 1) return items;' + [cursor1, cursor2] = editor.getCursors() + + expect(cursor1.getBufferPosition()).toEqual [1, 5] + expect(cursor2.getBufferPosition()).toEqual [2, 7] + + describe "when there are multiple non-empty selections", -> + describe "when the selections are on the same line", -> + it "replaces each selection range with the inserted characters", -> + editor.setSelectedBufferRanges([[[0,4], [0,13]], [[0,22], [0,24]]]) + editor.insertText("x") + + [cursor1, cursor2] = editor.getCursors() + [selection1, selection2] = editor.getSelections() + + expect(cursor1.getScreenPosition()).toEqual [0, 5] + expect(cursor2.getScreenPosition()).toEqual [0, 15] + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + + expect(editor.lineForBufferRow(0)).toBe "var x = functix () {" + + describe "when the selections are on different lines", -> + it "replaces each selection with the given text, clears the selections, and places the cursor at the end of each selection's inserted text", -> + editor.setSelectedBufferRanges([[[1, 0], [1, 2]], [[2, 0], [2, 4]]]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' + expect(buffer.lineForRow(2)).toBe 'xxxif (items.length <= 1) return items;' + [selection1, selection2] = editor.getSelections() + + expect(selection1.isEmpty()).toBeTruthy() + expect(selection1.cursor.getBufferPosition()).toEqual [1, 3] + expect(selection2.isEmpty()).toBeTruthy() + expect(selection2.cursor.getBufferPosition()).toEqual [2, 3] + + describe "when there is a selection that ends on a folded line", -> + it "destroys the selection", -> + editor.createFold(2,4) + editor.setSelectedBufferRange([[1,0], [2,0]]) + editor.insertText('holy cow') + expect(editor.lineForScreenRow(2).fold).toBeUndefined() + + describe ".insertNewline()", -> + describe "when there is a single cursor", -> + describe "when the cursor is at the beginning of a line", -> + it "inserts an empty line before it", -> + editor.setCursorScreenPosition(row: 1, column: 0) + + editor.insertNewline() + + expect(buffer.lineForRow(1)).toBe '' + expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) + + describe "when the cursor is in the middle of a line", -> + it "splits the current line to form a new line", -> + editor.setCursorScreenPosition(row: 1, column: 6) + originalLine = buffer.lineForRow(1) + lineBelowOriginalLine = buffer.lineForRow(2) + + editor.insertNewline() + + expect(buffer.lineForRow(1)).toBe originalLine[0...6] + expect(buffer.lineForRow(2)).toBe originalLine[6..] + expect(buffer.lineForRow(3)).toBe lineBelowOriginalLine + expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) + + describe "when the cursor is on the end of a line", -> + it "inserts an empty line after it", -> + editor.setCursorScreenPosition(row: 1, column: buffer.lineForRow(1).length) + + editor.insertNewline() + + expect(buffer.lineForRow(2)).toBe '' + expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) + + describe "when there are multiple cursors", -> + describe "when the cursors are on the same line", -> + it "breaks the line at the cursor locations", -> + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.insertNewline() + + expect(editor.lineForBufferRow(3)).toBe " var pivot" + expect(editor.lineForBufferRow(4)).toBe " = items.shift(), current" + expect(editor.lineForBufferRow(5)).toBe ", left = [], right = [];" + expect(editor.lineForBufferRow(6)).toBe " while(items.length > 0) {" + + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [4, 0] + expect(cursor2.getBufferPosition()).toEqual [5, 0] + + describe "when the cursors are on different lines", -> + it "inserts newlines at each cursor location", -> + editor.setCursorScreenPosition([3, 0]) + editor.addCursorAtScreenPosition([6, 0]) + + editor.insertText("\n") + expect(editor.lineForBufferRow(3)).toBe "" + expect(editor.lineForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" + expect(editor.lineForBufferRow(5)).toBe " while(items.length > 0) {" + expect(editor.lineForBufferRow(6)).toBe " current = items.shift();" + expect(editor.lineForBufferRow(7)).toBe "" + expect(editor.lineForBufferRow(8)).toBe " current < pivot ? left.push(current) : right.push(current);" + expect(editor.lineForBufferRow(9)).toBe " }" + + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [4,0] + expect(cursor2.getBufferPosition()).toEqual [8,0] + + describe ".insertNewlineBelow()", -> + describe "when the operation is undone", -> + it "places the cursor back at the previous location", -> + editor.setCursorBufferPosition([0,2]) + editor.insertNewlineBelow() + expect(editor.getCursorBufferPosition()).toEqual [1,0] + editor.undo() + expect(editor.getCursorBufferPosition()).toEqual [0,2] + + it "inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", -> + atom.config.set("editor.autoIndent", true) + editor.insertNewlineBelow() + expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" + expect(buffer.lineForRow(1)).toBe " " + expect(editor.getCursorBufferPosition()).toEqual [1, 2] + + describe ".insertNewlineAbove()", -> + describe "when the cursor is on first line", -> + it "inserts a newline on the first line and moves the cursor to the first line", -> + editor.setCursorBufferPosition([0]) + editor.insertNewlineAbove() + expect(editor.getCursorBufferPosition()).toEqual [0,0] + expect(editor.lineForBufferRow(0)).toBe '' + expect(editor.lineForBufferRow(1)).toBe 'var quicksort = function () {' + expect(editor.buffer.getLineCount()).toBe 14 + + describe "when the cursor is not on the first line", -> + it "inserts a newline above the current line and moves the cursor to the inserted line", -> + editor.setCursorBufferPosition([3,4]) + editor.insertNewlineAbove() + expect(editor.getCursorBufferPosition()).toEqual [3,0] + expect(editor.lineForBufferRow(3)).toBe '' + expect(editor.lineForBufferRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(editor.buffer.getLineCount()).toBe 14 + + editor.undo() + expect(editor.getCursorBufferPosition()).toEqual [3,4] + + describe ".backspace()", -> + describe "when there is a single cursor", -> + changeScreenRangeHandler = null + + beforeEach -> + selection = editor.getLastSelection() + changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') + selection.on 'screen-range-changed', changeScreenRangeHandler + + describe "when the cursor is on the middle of the line", -> + it "removes the character before the cursor", -> + editor.setCursorScreenPosition(row: 1, column: 7) + expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" + + editor.backspace() + + line = buffer.lineForRow(1) + expect(line).toBe " var ort = function(items) {" + expect(editor.getCursorScreenPosition()).toEqual {row: 1, column: 6} + expect(changeScreenRangeHandler).toHaveBeenCalled() + expect(editor.getCursor().isVisible()).toBeTruthy() + + describe "when the cursor is at the beginning of a line", -> + it "joins it with the line above", -> + originalLine0 = buffer.lineForRow(0) + expect(originalLine0).toBe "var quicksort = function () {" + expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" + + editor.setCursorScreenPosition(row: 1, column: 0) + editor.backspace() + + line0 = buffer.lineForRow(0) + line1 = buffer.lineForRow(1) + expect(line0).toBe "var quicksort = function () { var sort = function(items) {" + expect(line1).toBe " if (items.length <= 1) return items;" + expect(editor.getCursorScreenPosition()).toEqual [0, originalLine0.length] + + expect(changeScreenRangeHandler).toHaveBeenCalled() + + describe "when the cursor is at the first column of the first line", -> + it "does nothing, but doesn't raise an error", -> + editor.setCursorScreenPosition(row: 0, column: 0) + editor.backspace() + + describe "when the cursor is on the first column of a line below a fold", -> + it "deletes the folded lines", -> + editor.setCursorScreenPosition([4,0]) + editor.foldCurrentRow() + editor.setCursorScreenPosition([5,0]) + editor.backspace() + + expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" + expect(buffer.lineForRow(4).fold).toBeUndefined() + + describe "when the cursor is in the middle of a line below a fold", -> + it "backspaces as normal", -> + editor.setCursorScreenPosition([4,0]) + editor.foldCurrentRow() + editor.setCursorScreenPosition([5,5]) + editor.backspace() + + expect(buffer.lineForRow(7)).toBe " }" + expect(buffer.lineForRow(8)).toBe " eturn sort(left).concat(pivot).concat(sort(right));" + + describe "when the cursor is on a folded screen line", -> + it "deletes all of the folded lines along with the fold", -> + editor.setCursorBufferPosition([3, 0]) + editor.foldCurrentRow() + editor.backspace() + + expect(buffer.lineForRow(1)).toBe "" + expect(buffer.lineForRow(2)).toBe " return sort(Array.apply(this, arguments));" + expect(editor.getCursorScreenPosition()).toEqual [1, 0] + + describe "when there are multiple cursors", -> + describe "when cursors are on the same line", -> + it "removes the characters preceding each cursor", -> + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.backspace() + + expect(editor.lineForBufferRow(3)).toBe " var pivo = items.shift(), curren, left = [], right = [];" + + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [3, 12] + expect(cursor2.getBufferPosition()).toEqual [3, 36] + + [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + + describe "when cursors are on different lines", -> + describe "when the cursors are in the middle of their lines", -> + it "removes the characters preceding each cursor", -> + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([4, 10]) + + editor.backspace() + + expect(editor.lineForBufferRow(3)).toBe " var pivo = items.shift(), current, left = [], right = [];" + expect(editor.lineForBufferRow(4)).toBe " whileitems.length > 0) {" + + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [3, 12] + expect(cursor2.getBufferPosition()).toEqual [4, 9] + + [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + + describe "when the cursors are on the first column of their lines", -> + it "removes the newlines preceding each cursor", -> + editor.setCursorScreenPosition([3, 0]) + editor.addCursorAtScreenPosition([6, 0]) + + editor.backspace() + expect(editor.lineForBufferRow(2)).toBe " if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = [];" + expect(editor.lineForBufferRow(3)).toBe " while(items.length > 0) {" + expect(editor.lineForBufferRow(4)).toBe " current = items.shift(); current < pivot ? left.push(current) : right.push(current);" + expect(editor.lineForBufferRow(5)).toBe " }" + + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [2,40] + expect(cursor2.getBufferPosition()).toEqual [4,30] + + describe "when there is a single selection", -> + it "deletes the selection, but not the character before it", -> + editor.setSelectedBufferRange([[0,5], [0,9]]) + editor.backspace() + expect(editor.buffer.lineForRow(0)).toBe 'var qsort = function () {' + + describe "when the selection ends on a folded line", -> + it "preserves the fold", -> + editor.setSelectedBufferRange([[3,0], [4,0]]) + editor.foldBufferRow(4) + editor.backspace() + + expect(buffer.lineForRow(3)).toBe " while(items.length > 0) {" + expect(editor.lineForScreenRow(3).fold).toBeDefined() + + describe "when there are multiple selections", -> + it "removes all selected text", -> + editor.setSelectedBufferRanges([[[0,4], [0,13]], [[0,16], [0,24]]]) + editor.backspace() + expect(editor.lineForBufferRow(0)).toBe 'var = () {' + + describe ".backspaceToBeginningOfWord()", -> + describe "when no text is selected", -> + it "deletes all text between the cursor and the beginning of the word", -> + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([3, 5]) + [cursor1, cursor2] = editor.getCursors() + + editor.backspaceToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe ' var sort = function(ems) {' + expect(buffer.lineForRow(3)).toBe ' ar pivot = items.shift(), current, left = [], right = [];' + expect(cursor1.getBufferPosition()).toEqual [1, 22] + expect(cursor2.getBufferPosition()).toEqual [3, 4] + + editor.backspaceToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe ' var sort = functionems) {' + expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];' + expect(cursor1.getBufferPosition()).toEqual [1, 21] + expect(cursor2.getBufferPosition()).toEqual [2, 39] + + editor.backspaceToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe ' var sort = ems) {' + expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];' + expect(cursor1.getBufferPosition()).toEqual [1, 13] + expect(cursor2.getBufferPosition()).toEqual [2, 34] + + describe "when text is selected", -> + it "deletes only selected text", -> + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.backspaceToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' + expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' + + describe ".backspaceToBeginningOfLine()", -> + describe "when no text is selected", -> + it "deletes all text between the cursor and the beginning of the line", -> + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + [cursor1, cursor2] = editor.getCursors() + + editor.backspaceToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe 'ems) {' + expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;' + expect(cursor1.getBufferPosition()).toEqual [1, 0] + expect(cursor2.getBufferPosition()).toEqual [2, 0] + + describe "when at the beginning of the line", -> + it "deletes the newline", -> + editor.setCursorBufferPosition([2]) + editor.backspaceToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' + + describe "when text is selected", -> + it "still deletes all text to begginning of the line", -> + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.backspaceToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe 'ems) {' + expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' + + describe ".delete()", -> + describe "when there is a single cursor", -> + describe "when the cursor is on the middle of a line", -> + it "deletes the character following the cursor", -> + editor.setCursorScreenPosition([1, 6]) + editor.delete() + expect(buffer.lineForRow(1)).toBe ' var ort = function(items) {' + + describe "when the cursor is on the end of a line", -> + it "joins the line with the following line", -> + editor.setCursorScreenPosition([1, buffer.lineForRow(1).length]) + editor.delete() + expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' + + describe "when the cursor is on the last column of the last line", -> + it "does nothing, but doesn't raise an error", -> + editor.setCursorScreenPosition([12, buffer.lineForRow(12).length]) + editor.delete() + expect(buffer.lineForRow(12)).toBe '};' + + describe "when the cursor is on the end of a line above a fold", -> + it "only deletes the lines inside the fold", -> + editor.foldBufferRow(4) + editor.setCursorScreenPosition([3, Infinity]) + cursorPositionBefore = editor.getCursorScreenPosition() + + editor.delete() + + expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" + expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" + expect(editor.getCursorScreenPosition()).toEqual cursorPositionBefore + + describe "when the cursor is in the middle a line above a fold", -> + it "deletes as normal", -> + editor.foldBufferRow(4) + editor.setCursorScreenPosition([3, 4]) + cursorPositionBefore = editor.getCursorScreenPosition() + + editor.delete() + + expect(buffer.lineForRow(3)).toBe " ar pivot = items.shift(), current, left = [], right = [];" + expect(editor.lineForScreenRow(4).fold).toBeDefined() + expect(editor.getCursorScreenPosition()).toEqual [3, 4] + + describe "when the cursor is on a folded line", -> + it "removes the lines contained by the fold", -> + editor.setSelectedBufferRange([[2, 0], [2, 0]]) + editor.createFold(2,4) + editor.createFold(2,6) + oldLine7 = buffer.lineForRow(7) + oldLine8 = buffer.lineForRow(8) + + editor.delete() + expect(editor.lineForScreenRow(2).text).toBe oldLine7 + expect(editor.lineForScreenRow(3).text).toBe oldLine8 + + describe "when there are multiple cursors", -> + describe "when cursors are on the same line", -> + it "removes the characters following each cursor", -> + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.delete() + + expect(editor.lineForBufferRow(3)).toBe " var pivot= items.shift(), current left = [], right = [];" + + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [3, 13] + expect(cursor2.getBufferPosition()).toEqual [3, 37] + + [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + + describe "when cursors are on different lines", -> + describe "when the cursors are in the middle of the lines", -> + it "removes the characters following each cursor", -> + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([4, 10]) + + editor.delete() + + expect(editor.lineForBufferRow(3)).toBe " var pivot= items.shift(), current, left = [], right = [];" + expect(editor.lineForBufferRow(4)).toBe " while(tems.length > 0) {" + + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [3, 13] + expect(cursor2.getBufferPosition()).toEqual [4, 10] + + [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + + describe "when the cursors are at the end of their lines", -> + it "removes the newlines following each cursor", -> + editor.setCursorScreenPosition([0, 29]) + editor.addCursorAtScreenPosition([1, 30]) + + editor.delete() + + expect(editor.lineForBufferRow(0)).toBe "var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items;" + + [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual [0,29] + expect(cursor2.getBufferPosition()).toEqual [0,59] + + describe "when there is a single selection", -> + it "deletes the selection, but not the character following it", -> + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.delete() + expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' + expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' + expect(editor.getSelection().isEmpty()).toBeTruthy() + + describe "when there are multiple selections", -> + describe "when selections are on the same line", -> + it "removes all selected text", -> + editor.setSelectedBufferRanges([[[0,4], [0,13]], [[0,16], [0,24]]]) + editor.delete() + expect(editor.lineForBufferRow(0)).toBe 'var = () {' + + describe ".deleteToEndOfWord()", -> + describe "when no text is selected", -> + it "deletes to the end of the word", -> + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + [cursor1, cursor2] = editor.getCursors() + + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' + expect(buffer.lineForRow(2)).toBe ' i (items.length <= 1) return items;' + expect(cursor1.getBufferPosition()).toEqual [1, 24] + expect(cursor2.getBufferPosition()).toEqual [2, 5] + + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' + expect(buffer.lineForRow(2)).toBe ' iitems.length <= 1) return items;' + expect(cursor1.getBufferPosition()).toEqual [1, 24] + expect(cursor2.getBufferPosition()).toEqual [2, 5] + + describe "when text is selected", -> + it "deletes only selected text", -> + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' + + describe ".indent()", -> + describe "when the selection is empty", -> + describe "when autoIndent is disabled", -> + describe "if 'softTabs' is true (the default)", -> + it "inserts 'tabLength' spaces into the buffer", -> + tabRegex = new RegExp("^[ ]{#{editor.getTabLength()}}") + expect(buffer.lineForRow(0)).not.toMatch(tabRegex) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(tabRegex) + + describe "if 'softTabs' is false", -> + it "insert a \t into the buffer", -> + editor.setSoftTabs(false) + expect(buffer.lineForRow(0)).not.toMatch(/^\t/) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t/) + + describe "when autoIndent is enabled", -> + describe "when the cursor's column is less than the suggested level of indentation", -> + describe "when 'softTabs' is true (the default)", -> + it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentaion", -> + buffer.insert([5, 0], " \n") + editor.setCursorBufferPosition [5, 0] + editor.indent(autoIndent: true) + expect(buffer.lineForRow(5)).toMatch /^\s+$/ + expect(buffer.lineForRow(5).length).toBe 6 + expect(editor.getCursorBufferPosition()).toEqual [5, 6] + + describe "when 'softTabs' is false", -> + it "moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentaion", -> + convertToHardTabs(buffer) + editor.setSoftTabs(false) + buffer.insert([5, 0], "\t\n") + editor.setCursorBufferPosition [5, 0] + editor.indent(autoIndent: true) + expect(buffer.lineForRow(5)).toMatch /^\t\t\t$/ + expect(editor.getCursorBufferPosition()).toEqual [5, 3] + + describe "when the line's indent level is greater than the suggested level of indentation", -> + describe "when 'softTabs' is true (the default)", -> + it "moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", -> + buffer.insert([7, 0], " \n") + editor.setCursorBufferPosition [7, 2] + editor.indent(autoIndent: true) + expect(buffer.lineForRow(7)).toMatch /^\s+$/ + expect(buffer.lineForRow(7).length).toBe 8 + expect(editor.getCursorBufferPosition()).toEqual [7, 8] + + describe "when 'softTabs' is false", -> + it "moves the cursor to the end of the leading whitespace and inserts \t into the buffer", -> + convertToHardTabs(buffer) + editor.setSoftTabs(false) + buffer.insert([7, 0], "\t\t\t\n") + editor.setCursorBufferPosition [7, 1] + editor.indent(autoIndent: true) + expect(buffer.lineForRow(7)).toMatch /^\t\t\t\t$/ + expect(editor.getCursorBufferPosition()).toEqual [7, 4] + + describe "when the selection is not empty", -> + it "indents the selected lines", -> + editor.setSelectedBufferRange([[0, 0], [10, 0]]) + selection = editor.getSelection() + spyOn(selection, "indentSelectedRows") + editor.indent() + expect(selection.indentSelectedRows).toHaveBeenCalled() + + describe "if editor.softTabs is false", -> + it "inserts a tab character into the buffer", -> + editor.setSoftTabs(false) + expect(buffer.lineForRow(0)).not.toMatch(/^\t/) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t/) + expect(editor.getCursorBufferPosition()).toEqual [0, 1] + expect(editor.getCursorScreenPosition()).toEqual [0, editor.getTabLength()] + + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t\t/) + expect(editor.getCursorBufferPosition()).toEqual [0, 2] + expect(editor.getCursorScreenPosition()).toEqual [0, editor.getTabLength() * 2] + + describe "pasteboard operations", -> + beforeEach -> + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + + describe ".cutSelectedText()", -> + it "removes the selected text from the buffer and places it on the pasteboard", -> + editor.cutSelectedText() + expect(buffer.lineForRow(0)).toBe "var = function () {" + expect(buffer.lineForRow(1)).toBe " var = function(items) {" + + expect(clipboard.readText()).toBe 'quicksort\nsort' + + describe ".cutToEndOfLine()", -> + describe "when soft wrap is on", -> + it "cuts up to the end of the line", -> + editor.setSoftWrap(true) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([2, 2]) + editor.cutToEndOfLine() + expect(editor.lineForScreenRow(2).text).toBe '= () {' + + describe "when soft wrap is off", -> + describe "when nothing is selected", -> + it "cuts up to the end of the line", -> + editor.setCursorBufferPosition([2, 20]) + editor.addCursorAtBufferPosition([3, 20]) + editor.cutToEndOfLine() + expect(buffer.lineForRow(2)).toBe ' if (items.length' + expect(buffer.lineForRow(3)).toBe ' var pivot = item' + expect(atom.pasteboard.read()[0]).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' + + describe "when text is selected", -> + it "only cuts the selected text, not to the end of the line", -> + editor.setSelectedBufferRanges([[[2,20], [2, 30]], [[3, 20], [3, 20]]]) + + editor.cutToEndOfLine() + + expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' + expect(buffer.lineForRow(3)).toBe ' var pivot = item' + expect(atom.pasteboard.read()[0]).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' + + describe ".copySelectedText()", -> + it "copies selected text onto the clipboard", -> + editor.copySelectedText() + expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" + expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" + expect(clipboard.readText()).toBe 'quicksort\nsort' + + describe ".pasteText()", -> + it "pastes text into the buffer", -> + atom.pasteboard.write('first') + editor.pasteText() + expect(editor.buffer.lineForRow(0)).toBe "var first = function () {" + expect(buffer.lineForRow(1)).toBe " var first = function(items) {" + + describe ".indentSelectedRows()", -> + describe "when nothing is selected", -> + describe "when softTabs is enabled", -> + it "indents line and retains selection", -> + editor.setSelectedBufferRange([[0,3], [0,3]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" + expect(editor.getSelectedBufferRange()).toEqual [[0, 3 + editor.getTabLength()], [0, 3 + editor.getTabLength()]] + + describe "when softTabs is disabled", -> + it "indents line and retains selection", -> + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[0,3], [0,3]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" + expect(editor.getSelectedBufferRange()).toEqual [[0, 3 + 1], [0, 3 + 1]] + + describe "when one line is selected", -> + describe "when softTabs is enabled", -> + it "indents line and retains selection", -> + editor.setSelectedBufferRange([[0,4], [0,14]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe "#{editor.getTabText()}var quicksort = function () {" + expect(editor.getSelectedBufferRange()).toEqual [[0, 4 + editor.getTabLength()], [0, 14 + editor.getTabLength()]] + + describe "when softTabs is disabled", -> + it "indents line and retains selection", -> + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[0,4], [0,14]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" + expect(editor.getSelectedBufferRange()).toEqual [[0, 4 + 1], [0, 14 + 1]] + + describe "when multiple lines are selected", -> + describe "when softTabs is enabled", -> + it "indents selected lines (that are not empty) and retains selection", -> + editor.setSelectedBufferRange([[9,1], [11,15]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe " };" + expect(buffer.lineForRow(10)).toBe "" + expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" + expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + editor.getTabLength()], [11, 15 + editor.getTabLength()]] + + it "does not indent the last row if the selection ends at column 0", -> + editor.setSelectedBufferRange([[9,1], [11,0]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe " };" + expect(buffer.lineForRow(10)).toBe "" + expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" + expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + editor.getTabLength()], [11, 0]] + + describe "when softTabs is disabled", -> + it "indents selected lines (that are not empty) and retains selection", -> + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[9,1], [11,15]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe "\t\t};" + expect(buffer.lineForRow(10)).toBe "" + expect(buffer.lineForRow(11)).toBe "\t\treturn sort(Array.apply(this, arguments));" + expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + 1], [11, 15 + 1]] + + describe ".outdentSelectedRows()", -> + describe "when nothing is selected", -> + it "outdents line and retains selection", -> + editor.setSelectedBufferRange([[1,3], [1,3]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" + expect(editor.getSelectedBufferRange()).toEqual [[1, 3 - editor.getTabLength()], [1, 3 - editor.getTabLength()]] + + it "outdents when indent is less than a tab length", -> + editor.insertText(' ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" + + it "outdents a single hard tab when indent is multiple hard tabs and and the session is using soft tabs", -> + editor.insertText('\t\t') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" + + it "outdents when a mix of hard tabs and soft tabs are used", -> + editor.insertText('\t ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" + + describe "when one line is selected", -> + it "outdents line and retains editor", -> + editor.setSelectedBufferRange([[1,4], [1,14]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" + expect(editor.getSelectedBufferRange()).toEqual [[1, 4 - editor.getTabLength()], [1, 14 - editor.getTabLength()]] + + describe "when multiple lines are selected", -> + it "outdents selected lines and retains editor", -> + editor.setSelectedBufferRange([[0,1], [3,15]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" + expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" + expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" + expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" + expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 15 - editor.getTabLength()]] + + it "does not outdent the last line of the selection if it ends at column 0", -> + editor.setSelectedBufferRange([[0,1], [3,0]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" + expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" + expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" + expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" + + expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 0]] + + describe ".toggleLineCommentsInSelection()", -> + it "toggles comments on the selected lines", -> + editor.setSelectedBufferRange([[4, 5], [7, 5]]) + editor.toggleLineCommentsInSelection() + + expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {" + expect(buffer.lineForRow(5)).toBe " // current = items.shift();" + expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" + expect(buffer.lineForRow(7)).toBe " // }" + expect(editor.getSelectedBufferRange()).toEqual [[4, 8], [7, 8]] + + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" + expect(buffer.lineForRow(5)).toBe " current = items.shift();" + expect(buffer.lineForRow(6)).toBe " current < pivot ? left.push(current) : right.push(current);" + expect(buffer.lineForRow(7)).toBe " }" + + it "does not comment the last line of a non-empty selection if it ends at column 0", -> + editor.setSelectedBufferRange([[4, 5], [7, 0]]) + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {" + expect(buffer.lineForRow(5)).toBe " // current = items.shift();" + expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" + expect(buffer.lineForRow(7)).toBe " }" + + it "uncomments lines if all lines match the comment regex", -> + editor.setSelectedBufferRange([[0, 0], [0, 1]]) + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {" + + editor.setSelectedBufferRange([[0, 0], [2, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(0)).toBe "// // var quicksort = function () {" + expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {" + expect(buffer.lineForRow(2)).toBe "// if (items.length <= 1) return items;" + + editor.setSelectedBufferRange([[0, 0], [2, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {" + expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" + expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" + + editor.setSelectedBufferRange([[0, 0], [0, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" + + it "uncomments commented lines separated by an empty line", -> + editor.setSelectedBufferRange([[0, 0], [1, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {" + expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {" + + buffer.insert([0, Infinity], '\n') + + editor.setSelectedBufferRange([[0, 0], [2, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" + expect(buffer.lineForRow(1)).toBe "" + expect(buffer.lineForRow(2)).toBe " var sort = function(items) {" + + it "preserves selection emptiness", -> + editor.setCursorBufferPosition([4, 0]) + editor.toggleLineCommentsInSelection() + expect(editor.getSelection().isEmpty()).toBeTruthy() + + it "does not explode if the current language mode has no comment regex", -> + editor.destroy() + editor = project.openSync(null, autoIndent: false) + editor.setSelectedBufferRange([[4, 5], [4, 5]]) + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" + + it "uncomments when the line lacks the trailing whitespace in the comment regex", -> + editor.setCursorBufferPosition([10, 0]) + editor.toggleLineCommentsInSelection() + + expect(buffer.lineForRow(10)).toBe "// " + expect(editor.getSelectedBufferRange()).toEqual [[10, 3], [10, 3]] + editor.backspace() + expect(buffer.lineForRow(10)).toBe "//" + + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(10)).toBe "" + expect(editor.getSelectedBufferRange()).toEqual [[10, 0], [10, 0]] + + it "uncomments when the line has leading whitespace", -> + editor.setCursorBufferPosition([10, 0]) + editor.toggleLineCommentsInSelection() + + expect(buffer.lineForRow(10)).toBe "// " + editor.moveCursorToBeginningOfLine() + editor.insertText(" ") + editor.setSelectedBufferRange([[10, 0], [10, 0]]) + editor.toggleLineCommentsInSelection() + expect(buffer.lineForRow(10)).toBe " " + + describe ".undo() and .redo()", -> + it "undoes/redoes the last change", -> + editor.insertText("foo") + editor.undo() + expect(buffer.lineForRow(0)).not.toContain "foo" + + editor.redo() + expect(buffer.lineForRow(0)).toContain "foo" + + it "batches the undo / redo of changes caused by multiple cursors", -> + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + + editor.insertText("foo") + editor.backspace() + + expect(buffer.lineForRow(0)).toContain "fovar" + expect(buffer.lineForRow(1)).toContain "fo " + + editor.undo() + + expect(buffer.lineForRow(0)).toContain "foo" + expect(buffer.lineForRow(1)).toContain "foo" + + editor.redo() + + expect(buffer.lineForRow(0)).not.toContain "foo" + expect(buffer.lineForRow(0)).toContain "fovar" + + it "restores the selected ranges after undo and redo", -> + editor.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) + editor.delete() + editor.delete() + + selections = editor.getSelections() + expect(buffer.lineForRow(1)).toBe ' var = function( {' + + expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 17], [1, 17]]] + + editor.undo() + expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] + + editor.undo() + expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 10]], [[1, 22], [1, 27]]] + + editor.redo() + expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] + + xit "restores folds after undo and redo", -> + editor.foldBufferRow(1) + editor.setSelectedBufferRange([[1, 0], [10, Infinity]], preserveFolds: true) + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + + editor.insertText """ + \ // testing + function foo() { + return 1 + 2; + } + """ + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + editor.foldBufferRow(2) + + editor.undo() + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + + editor.redo() + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + + describe "begin/commitTransaction()", -> + it "restores the selection when the transaction is undone/redone", -> + buffer.setText('1234') + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + editor.beginTransaction() + + editor.delete() + editor.moveCursorToEndOfLine() + editor.insertText('5') + expect(buffer.getText()).toBe '145' + + editor.commitTransaction() + + editor.undo() + expect(buffer.getText()).toBe '1234' + expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] + + editor.redo() + expect(buffer.getText()).toBe '145' + expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 3]] + + describe "when the buffer is changed (via its direct api, rather than via than edit session)", -> + it "moves the cursor so it is in the same relative position of the buffer", -> + expect(editor.getCursorScreenPosition()).toEqual [0, 0] + editor.addCursorAtScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 0]) + [cursor1, cursor2, cursor3] = editor.getCursors() + + buffer.insert([0, 1], 'abc') + + expect(cursor1.getScreenPosition()).toEqual [0, 0] + expect(cursor2.getScreenPosition()).toEqual [0, 8] + expect(cursor3.getScreenPosition()).toEqual [1, 0] + + it "does not destroy cursors or selections when a change encompasses them", -> + cursor = editor.getCursor() + cursor.setBufferPosition [3, 3] + editor.buffer.delete([[3, 1], [3, 5]]) + expect(cursor.getBufferPosition()).toEqual [3, 1] + expect(editor.getCursors().indexOf(cursor)).not.toBe -1 + + selection = editor.getLastSelection() + selection.setBufferRange [[3, 5], [3, 10]] + editor.buffer.delete [[3, 3], [3, 8]] + expect(selection.getBufferRange()).toEqual [[3, 3], [3, 5]] + expect(editor.getSelections().indexOf(selection)).not.toBe -1 + + it "merges cursors when the change causes them to overlap", -> + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 2]) + editor.addCursorAtScreenPosition([1, 2]) + + [cursor1, cursor2, cursor3] = editor.getCursors() + expect(editor.getCursors().length).toBe 3 + + buffer.delete([[0, 0], [0, 2]]) + + expect(editor.getCursors().length).toBe 2 + expect(editor.getCursors()).toEqual [cursor1, cursor3] + expect(cursor1.getBufferPosition()).toEqual [0,0] + expect(cursor3.getBufferPosition()).toEqual [1,2] + + describe ".deleteLine()", -> + it "deletes the first line when the cursor is there", -> + editor.getCursor().moveToTop() + line1 = buffer.lineForRow(1) + count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line1) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + + it "deletes the last line when the cursor is there", -> + count = buffer.getLineCount() + secondToLastLine = buffer.lineForRow(count - 2) + expect(buffer.lineForRow(count - 1)).not.toBe(secondToLastLine) + editor.getCursor().moveToBottom() + editor.deleteLine() + newCount = buffer.getLineCount() + expect(buffer.lineForRow(newCount - 1)).toBe(secondToLastLine) + expect(newCount).toBe(count - 1) + + it "deletes whole lines when partial lines are selected", -> + editor.setSelectedBufferRange([[0, 2], [1, 2]]) + line2 = buffer.lineForRow(2) + count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line2) + expect(buffer.lineForRow(1)).not.toBe(line2) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line2) + expect(buffer.getLineCount()).toBe(count - 2) + + it "only deletes first line if only newline is selected on second line", -> + editor.setSelectedBufferRange([[0, 2], [1, 0]]) + line1 = buffer.lineForRow(1) + count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line1) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + + it "deletes the entire region when invoke on a folded region", -> + editor.foldBufferRow(1) + editor.getCursor().moveToTop() + editor.getCursor().moveDown() + expect(buffer.getLineCount()).toBe(13) + editor.deleteLine() + expect(buffer.getLineCount()).toBe(4) + + it "deletes the entire file from the bottom up", -> + count = buffer.getLineCount() + expect(count).toBeGreaterThan(0) + for line in [0...count] + editor.getCursor().moveToBottom() + editor.deleteLine() + expect(buffer.getLineCount()).toBe(1) + expect(buffer.getText()).toBe('') + + it "deletes the entire file from the top down", -> + count = buffer.getLineCount() + expect(count).toBeGreaterThan(0) + for line in [0...count] + editor.getCursor().moveToTop() + editor.deleteLine() + expect(buffer.getLineCount()).toBe(1) + expect(buffer.getText()).toBe('') + + describe "when soft wrap is enabled", -> + it "deletes the entire line that the cursor is on", -> + editor.setSoftWrap(true) + editor.setEditorWidthInChars(10) + editor.setCursorBufferPosition([6]) + + line7 = buffer.lineForRow(7) + count = buffer.getLineCount() + expect(buffer.lineForRow(6)).not.toBe(line7) + editor.deleteLine() + expect(buffer.lineForRow(6)).toBe(line7) + expect(buffer.getLineCount()).toBe(count - 1) + + describe "when the line being deleted preceeds a fold, and the command is undone", -> + it "restores the line and preserves the fold", -> editor.setCursorBufferPosition([4]) editor.foldCurrentRow() - editor.trigger 'editor:duplicate-line' - expect(editor.getCursorScreenPosition()).toEqual [5] expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - expect(editor.isFoldedAtScreenRow(5)).toBeTruthy() - expect(buffer.lineForRow(8)).toBe ' while(items.length > 0) {' - expect(buffer.lineForRow(9)).toBe ' current = items.shift();' - expect(buffer.lineForRow(10)).toBe ' current < pivot ? left.push(current) : right.push(current);' - expect(buffer.lineForRow(11)).toBe ' }' + editor.setCursorBufferPosition([3]) + editor.deleteLine() + expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' + editor.undo() + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - describe "when the cursor is on the last line and it doesn't have a trailing newline", -> - it "inserts a newline and the duplicated line", -> - editor.moveCursorToBottom() - editor.trigger 'editor:duplicate-line' - expect(buffer.lineForRow(12)).toBe '};' - expect(buffer.lineForRow(13)).toBe '};' - expect(buffer.lineForRow(14)).toBeUndefined() - expect(editor.getCursorBufferPosition()).toEqual [13, 2] + describe ".replaceSelectedText(options, fn)", -> + describe "when no text is selected", -> + it "inserts the text returned from the function at the cursor position", -> + editor.replaceSelectedText {}, -> '123' + expect(buffer.lineForRow(0)).toBe '123var quicksort = function () {' - describe "when the cursor in on the last line and it is only a newline", -> - it "duplicates the current line below and moves the cursor down one row", -> - editor.moveCursorToBottom() - editor.insertNewline() - editor.moveCursorToBottom() - editor.trigger 'editor:duplicate-line' - expect(buffer.lineForRow(13)).toBe '' - expect(buffer.lineForRow(14)).toBe '' - expect(buffer.lineForRow(15)).toBeUndefined() - expect(editor.getCursorBufferPosition()).toEqual [14, 0] + editor.replaceSelectedText {selectWordIfEmpty: true}, -> 'var' + editor.setCursorBufferPosition([0]) + expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - describe "when the cursor is on the second to last line and the last line only a newline", -> - it "duplicates the current line below and moves the cursor down one row", -> - editor.moveCursorToBottom() - editor.insertNewline() - editor.setCursorBufferPosition([12]) - editor.trigger 'editor:duplicate-line' - expect(buffer.lineForRow(12)).toBe '};' - expect(buffer.lineForRow(13)).toBe '};' - expect(buffer.lineForRow(14)).toBe '' - expect(buffer.lineForRow(15)).toBeUndefined() - expect(editor.getCursorBufferPosition()).toEqual [13, 0] + editor.setCursorBufferPosition([10]) + editor.replaceSelectedText null, -> '' + expect(buffer.lineForRow(10)).toBe '' - describe "editor:save-debug-snapshot", -> - it "saves the state of the rendered lines, the display buffer, and the buffer to a file of the user's choosing", -> - saveDialogCallback = null - spyOn(atom, 'showSaveDialog').andCallFake (callback) -> saveDialogCallback = callback - spyOn(fs, 'writeFileSync') + describe "when text is selected", -> + it "replaces the selected text with the text returned from the function", -> + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + editor.replaceSelectedText {}, -> 'ia' + expect(buffer.lineForRow(0)).toBe 'via quicksort = function () {' - editor.trigger 'editor:save-debug-snapshot' + describe ".transpose()", -> + it "swaps two characters", -> + editor.buffer.setText("abc") + editor.setCursorScreenPosition([0, 1]) + editor.transpose() + expect(editor.lineForBufferRow(0)).toBe 'bac' - statePath = path.join(temp.dir, 'state') - expect(atom.showSaveDialog).toHaveBeenCalled() - saveDialogCallback(statePath) - expect(fs.writeFileSync).toHaveBeenCalled() - expect(fs.writeFileSync.argsForCall[0][0]).toBe statePath - expect(typeof fs.writeFileSync.argsForCall[0][1]).toBe 'string' + it "reverses a selection", -> + editor.buffer.setText("xabcz") + editor.setSelectedBufferRange([[0, 1], [0, 4]]) + editor.transpose() + expect(editor.lineForBufferRow(0)).toBe 'xcbaz' - describe "when the escape key is pressed on the editor", -> - it "clears multiple selections if there are any, and otherwise allows other bindings to be handled", -> - keymap.bindKeys 'name', '.editor', 'escape': 'test-event' - testEventHandler = jasmine.createSpy("testEventHandler") + describe ".upperCase()", -> + describe "when there is no selection", -> + it "upper cases the current word", -> + editor.buffer.setText("aBc") + editor.setCursorScreenPosition([0, 1]) + editor.upperCase() + expect(editor.lineForBufferRow(0)).toBe 'ABC' + expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 1]] - editor.on 'test-event', testEventHandler - editor.activeEditSession.addSelectionForBufferRange([[3, 0], [3, 0]]) - expect(editor.activeEditSession.getSelections().length).toBe 2 + describe "when there is a selection", -> + it "upper cases the current selection", -> + editor.buffer.setText("abc") + editor.setSelectedBufferRange([[0,0], [0,2]]) + editor.upperCase() + expect(editor.lineForBufferRow(0)).toBe 'ABc' + expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - editor.trigger(keydownEvent('escape')) - expect(editor.activeEditSession.getSelections().length).toBe 1 - expect(testEventHandler).not.toHaveBeenCalled() + describe ".lowerCase()", -> + describe "when there is no selection", -> + it "lower cases the current word", -> + editor.buffer.setText("aBC") + editor.setCursorScreenPosition([0, 1]) + editor.lowerCase() + expect(editor.lineForBufferRow(0)).toBe 'abc' + expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 1]] - editor.trigger(keydownEvent('escape')) - expect(testEventHandler).toHaveBeenCalled() + describe "when there is a selection", -> + it "lower cases the current selection", -> + editor.buffer.setText("ABC") + editor.setSelectedBufferRange([[0,0], [0,2]]) + editor.lowerCase() + expect(editor.lineForBufferRow(0)).toBe 'abC' + expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - describe "when the editor is attached but invisible", -> - describe "when the editor's text is changed", -> - it "redraws the editor when it is next shown", -> - window.rootView = new RootView - rootView.openSync('sample.js') - rootView.attachToDom() - editor = rootView.getActiveView() + describe "soft-tabs detection", -> + it "assigns soft / hard tabs based on the contents of the buffer, or uses the default if unknown", -> + editor = project.openSync('sample.js', softTabs: false) + expect(editor.getSoftTabs()).toBeTruthy() - view = $$ -> @div id: 'view', tabindex: -1, 'View' - editor.getPane().showItem(view) - expect(editor.isVisible()).toBeFalsy() + editor = project.openSync('sample-with-tabs.coffee', softTabs: true) + expect(editor.getSoftTabs()).toBeFalsy() - editor.setText('hidden changes') - editor.setCursorBufferPosition([0,4]) + editor = project.openSync(null, softTabs: false) + expect(editor.getSoftTabs()).toBeFalsy() - displayUpdatedHandler = jasmine.createSpy("displayUpdatedHandler") - editor.on 'editor:display-updated', displayUpdatedHandler - editor.getPane().showItem(editor.getModel()) - expect(editor.isVisible()).toBeTruthy() + describe ".indentLevelForLine(line)", -> + it "returns the indent level when the line has only leading whitespace", -> + expect(editor.indentLevelForLine(" hello")).toBe(2) + expect(editor.indentLevelForLine(" hello")).toBe(1.5) - waitsFor -> - displayUpdatedHandler.callCount is 1 + it "returns the indent level when the line has only leading tabs", -> + expect(editor.indentLevelForLine("\t\thello")).toBe(2) - runs -> - expect(editor.renderedLines.find('.line').text()).toBe 'hidden changes' + it "returns the indent level when the line has mixed leading whitespace and tabs", -> + expect(editor.indentLevelForLine("\t hello")).toBe(2) + expect(editor.indentLevelForLine(" \thello")).toBe(2) + expect(editor.indentLevelForLine(" \t hello")).toBe(2.5) + expect(editor.indentLevelForLine(" \t \thello")).toBe(3.5) - it "redraws the editor when it is next reattached", -> - editor.attachToDom() - editor.hide() - editor.setText('hidden changes') - editor.setCursorBufferPosition([0,4]) - editor.detach() + describe "when the buffer is reloaded", -> + it "preserves the current cursor position", -> + editor.setCursorScreenPosition([0, 1]) + editor.buffer.reload() + expect(editor.getCursorScreenPosition()).toEqual [0,1] - displayUpdatedHandler = jasmine.createSpy("displayUpdatedHandler") - editor.on 'editor:display-updated', displayUpdatedHandler - editor.show() - editor.attachToDom() + describe "when a better-matched grammar is added to syntax", -> + it "switches to the better-matched grammar and re-tokenizes the buffer", -> + editor.destroy() + jsGrammar = atom.syntax.selectGrammar('a.js') + atom.syntax.removeGrammar(jsGrammar) - waitsFor -> - displayUpdatedHandler.callCount is 1 + editor = project.openSync('sample.js', autoIndent: false) + expect(editor.getGrammar()).toBe atom.syntax.nullGrammar + expect(editor.lineForScreenRow(0).tokens.length).toBe 1 - runs -> - expect(editor.renderedLines.find('.line').text()).toBe 'hidden changes' + atom.syntax.addGrammar(jsGrammar) + expect(editor.getGrammar()).toBe jsGrammar + expect(editor.lineForScreenRow(0).tokens.length).toBeGreaterThan 1 - describe "editor:scroll-to-cursor", -> - it "scrolls to and centers the editor on the cursor's position", -> - editor.attachToDom(heightInLines: 3) - editor.setCursorBufferPosition([1, 2]) - editor.scrollToBottom() - expect(editor.getFirstVisibleScreenRow()).not.toBe 0 - expect(editor.getLastVisibleScreenRow()).not.toBe 2 - editor.trigger('editor:scroll-to-cursor') - expect(editor.getFirstVisibleScreenRow()).toBe 0 - expect(editor.getLastVisibleScreenRow()).toBe 2 + describe "auto-indent", -> + copyText = (text, {startColumn}={}) -> + startColumn ?= 0 + editor.setCursorBufferPosition([0, 0]) + editor.insertText(text) + numberOfNewlines = text.match(/\n/g)?.length + endColumn = text.match(/[^\n]*$/)[0]?.length + editor.getSelection().setBufferRange([[0,startColumn], [numberOfNewlines,endColumn]]) + editor.cutSelectedText() - describe "when the editor is removed", -> - it "fires a editor:will-be-removed event", -> - window.rootView = new RootView - rootView.openSync('sample.js') - rootView.attachToDom() - editor = rootView.getActiveView() + describe "editor.autoIndent", -> + describe "when editor.autoIndent is false (default)", -> + describe "when `indent` is triggered", -> + it "does not auto-indent the line", -> + editor.setCursorBufferPosition([1, 30]) + editor.insertText("\n ") + expect(editor.lineForBufferRow(2)).toBe " " - willBeRemovedHandler = jasmine.createSpy('fileChange') - editor.on 'editor:will-be-removed', willBeRemovedHandler - editor.getPane().destroyActiveItem() - expect(willBeRemovedHandler).toHaveBeenCalled() + atom.config.set("editor.autoIndent", false) + editor.indent() + expect(editor.lineForBufferRow(2)).toBe " " - describe "when setInvisibles is toggled (regression)", -> - it "renders inserted newlines properly", -> - editor.setShowInvisibles(true) - editor.setCursorBufferPosition([0, 0]) - editor.attachToDom(heightInLines: 20) - editor.setShowInvisibles(false) - editor.insertText("\n") + describe "when editor.autoIndent is true", -> + beforeEach -> + atom.config.set("editor.autoIndent", true) - for rowNumber in [1..5] - expect(editor.lineElementForScreenRow(rowNumber).text()).toBe buffer.lineForRow(rowNumber) + describe "when `indent` is triggered", -> + it "auto-indents the line", -> + editor.setCursorBufferPosition([1, 30]) + editor.insertText("\n ") + expect(editor.lineForBufferRow(2)).toBe " " - describe "when the window is resized", -> - it "updates the active edit session with the current soft wrap column", -> - editor.attachToDom() - setEditorWidthInChars(editor, 50) - expect(editor.activeEditSession.getSoftWrapColumn()).toBe 50 - setEditorWidthInChars(editor, 100) - $(window).trigger 'resize' - expect(editor.activeEditSession.getSoftWrapColumn()).toBe 100 + atom.config.set("editor.autoIndent", true) + editor.indent() + expect(editor.lineForBufferRow(2)).toBe " " + + describe "when a newline is added", -> + describe "when the line preceding the newline adds a new level of indentation", -> + it "indents the newline to one additional level of indentation beyond the preceding line", -> + editor.setCursorBufferPosition([1, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 + + describe "when the line preceding the newline does't add a level of indentation", -> + it "indents the new line to the same level a as the preceding line", -> + editor.setCursorBufferPosition([5, 13]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(6)).toBe editor.indentationForBufferRow(5) + + describe "when the line preceding the newline is a comment", -> + it "maintains the indent of the commented line", -> + editor.setCursorBufferPosition([0, 0]) + editor.insertText(' //') + editor.setCursorBufferPosition([0, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(1)).toBe 2 + + it "does not indent the line preceding the newline", -> + editor.setCursorBufferPosition([2, 0]) + editor.insertText(' var this-line-should-be-indented-more\n') + expect(editor.indentationForBufferRow(1)).toBe 1 + + atom.config.set("editor.autoIndent", true) + editor.setCursorBufferPosition([2, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(1)).toBe 1 + expect(editor.indentationForBufferRow(2)).toBe 1 + + describe "when inserted text matches a decrease indent pattern", -> + describe "when the preceding line matches an increase indent pattern", -> + it "decreases the indentation to match that of the preceding line", -> + editor.setCursorBufferPosition([1, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 + editor.insertText('}') + expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + + describe "when the preceding line doesn't match an increase indent pattern", -> + it "decreases the indentation to be one level below that of the preceding line", -> + editor.setCursorBufferPosition([3, Infinity]) + editor.insertText('\n ') + expect(editor.indentationForBufferRow(4)).toBe editor.indentationForBufferRow(3) + editor.insertText('}') + expect(editor.indentationForBufferRow(4)).toBe editor.indentationForBufferRow(3) - 1 + + it "doesn't break when decreasing the indentation on a row that has no indentation", -> + editor.setCursorBufferPosition([12, Infinity]) + editor.insertText("\n}; # too many closing brackets!") + expect(editor.lineForBufferRow(13)).toBe "}; # too many closing brackets!" + + describe "when inserted text does not match a decrease indent pattern", -> + it "does not the indentation", -> + editor.setCursorBufferPosition([12, 0]) + editor.insertText(' ') + expect(editor.lineForBufferRow(12)).toBe ' };' + editor.insertText('\t\t') + expect(editor.lineForBufferRow(12)).toBe ' \t\t};' + + describe "when the current line does not match a decrease indent pattern", -> + it "leaves the line unchanged", -> + editor.setCursorBufferPosition([2, 4]) + expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 + editor.insertText('foo') + expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 + + describe "editor.normalizeIndentOnPaste", -> + beforeEach -> + atom.config.set('editor.normalizeIndentOnPaste', true) + + it "does not normalize the indentation level of the text when editor.normalizeIndentOnPaste is false", -> + copyText(" function() {\nvar cool = 1;\n }\n") + atom.config.set('editor.normalizeIndentOnPaste', false) + editor.setCursorBufferPosition([5, 2]) + editor.pasteText() + expect(editor.lineForBufferRow(5)).toBe " function() {" + expect(editor.lineForBufferRow(6)).toBe "var cool = 1;" + expect(editor.lineForBufferRow(7)).toBe " }" + + describe "when the inserted text contains no newlines", -> + it "does not adjust the indentation level of the text", -> + editor.setCursorBufferPosition([5, 2]) + editor.insertText("foo", indentBasis: 5) + expect(editor.lineForBufferRow(5)).toBe " foo current = items.shift();" + + it "does not adjust the whitespace if there are preceding characters", -> + copyText(" foo") + editor.setCursorBufferPosition([5, 30]) + editor.pasteText() + + expect(editor.lineForBufferRow(5)).toBe " current = items.shift(); foo" + + describe "when the inserted text contains newlines", -> + describe "when the cursor is preceded only by whitespace characters", -> + it "normalizes indented lines to the cursor's current indentation level", -> + copyText(" while (true) {\n foo();\n }\n", {startColumn: 2}) + editor.setCursorBufferPosition([3, 4]) + editor.pasteText() + + expect(editor.lineForBufferRow(3)).toBe " while (true) {" + expect(editor.lineForBufferRow(4)).toBe " foo();" + expect(editor.lineForBufferRow(5)).toBe " }" + expect(editor.lineForBufferRow(6)).toBe "var pivot = items.shift(), current, left = [], right = [];" + + describe "when the cursor is preceded by non-whitespace characters", -> + it "normalizes the indentation level of all lines based on the level of the existing first line", -> + copyText(" while (true) {\n foo();\n }\n", {startColumn: 0}) + editor.setCursorBufferPosition([1, Infinity]) + editor.pasteText() + + expect(editor.lineForBufferRow(1)).toBe " var sort = function(items) {while (true) {" + expect(editor.lineForBufferRow(2)).toBe " foo();" + expect(editor.lineForBufferRow(3)).toBe " }" + expect(editor.lineForBufferRow(4)).toBe "" + + it "autoIndentSelectedRows auto-indents the selection", -> + editor.setCursorBufferPosition([2, 0]) + editor.insertText("function() {\ninside=true\n}\n i=1\n") + editor.getSelection().setBufferRange([[2,0], [6,0]]) + editor.autoIndentSelectedRows() + + expect(editor.lineForBufferRow(2)).toBe " function() {" + expect(editor.lineForBufferRow(3)).toBe " inside=true" + expect(editor.lineForBufferRow(4)).toBe " }" + expect(editor.lineForBufferRow(5)).toBe " i=1" + + describe ".destroy()", -> + it "destroys all markers associated with the edit session", -> + expect(buffer.getMarkerCount()).toBeGreaterThan 0 + editor.destroy() + expect(buffer.getMarkerCount()).toBe 0 + + describe ".joinLine()", -> + describe "when no text is selected", -> + describe "when the line below isn't empty", -> + it "joins the line below with the current line separated by a space and moves the cursor to the start of line that was moved up", -> + editor.joinLine() + expect(editor.lineForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' + expect(editor.getCursorBufferPosition()).toEqual [0, 30] + + describe "when the line below is empty", -> + it "deletes the line below and moves the cursor to the end of the line", -> + editor.setCursorBufferPosition([9]) + editor.joinLine() + expect(editor.lineForBufferRow(9)).toBe ' };' + expect(editor.lineForBufferRow(10)).toBe ' return sort(Array.apply(this, arguments));' + expect(editor.getCursorBufferPosition()).toEqual [9, 4] + + describe "when the cursor is on the last row", -> + it "does nothing", -> + editor.setCursorBufferPosition([Infinity, Infinity]) + editor.joinLine() + expect(editor.lineForBufferRow(12)).toBe '};' + + describe "when text is selected", -> + describe "when the selection does not span multiple lines", -> + it "joins the line below with the current line separated by a space and retains the selected text", -> + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + editor.joinLine() + expect(editor.lineForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' + expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] + + describe "when the selection spans multiple lines", -> + it "joins all selected lines separated by a space and retains the selected text", -> + editor.setSelectedBufferRange([[9, 3], [12, 1]]) + editor.joinLine() + expect(editor.lineForBufferRow(9)).toBe ' }; return sort(Array.apply(this, arguments)); };' + expect(editor.getSelectedBufferRange()).toEqual [[9, 3], [9, 49]] + + describe ".shouldPromptToSave()", -> + it "returns false when an edit session's buffer is in use by more than one session", -> + expect(editor.shouldPromptToSave()).toBeFalsy() + buffer.setText('changed') + expect(editor.shouldPromptToSave()).toBeTruthy() + editor2 = project.openSync('sample.js', autoIndent: false) + expect(editor.shouldPromptToSave()).toBeFalsy() + editor2.destroy() + expect(editor.shouldPromptToSave()).toBeTruthy() + + describe "when the edit session contains surrogate pair characters", -> + it "correctly backspaces over them", -> + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') + editor.moveCursorToBottom() + editor.backspace() + expect(editor.getText()).toBe '\uD835\uDF97\uD835\uDF97' + editor.backspace() + expect(editor.getText()).toBe '\uD835\uDF97' + editor.backspace() + expect(editor.getText()).toBe '' + + it "correctly deletes over them", -> + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') + editor.moveCursorToTop() + editor.delete() + expect(editor.getText()).toBe '\uD835\uDF97\uD835\uDF97' + editor.delete() + expect(editor.getText()).toBe '\uD835\uDF97' + editor.delete() + expect(editor.getText()).toBe '' + + it "correctly moves over them", -> + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97\n') + editor.moveCursorToTop() + editor.moveCursorRight() + expect(editor.getCursorBufferPosition()).toEqual [0, 2] + editor.moveCursorRight() + expect(editor.getCursorBufferPosition()).toEqual [0, 4] + editor.moveCursorRight() + expect(editor.getCursorBufferPosition()).toEqual [0, 6] + editor.moveCursorRight() + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + editor.moveCursorLeft() + expect(editor.getCursorBufferPosition()).toEqual [0, 6] + editor.moveCursorLeft() + expect(editor.getCursorBufferPosition()).toEqual [0, 4] + editor.moveCursorLeft() + expect(editor.getCursorBufferPosition()).toEqual [0, 2] + editor.moveCursorLeft() + expect(editor.getCursorBufferPosition()).toEqual [0, 0] diff --git a/spec/editor-view-spec.coffee b/spec/editor-view-spec.coffee new file mode 100644 index 000000000..7b1dc5493 --- /dev/null +++ b/spec/editor-view-spec.coffee @@ -0,0 +1,2825 @@ +RootView = require '../src/root-view' +EditorView = require '../src/editor-view' +{$, $$} = require '../src/space-pen-extensions' +_ = require 'underscore-plus' +fs = require 'fs-plus' +path = require 'path' +temp = require 'temp' + +describe "EditorView", -> + [buffer, editorView, editor, cachedLineHeight, cachedCharWidth] = [] + + beforeEach -> + atom.activatePackage('language-text', sync: true) + atom.activatePackage('language-javascript', sync: true) + editor = project.openSync('sample.js') + buffer = editor.buffer + editorView = new EditorView(editor) + editorView.lineOverdraw = 2 + editorView.isFocused = true + editorView.enableKeymap() + editorView.calculateHeightInLines = -> + Math.ceil(@height() / @lineHeight) + editorView.attachToDom = ({ heightInLines, widthInChars } = {}) -> + heightInLines ?= @getBuffer().getLineCount() + @height(getLineHeight() * heightInLines) + @width(getCharWidth() * widthInChars) if widthInChars + $('#jasmine-content').append(this) + + getLineHeight = -> + return cachedLineHeight if cachedLineHeight? + calcDimensions() + cachedLineHeight + + getCharWidth = -> + return cachedCharWidth if cachedCharWidth? + calcDimensions() + cachedCharWidth + + calcDimensions = -> + editorForMeasurement = new EditorView(editor: project.openSync('sample.js')) + editorForMeasurement.attachToDom() + cachedLineHeight = editorForMeasurement.lineHeight + cachedCharWidth = editorForMeasurement.charWidth + editorForMeasurement.remove() + + describe "construction", -> + it "throws an error if no edit session is given", -> + expect(-> new EditorView).toThrow() + + describe "when the editor view view is attached to the dom", -> + it "calculates line height and char width and updates the pixel position of the cursor", -> + expect(editorView.lineHeight).toBeNull() + expect(editorView.charWidth).toBeNull() + editorView.setCursorScreenPosition(row: 2, column: 2) + + editorView.attachToDom() + + expect(editorView.lineHeight).not.toBeNull() + expect(editorView.charWidth).not.toBeNull() + expect(editorView.find('.cursor').offset()).toEqual pagePixelPositionForPoint(editorView, [2, 2]) + + it "is focused", -> + editorView.attachToDom() + expect(editorView).toMatchSelector ":has(:focus)" + + describe "when the editor view view receives focus", -> + it "focuses the hidden input", -> + editorView.attachToDom() + editorView.focus() + expect(editorView).not.toMatchSelector ':focus' + expect(editorView.hiddenInput).toMatchSelector ':focus' + + it "does not scroll the editor view (regression)", -> + editorView.attachToDom(heightInLines: 2) + editorView.selectAll() + editorView.hiddenInput.blur() + editorView.focus() + + expect(editorView.hiddenInput).toMatchSelector ':focus' + expect($(editorView[0]).scrollTop()).toBe 0 + expect($(editorView.scrollView[0]).scrollTop()).toBe 0 + + editorView.moveCursorToBottom() + editorView.hiddenInput.blur() + editorView.scrollTop(0) + editorView.focus() + + expect(editorView.hiddenInput).toMatchSelector ':focus' + expect($(editorView[0]).scrollTop()).toBe 0 + expect($(editorView.scrollView[0]).scrollTop()).toBe 0 + + describe "when the hidden input is focused / unfocused", -> + it "assigns the isFocused flag on the editor view view and also adds/removes the .focused css class", -> + editorView.attachToDom() + editorView.isFocused = false + editorView.hiddenInput.focus() + expect(editorView.isFocused).toBeTruthy() + + editorView.hiddenInput.focusout() + expect(editorView.isFocused).toBeFalsy() + + describe "when the activeEditSession's file is modified on disk", -> + it "triggers an alert", -> + filePath = path.join(temp.dir, 'atom-changed-file.txt') + fs.writeFileSync(filePath, "") + editor = project.openSync(filePath) + editorView.edit(editor) + editorView.insertText("now the buffer is modified") + + fileChangeHandler = jasmine.createSpy('fileChange') + editor.buffer.file.on 'contents-changed', fileChangeHandler + + spyOn(atom, "confirm") + + fs.writeFileSync(filePath, "a file change") + + waitsFor "file to trigger contents-changed event", -> + fileChangeHandler.callCount > 0 + + runs -> + expect(atom.confirm).toHaveBeenCalled() + + describe ".remove()", -> + it "destroys the edit session", -> + editorView.remove() + expect(editorView.activeEditSession.destroyed).toBeTruthy() + + describe ".edit(editor)", -> + [newEditSession, newBuffer] = [] + + beforeEach -> + newEditSession = project.openSync('two-hundred.txt') + newBuffer = newEditSession.buffer + + it "updates the rendered lines, cursors, selections, scroll position, and event subscriptions to match the given edit session", -> + editorView.attachToDom(heightInLines: 5, widthInChars: 30) + editorView.setCursorBufferPosition([6, 13]) + editorView.scrollToBottom() + editorView.scrollLeft(150) + previousScrollHeight = editorView.verticalScrollbar.prop('scrollHeight') + previousScrollTop = editorView.scrollTop() + previousScrollLeft = editorView.scrollLeft() + + newEditSession.setScrollTop(900) + newEditSession.setSelectedBufferRange([[40, 0], [43, 1]]) + + editorView.edit(newEditSession) + { firstRenderedScreenRow, lastRenderedScreenRow } = editorView + expect(editorView.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe newBuffer.lineForRow(firstRenderedScreenRow) + expect(editorView.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe newBuffer.lineForRow(editorView.lastRenderedScreenRow) + expect(editorView.scrollTop()).toBe 900 + expect(editorView.scrollLeft()).toBe 0 + expect(editorView.getSelectionView().regions[0].position().top).toBe 40 * editorView.lineHeight + editorView.insertText("hello") + expect(editorView.lineElementForScreenRow(40).text()).toBe "hello3" + + editorView.edit(editor) + { firstRenderedScreenRow, lastRenderedScreenRow } = editorView + expect(editorView.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe buffer.lineForRow(firstRenderedScreenRow) + expect(editorView.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe buffer.lineForRow(editorView.lastRenderedScreenRow) + expect(editorView.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight + expect(editorView.scrollTop()).toBe previousScrollTop + expect(editorView.scrollLeft()).toBe previousScrollLeft + expect(editorView.getCursorView().position()).toEqual { top: 6 * editorView.lineHeight, left: 13 * editorView.charWidth } + editorView.insertText("goodbye") + expect(editorView.lineElementForScreenRow(6).text()).toMatch /^ currentgoodbye/ + + it "triggers alert if edit session's buffer goes into conflict with changes on disk", -> + filePath = path.join(temp.dir, 'atom-changed-file.txt') + fs.writeFileSync(filePath, "") + tempEditSession = project.openSync(filePath) + editorView.edit(tempEditSession) + tempEditSession.insertText("a buffer change") + + spyOn(atom, "confirm") + + contentsConflictedHandler = jasmine.createSpy("contentsConflictedHandler") + tempEditSession.on 'contents-conflicted', contentsConflictedHandler + fs.writeFileSync(filePath, "a file change") + waitsFor -> + contentsConflictedHandler.callCount > 0 + + runs -> + expect(atom.confirm).toHaveBeenCalled() + + describe ".scrollTop(n)", -> + beforeEach -> + editorView.attachToDom(heightInLines: 5) + expect(editorView.verticalScrollbar.scrollTop()).toBe 0 + + describe "when called with a scroll top argument", -> + it "sets the scrollTop of the vertical scrollbar and sets scrollTop on the line numbers and lines", -> + editorView.scrollTop(100) + expect(editorView.verticalScrollbar.scrollTop()).toBe 100 + expect(editorView.scrollView.scrollTop()).toBe 0 + expect(editorView.renderedLines.css('top')).toBe "-100px" + expect(editorView.gutter.lineNumbers.css('top')).toBe "-100px" + + editorView.scrollTop(120) + expect(editorView.verticalScrollbar.scrollTop()).toBe 120 + expect(editorView.scrollView.scrollTop()).toBe 0 + expect(editorView.renderedLines.css('top')).toBe "-120px" + expect(editorView.gutter.lineNumbers.css('top')).toBe "-120px" + + it "does not allow negative scrollTops to be assigned", -> + editorView.scrollTop(-100) + expect(editorView.scrollTop()).toBe 0 + + it "doesn't do anything if the scrollTop hasn't changed", -> + editorView.scrollTop(100) + spyOn(editorView.verticalScrollbar, 'scrollTop') + spyOn(editorView.renderedLines, 'css') + spyOn(editorView.gutter.lineNumbers, 'css') + + editorView.scrollTop(100) + expect(editorView.verticalScrollbar.scrollTop).not.toHaveBeenCalled() + expect(editorView.renderedLines.css).not.toHaveBeenCalled() + expect(editorView.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", -> + editorView.scrollTop(100, adjustVerticalScrollbar: false) + expect(editorView.verticalScrollbar.scrollTop()).toBe 0 + expect(editorView.renderedLines.css('top')).toBe "-100px" + expect(editorView.gutter.lineNumbers.css('top')).toBe "-100px" + + describe "when called with no argument", -> + it "returns the last assigned value or 0 if none has been assigned", -> + expect(editorView.scrollTop()).toBe 0 + editorView.scrollTop(50) + expect(editorView.scrollTop()).toBe 50 + + it "sets the new scroll top position on the active edit session", -> + expect(editorView.activeEditSession.getScrollTop()).toBe 0 + editorView.scrollTop(123) + expect(editorView.activeEditSession.getScrollTop()).toBe 123 + + describe ".scrollHorizontally(pixelPosition)", -> + it "sets the new scroll left position on the active edit session", -> + editorView.attachToDom(heightInLines: 5) + setEditorWidthInChars(editorView, 5) + expect(editorView.activeEditSession.getScrollLeft()).toBe 0 + editorView.scrollHorizontally(left: 50) + expect(editorView.activeEditSession.getScrollLeft()).toBeGreaterThan 0 + expect(editorView.activeEditSession.getScrollLeft()).toBe editorView.scrollLeft() + + describe "editor:attached event", -> + it 'only triggers an editor:attached event when it is first added to the DOM', -> + openHandler = jasmine.createSpy('openHandler') + editorView.on 'editor:attached', openHandler + + editorView.attachToDom() + expect(openHandler).toHaveBeenCalled() + [event, eventEditor] = openHandler.argsForCall[0] + expect(eventEditor).toBe editorView + + openHandler.reset() + editorView.attachToDom() + expect(openHandler).not.toHaveBeenCalled() + + describe "editor:path-changed event", -> + filePath = null + + beforeEach -> + filePath = path.join(temp.dir, 'something.txt') + fs.writeFileSync(filePath, filePath) + + afterEach -> + fs.removeSync(filePath) if fs.existsSync(filePath) + + it "emits event when buffer's path is changed", -> + eventHandler = jasmine.createSpy('eventHandler') + editorView.on 'editor:path-changed', eventHandler + editorView.getBuffer().saveAs(filePath) + expect(eventHandler).toHaveBeenCalled() + + it "emits event when editor view view receives a new buffer", -> + eventHandler = jasmine.createSpy('eventHandler') + editorView.on 'editor:path-changed', eventHandler + editorView.edit(project.openSync(filePath)) + expect(eventHandler).toHaveBeenCalled() + + it "stops listening to events on previously set buffers", -> + eventHandler = jasmine.createSpy('eventHandler') + oldBuffer = editorView.getBuffer() + editorView.on 'editor:path-changed', eventHandler + + editorView.edit(project.openSync(filePath)) + expect(eventHandler).toHaveBeenCalled() + + eventHandler.reset() + oldBuffer.saveAs(path.join(temp.dir, 'atom-bad.txt')) + expect(eventHandler).not.toHaveBeenCalled() + + eventHandler.reset() + editorView.getBuffer().saveAs(path.join(temp.dir, 'atom-new.txt')) + expect(eventHandler).toHaveBeenCalled() + + it "loads the grammar for the new path", -> + expect(editorView.getGrammar().name).toBe 'JavaScript' + editorView.getBuffer().saveAs(filePath) + expect(editorView.getGrammar().name).toBe 'Plain Text' + + describe "font family", -> + beforeEach -> + expect(editorView.css('font-family')).toBe 'Courier' + + it "when there is no config in fontFamily don't set it", -> + atom.config.set('editor.fontFamily', null) + expect(editorView.css('font-family')).toBe '' + + describe "when the font family changes", -> + [fontFamily] = [] + + beforeEach -> + if process.platform is 'darwin' + fontFamily = "PCMyungjo" + else + fontFamily = "Consolas" + + it "updates the font family of editors and recalculates dimensions critical to cursor positioning", -> + editorView.attachToDom(12) + lineHeightBefore = editorView.lineHeight + charWidthBefore = editorView.charWidth + editorView.setCursorScreenPosition [5, 6] + + atom.config.set("editor.fontFamily", fontFamily) + expect(editorView.css('font-family')).toBe fontFamily + expect(editorView.charWidth).not.toBe charWidthBefore + expect(editorView.getCursorView().position()).toEqual { top: 5 * editorView.lineHeight, left: 6 * editorView.charWidth } + + newEditor = new EditorView(editorView.activeEditSession.copy()) + newEditor.attachToDom() + expect(newEditor.css('font-family')).toBe fontFamily + + describe "font size", -> + beforeEach -> + expect(editorView.css('font-size')).not.toBe "20px" + expect(editorView.css('font-size')).not.toBe "10px" + + it "sets the initial font size based on the value from config", -> + expect(editorView.css('font-size')).toBe "#{atom.config.get('editor.fontSize')}px" + + describe "when the font size changes", -> + it "updates the font sizes of editors and recalculates dimensions critical to cursor positioning", -> + atom.config.set("editor.fontSize", 10) + editorView.attachToDom() + lineHeightBefore = editorView.lineHeight + charWidthBefore = editorView.charWidth + editorView.setCursorScreenPosition [5, 6] + + atom.config.set("editor.fontSize", 30) + expect(editorView.css('font-size')).toBe '30px' + expect(editorView.lineHeight).toBeGreaterThan lineHeightBefore + expect(editorView.charWidth).toBeGreaterThan charWidthBefore + expect(editorView.getCursorView().position()).toEqual { top: 5 * editorView.lineHeight, left: 6 * editorView.charWidth } + expect(editorView.renderedLines.outerHeight()).toBe buffer.getLineCount() * editorView.lineHeight + expect(editorView.verticalScrollbarContent.height()).toBe buffer.getLineCount() * editorView.lineHeight + + newEditor = new EditorView(editorView.activeEditSession.copy()) + editorView.remove() + newEditor.attachToDom() + expect(newEditor.css('font-size')).toBe '30px' + + it "updates the position and size of selection regions", -> + atom.config.set("editor.fontSize", 10) + editorView.setSelectedBufferRange([[5, 2], [5, 7]]) + editorView.attachToDom() + + atom.config.set("editor.fontSize", 30) + selectionRegion = editorView.find('.region') + expect(selectionRegion.position().top).toBe 5 * editorView.lineHeight + expect(selectionRegion.position().left).toBe 2 * editorView.charWidth + expect(selectionRegion.height()).toBe editorView.lineHeight + expect(selectionRegion.width()).toBe 5 * editorView.charWidth + + it "updates lines if there are unrendered lines", -> + editorView.attachToDom(heightInLines: 5) + originalLineCount = editorView.renderedLines.find(".line").length + expect(originalLineCount).toBeGreaterThan 0 + + atom.config.set("editor.fontSize", 10) + expect(editorView.renderedLines.find(".line").length).toBeGreaterThan originalLineCount + + describe "when the font size changes while editor view view is detached", -> + it "redraws the editor view view according to the new font size when it is reattached", -> + editorView.setCursorScreenPosition([4, 2]) + editorView.attachToDom() + initialLineHeight = editorView.lineHeight + initialCharWidth = editorView.charWidth + initialCursorPosition = editorView.getCursorView().position() + initialScrollbarHeight = editorView.verticalScrollbarContent.height() + editorView.detach() + + atom.config.set("editor.fontSize", 10) + expect(editorView.lineHeight).toBe initialLineHeight + expect(editorView.charWidth).toBe initialCharWidth + + editorView.attachToDom() + expect(editorView.lineHeight).not.toBe initialLineHeight + expect(editorView.charWidth).not.toBe initialCharWidth + expect(editorView.getCursorView().position()).not.toEqual initialCursorPosition + expect(editorView.verticalScrollbarContent.height()).not.toBe initialScrollbarHeight + + describe "mouse events", -> + beforeEach -> + editorView.attachToDom() + editorView.css(position: 'absolute', top: 10, left: 10, width: 400) + + describe "single-click", -> + it "re-positions the cursor to the clicked row / column", -> + expect(editorView.getCursorScreenPosition()).toEqual(row: 0, column: 0) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 10]) + expect(editorView.getCursorScreenPosition()).toEqual(row: 3, column: 10) + + describe "when the lines are scrolled to the right", -> + it "re-positions the cursor on the clicked location", -> + setEditorWidthInChars(editorView, 30) + expect(editorView.getCursorScreenPosition()).toEqual(row: 0, column: 0) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 30]) # scrolls lines to the right + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 50]) + expect(editorView.getCursorBufferPosition()).toEqual(row: 3, column: 50) + + describe "when the editor view view is using a variable-width font", -> + beforeEach -> + editorView.setFontFamily('sans-serif') + + it "positions the cursor to the clicked row and column", -> + {top, left} = editorView.pixelOffsetForScreenPosition([3, 30]) + editorView.renderedLines.trigger mousedownEvent(pageX: left, pageY: top) + expect(editorView.getCursorScreenPosition()).toEqual [3, 30] + + describe "double-click", -> + it "selects the word under the cursor, and expands the selection wordwise in either direction on a subsequent shift-click", -> + expect(editorView.getCursorScreenPosition()).toEqual(row: 0, column: 0) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [8, 24], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [8, 24], originalEvent: {detail: 2}) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedText()).toBe "concat" + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [8, 7], shiftKey: true) + editorView.renderedLines.trigger 'mouseup' + + expect(editorView.getSelectedText()).toBe "return sort(left).concat" + + it "stops selecting by word when the selection is emptied", -> + expect(editorView.getCursorScreenPosition()).toEqual(row: 0, column: 0) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 2}) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedText()).toBe "quicksort" + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 10]) + editorView.renderedLines.trigger 'mouseup' + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 12], originalEvent: {detail: 1}, shiftKey: true) + expect(editorView.getSelectedBufferRange()).toEqual [[3, 10], [3, 12]] + + describe "when clicking between a word and a non-word", -> + it "selects the word", -> + expect(editorView.getCursorScreenPosition()).toEqual(row: 0, column: 0) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 21], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 21], originalEvent: {detail: 2}) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedText()).toBe "function" + + editorView.setCursorBufferPosition([0, 0]) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 22], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 22], originalEvent: {detail: 2}) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedText()).toBe "items" + + editorView.setCursorBufferPosition([0, 0]) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 28], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 28], originalEvent: {detail: 2}) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedText()).toBe "{" + + describe "triple/quardruple/etc-click", -> + it "selects the line under the cursor", -> + expect(editorView.getCursorScreenPosition()).toEqual(row: 0, column: 0) + + # Triple click + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 8], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 8], originalEvent: {detail: 2}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 8], originalEvent: {detail: 3}) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedText()).toBe " var sort = function(items) {\n" + + # Quad click + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 3], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 3], originalEvent: {detail: 2}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 3], originalEvent: {detail: 3}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 3], originalEvent: {detail: 4}) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedText()).toBe " if (items.length <= 1) return items;\n" + + it "expands the selection linewise in either direction on a subsequent shift-click, but stops selecting linewise once the selection is emptied", -> + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 8], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 8], originalEvent: {detail: 2}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 8], originalEvent: {detail: 3}) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedBufferRange()).toEqual [[4, 0], [5, 0]] + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 8], originalEvent: {detail: 1}, shiftKey: true) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedBufferRange()).toEqual [[1, 0], [5, 0]] + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 8], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelection().isEmpty()).toBeTruthy() + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 8], originalEvent: {detail: 1}, shiftKey: true) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedBufferRange()).toEqual [[2, 8], [3, 8]] + + describe "shift-click", -> + it "selects from the cursor's current location to the clicked location", -> + editorView.setCursorScreenPosition([4, 7]) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true) + expect(editorView.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] + + describe "shift-double-click", -> + it "expands the selection on the first click and ignores the second click", -> + editorView.setCursorScreenPosition([4, 7]) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 }) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 }) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] + + describe "shift-triple-click", -> + it "expands the selection on the first click and ignores the second click", -> + editorView.setCursorScreenPosition([4, 7]) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 }) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 }) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 3 }) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] + + describe "meta-click", -> + it "places an additional cursor", -> + editorView.attachToDom() + setEditorHeightInLines(editorView, 5) + editorView.setCursorBufferPosition([3, 0]) + editorView.scrollTop(editorView.lineHeight * 6) + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [6, 0], metaKey: true) + expect(editorView.scrollTop()).toBe editorView.lineHeight * (6 - editorView.vScrollMargin) + + [cursor1, cursor2] = editorView.getCursorViews() + expect(cursor1.position()).toEqual(top: 3 * editorView.lineHeight, left: 0) + expect(cursor1.getBufferPosition()).toEqual [3, 0] + expect(cursor2.position()).toEqual(top: 6 * editorView.lineHeight, left: 0) + expect(cursor2.getBufferPosition()).toEqual [6, 0] + + describe "click and drag", -> + it "creates a selection from the initial click to mouse cursor's location ", -> + editorView.attachToDom() + editorView.css(position: 'absolute', top: 10, left: 10) + + # start + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 10]) + + # moving changes selection + $(document).trigger mousemoveEvent(editorView: editorView, point: [5, 27]) + + range = editorView.getSelection().getScreenRange() + expect(range.start).toEqual({row: 4, column: 10}) + expect(range.end).toEqual({row: 5, column: 27}) + expect(editorView.getCursorScreenPosition()).toEqual(row: 5, column: 27) + + # mouse up may occur outside of editorView, but still need to halt selection + $(document).trigger 'mouseup' + + # moving after mouse up should not change selection + editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [8, 8]) + + range = editorView.getSelection().getScreenRange() + expect(range.start).toEqual({row: 4, column: 10}) + expect(range.end).toEqual({row: 5, column: 27}) + expect(editorView.getCursorScreenPosition()).toEqual(row: 5, column: 27) + + it "selects and scrolls if the mouse is dragged outside of the editor view view itself", -> + editorView.vScrollMargin = 0 + editorView.attachToDom(heightInLines: 5) + editorView.scrollToBottom() + + spyOn(window, 'setInterval').andCallFake -> + + # start + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [12, 0]) + originalScrollTop = editorView.scrollTop() + + # moving changes selection + $(document).trigger mousemoveEvent(editorView: editorView, pageX: 0, pageY: -1) + expect(editorView.scrollTop()).toBe originalScrollTop - editorView.lineHeight + + # every mouse move selects more text + for x in [0..10] + $(document).trigger mousemoveEvent(editorView: editorView, pageX: 0, pageY: -1) + + expect(editorView.scrollTop()).toBe 0 + + it "ignores non left-click and drags", -> + editorView.attachToDom() + editorView.css(position: 'absolute', top: 10, left: 10) + + event = mousedownEvent(editorView: editorView, point: [4, 10]) + event.originalEvent.which = 2 + editorView.renderedLines.trigger(event) + $(document).trigger mousemoveEvent(editorView: editorView, point: [5, 27]) + $(document).trigger 'mouseup' + + range = editorView.getSelection().getScreenRange() + expect(range.start).toEqual({row: 4, column: 10}) + expect(range.end).toEqual({row: 4, column: 10}) + + it "ignores ctrl-click and drags", -> + editorView.attachToDom() + editorView.css(position: 'absolute', top: 10, left: 10) + + event = mousedownEvent(editorView: editorView, point: [4, 10]) + event.ctrlKey = true + editorView.renderedLines.trigger(event) + $(document).trigger mousemoveEvent(editorView: editorView, point: [5, 27]) + $(document).trigger 'mouseup' + + range = editorView.getSelection().getScreenRange() + expect(range.start).toEqual({row: 4, column: 10}) + expect(range.end).toEqual({row: 4, column: 10}) + + describe "double-click and drag", -> + it "selects the word under the cursor, then continues to select by word in either direction as the mouse is dragged", -> + expect(editorView.getCursorScreenPosition()).toEqual(row: 0, column: 0) + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 1}) + editorView.renderedLines.trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 2}) + expect(editorView.getSelectedText()).toBe "quicksort" + + editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [1, 8]) + expect(editorView.getSelectedBufferRange()).toEqual [[0, 4], [1, 10]] + expect(editorView.getCursorBufferPosition()).toEqual [1, 10] + + editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [0, 1]) + expect(editorView.getSelectedBufferRange()).toEqual [[0, 0], [0, 13]] + expect(editorView.getCursorBufferPosition()).toEqual [0, 0] + + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedBufferRange()).toEqual [[0, 0], [0, 13]] + + # shift-clicking still selects by word, but does not preserve the initial range + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 25], originalEvent: {detail: 1}, shiftKey: true) + editorView.renderedLines.trigger 'mouseup' + expect(editorView.getSelectedBufferRange()).toEqual [[0, 13], [5, 27]] + + describe "triple-click and drag", -> + it "expands the initial selection linewise in either direction", -> + editorView.attachToDom() + + # triple click + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 7], originalEvent: {detail: 1}) + $(document).trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 7], originalEvent: {detail: 2}) + $(document).trigger 'mouseup' + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 7], originalEvent: {detail: 3}) + expect(editorView.getSelectedBufferRange()).toEqual [[4, 0], [5, 0]] + + # moving changes selection linewise + editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [5, 27]) + expect(editorView.getSelectedBufferRange()).toEqual [[4, 0], [6, 0]] + expect(editorView.getCursorBufferPosition()).toEqual [6, 0] + + # moving changes selection linewise + editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [2, 27]) + expect(editorView.getSelectedBufferRange()).toEqual [[2, 0], [5, 0]] + expect(editorView.getCursorBufferPosition()).toEqual [2, 0] + + # mouse up may occur outside of editorView, but still need to halt selection + $(document).trigger 'mouseup' + + describe "meta-click and drag", -> + it "adds an additional selection", -> + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 10]) + editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [5, 27]) + editorView.renderedLines.trigger 'mouseup' + + editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [6, 10], metaKey: true) + editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [8, 27], metaKey: true) + editorView.renderedLines.trigger 'mouseup' + + selections = editorView.getSelections() + expect(selections.length).toBe 2 + [selection1, selection2] = selections + expect(selection1.getScreenRange()).toEqual [[4, 10], [5, 27]] + expect(selection2.getScreenRange()).toEqual [[6, 10], [8, 27]] + + describe "when text input events are triggered on the hidden input element", -> + it "inserts the typed character at the cursor position, both in the buffer and the pre element", -> + editorView.attachToDom() + editorView.setCursorScreenPosition(row: 1, column: 6) + + expect(buffer.lineForRow(1).charAt(6)).not.toBe 'q' + + editorView.hiddenInput.textInput 'q' + + expect(buffer.lineForRow(1).charAt(6)).toBe 'q' + expect(editorView.getCursorScreenPosition()).toEqual(row: 1, column: 7) + expect(editorView.renderedLines.find('.line:eq(1)')).toHaveText buffer.lineForRow(1) + + describe "selection rendering", -> + [charWidth, lineHeight, selection, selectionView] = [] + + beforeEach -> + editorView.attachToDom() + editorView.width(500) + { charWidth, lineHeight } = editorView + selection = editorView.getSelection() + selectionView = editorView.getSelectionView() + + describe "when a selection is added", -> + it "adds a selection view for it with the proper regions", -> + editorView.activeEditSession.addSelectionForBufferRange([[2, 7], [2, 25]]) + selectionViews = editorView.getSelectionViews() + expect(selectionViews.length).toBe 2 + expect(selectionViews[1].regions.length).toBe 1 + region = selectionViews[1].regions[0] + expect(region.position().top).toBeCloseTo(2 * lineHeight) + expect(region.position().left).toBeCloseTo(7 * charWidth) + expect(region.height()).toBeCloseTo lineHeight + expect(region.width()).toBeCloseTo((25 - 7) * charWidth) + + describe "when a selection changes", -> + describe "when the selection is within a single line", -> + it "covers the selection's range with a single region", -> + selection.setBufferRange([[2, 7], [2, 25]]) + + expect(selectionView.regions.length).toBe 1 + region = selectionView.regions[0] + expect(region.position().top).toBeCloseTo(2 * lineHeight) + expect(region.position().left).toBeCloseTo(7 * charWidth) + expect(region.height()).toBeCloseTo lineHeight + expect(region.width()).toBeCloseTo((25 - 7) * charWidth) + + describe "when the selection spans 2 lines", -> + it "covers the selection's range with 2 regions", -> + selection.setBufferRange([[2,7],[3,25]]) + + expect(selectionView.regions.length).toBe 2 + + region1 = selectionView.regions[0] + expect(region1.position().top).toBeCloseTo(2 * lineHeight) + expect(region1.position().left).toBeCloseTo(7 * charWidth) + expect(region1.height()).toBeCloseTo lineHeight + + expect(region1.width()).toBeCloseTo(editorView.renderedLines.outerWidth() - region1.position().left) + region2 = selectionView.regions[1] + expect(region2.position().top).toBeCloseTo(3 * lineHeight) + expect(region2.position().left).toBeCloseTo(0) + expect(region2.height()).toBeCloseTo lineHeight + expect(region2.width()).toBeCloseTo(25 * charWidth) + + describe "when the selection spans more than 2 lines", -> + it "covers the selection's range with 3 regions", -> + selection.setBufferRange([[2,7],[6,25]]) + + expect(selectionView.regions.length).toBe 3 + + region1 = selectionView.regions[0] + expect(region1.position().top).toBeCloseTo(2 * lineHeight) + expect(region1.position().left).toBeCloseTo(7 * charWidth) + expect(region1.height()).toBeCloseTo lineHeight + + expect(region1.width()).toBeCloseTo(editorView.renderedLines.outerWidth() - region1.position().left) + region2 = selectionView.regions[1] + expect(region2.position().top).toBeCloseTo(3 * lineHeight) + expect(region2.position().left).toBeCloseTo(0) + expect(region2.height()).toBeCloseTo(3 * lineHeight) + expect(region2.width()).toBeCloseTo(editorView.renderedLines.outerWidth()) + + # resizes with the editorView + expect(editorView.width()).toBeLessThan(800) + editorView.width(800) + editorView.resize() # call to trigger the resize event. + + region2 = selectionView.regions[1] + expect(region2.width()).toBe(editorView.renderedLines.outerWidth()) + + region3 = selectionView.regions[2] + expect(region3.position().top).toBeCloseTo(6 * lineHeight) + expect(region3.position().left).toBeCloseTo(0) + expect(region3.height()).toBeCloseTo lineHeight + expect(region3.width()).toBeCloseTo(25 * charWidth) + + it "clears previously drawn regions before creating new ones", -> + selection.setBufferRange([[2,7],[4,25]]) + expect(selectionView.regions.length).toBe 3 + expect(selectionView.find('.region').length).toBe 3 + + selectionView.updateDisplay() + expect(selectionView.regions.length).toBe 3 + expect(selectionView.find('.region').length).toBe 3 + + describe "when a selection merges with another selection", -> + it "removes the merged selection view", -> + editor = editorView.activeEditSession + editor.setCursorScreenPosition([4, 10]) + editor.selectToScreenPosition([5, 27]) + editor.addCursorAtScreenPosition([3, 10]) + editor.selectToScreenPosition([6, 27]) + + expect(editorView.getSelectionViews().length).toBe 1 + expect(editorView.find('.region').length).toBe 3 + + describe "when a selection is added and removed before the display is updated", -> + it "does not attempt to render the selection", -> + # don't update display until we request it + jasmine.unspy(editorView, 'requestDisplayUpdate') + spyOn(editorView, 'requestDisplayUpdate') + + editor = editorView.activeEditSession + selection = editor.addSelectionForBufferRange([[3, 0], [3, 4]]) + selection.destroy() + editorView.updateDisplay() + expect(editorView.getSelectionViews().length).toBe 1 + + describe "when the selection is created with the selectAll event", -> + it "does not scroll to the end of the buffer", -> + editorView.height(150) + editorView.selectAll() + expect(editorView.scrollTop()).toBe 0 + + # regression: does not scroll the scroll view when the editorView is refocused + editorView.hiddenInput.blur() + editorView.hiddenInput.focus() + expect(editorView.scrollTop()).toBe 0 + expect(editorView.scrollView.scrollTop()).toBe 0 + + # does autoscroll when the selection is cleared + editorView.moveCursorDown() + expect(editorView.scrollTop()).toBeGreaterThan(0) + + describe "selection autoscrolling and highlighting when setting selected buffer range", -> + beforeEach -> + setEditorHeightInLines(editorView, 4) + + describe "if autoscroll is true", -> + it "centers the viewport on the selection if its vertical center is currently offscreen", -> + editorView.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) + expect(editorView.scrollTop()).toBe 0 + + editorView.setSelectedBufferRange([[6, 0], [8, 0]], autoscroll: true) + expect(editorView.scrollTop()).toBe 5 * editorView.lineHeight + + it "highlights the selection if autoscroll is true", -> + editorView.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) + expect(editorView.getSelectionView()).toHaveClass 'highlighted' + advanceClock(1000) + expect(editorView.getSelectionView()).not.toHaveClass 'highlighted' + + editorView.setSelectedBufferRange([[3, 0], [5, 0]], autoscroll: true) + expect(editorView.getSelectionView()).toHaveClass 'highlighted' + + advanceClock(500) + spyOn(editorView.getSelectionView(), 'removeClass').andCallThrough() + editorView.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) + expect(editorView.getSelectionView().removeClass).toHaveBeenCalledWith('highlighted') + expect(editorView.getSelectionView()).toHaveClass 'highlighted' + + advanceClock(500) + expect(editorView.getSelectionView()).toHaveClass 'highlighted' + + describe "if autoscroll is false", -> + it "does not scroll to the selection or the cursor", -> + editorView.scrollToBottom() + scrollTopBefore = editorView.scrollTop() + editorView.setSelectedBufferRange([[0, 0], [1, 0]], autoscroll: false) + expect(editorView.scrollTop()).toBe scrollTopBefore + + describe "if autoscroll is not specified", -> + it "autoscrolls to the cursor as normal", -> + editorView.scrollToBottom() + editorView.setSelectedBufferRange([[0, 0], [1, 0]]) + expect(editorView.scrollTop()).toBe 0 + + describe "cursor rendering", -> + describe "when the cursor moves", -> + charWidth = null + + beforeEach -> + editorView.attachToDom() + editorView.vScrollMargin = 3 + editorView.hScrollMargin = 5 + {charWidth} = editorView + + it "repositions the cursor's view on screen", -> + editorView.setCursorScreenPosition(row: 2, column: 2) + expect(editorView.getCursorView().position()).toEqual(top: 2 * editorView.lineHeight, left: 2 * editorView.charWidth) + + it "hides the cursor when the selection is non-empty, and shows it otherwise", -> + cursorView = editorView.getCursorView() + expect(editorView.getSelection().isEmpty()).toBeTruthy() + expect(cursorView).toBeVisible() + + editorView.setSelectedBufferRange([[0, 0], [3, 0]]) + expect(editorView.getSelection().isEmpty()).toBeFalsy() + expect(cursorView).toBeHidden() + + editorView.setCursorBufferPosition([1, 3]) + expect(editorView.getSelection().isEmpty()).toBeTruthy() + expect(cursorView).toBeVisible() + + it "moves the hiddenInput to the same position with cursor's view", -> + editorView.setCursorScreenPosition(row: 2, column: 2) + expect(editorView.getCursorView().offset()).toEqual(editorView.hiddenInput.offset()) + + describe "when the editor view is using a variable-width font", -> + beforeEach -> + editorView.setFontFamily('sans-serif') + + describe "on #darwin or #linux", -> + it "correctly positions the cursor", -> + editorView.setCursorBufferPosition([3, 30]) + expect(editorView.getCursorView().position()).toEqual {top: 3 * editorView.lineHeight, left: 178} + editorView.setCursorBufferPosition([3, Infinity]) + expect(editorView.getCursorView().position()).toEqual {top: 3 * editorView.lineHeight, left: 353} + + describe "on #win32", -> + it "correctly positions the cursor", -> + editorView.setCursorBufferPosition([3, 30]) + expect(editorView.getCursorView().position()).toEqual {top: 3 * editorView.lineHeight, left: 175} + editorView.setCursorBufferPosition([3, Infinity]) + expect(editorView.getCursorView().position()).toEqual {top: 3 * editorView.lineHeight, left: 346} + + describe "autoscrolling", -> + it "only autoscrolls when the last cursor is moved", -> + editorView.setCursorBufferPosition([11,0]) + editorView.addCursorAtBufferPosition([6,50]) + [cursor1, cursor2] = editorView.getCursors() + + spyOn(editorView, 'scrollToPixelPosition') + cursor1.setScreenPosition([10, 10]) + expect(editorView.scrollToPixelPosition).not.toHaveBeenCalled() + + cursor2.setScreenPosition([11, 11]) + expect(editorView.scrollToPixelPosition).toHaveBeenCalled() + + it "does not autoscroll if the 'autoscroll' option is false", -> + editorView.setCursorBufferPosition([11,0]) + spyOn(editorView, 'scrollToPixelPosition') + editorView.setCursorScreenPosition([10, 10], autoscroll: false) + expect(editorView.scrollToPixelPosition).not.toHaveBeenCalled() + + it "autoscrolls to cursor if autoscroll is true, even if the position does not change", -> + spyOn(editorView, 'scrollToPixelPosition') + editorView.setCursorScreenPosition([4, 10], autoscroll: false) + editorView.setCursorScreenPosition([4, 10]) + expect(editorView.scrollToPixelPosition).toHaveBeenCalled() + editorView.scrollToPixelPosition.reset() + + editorView.setCursorBufferPosition([4, 10]) + expect(editorView.scrollToPixelPosition).toHaveBeenCalled() + + it "does not autoscroll the cursor based on a buffer change, unless the buffer change was initiated by the cursor", -> + lastVisibleRow = editorView.getLastVisibleScreenRow() + editorView.addCursorAtBufferPosition([lastVisibleRow, 0]) + spyOn(editorView, 'scrollToPixelPosition') + buffer.insert([lastVisibleRow, 0], "\n\n") + expect(editorView.scrollToPixelPosition).not.toHaveBeenCalled() + editorView.insertText('\n\n') + expect(editorView.scrollToPixelPosition.callCount).toBe 1 + + it "autoscrolls on undo/redo", -> + spyOn(editorView, 'scrollToPixelPosition') + editorView.insertText('\n\n') + expect(editorView.scrollToPixelPosition.callCount).toBe 1 + editorView.undo() + expect(editorView.scrollToPixelPosition.callCount).toBe 2 + editorView.redo() + expect(editorView.scrollToPixelPosition.callCount).toBe 3 + + describe "when the last cursor exceeds the upper or lower scroll margins", -> + describe "when the editor view is taller than twice the vertical scroll margin", -> + it "sets the scrollTop so the cursor remains within the scroll margin", -> + setEditorHeightInLines(editorView, 10) + + _.times 6, -> editorView.moveCursorDown() + expect(editorView.scrollTop()).toBe(0) + + editorView.moveCursorDown() + expect(editorView.scrollTop()).toBe(editorView.lineHeight) + + editorView.moveCursorDown() + expect(editorView.scrollTop()).toBe(editorView.lineHeight * 2) + + _.times 3, -> editorView.moveCursorUp() + + editorView.moveCursorUp() + expect(editorView.scrollTop()).toBe(editorView.lineHeight) + + editorView.moveCursorUp() + expect(editorView.scrollTop()).toBe(0) + + describe "when the editor view is shorter than twice the vertical scroll margin", -> + it "sets the scrollTop based on a reduced scroll margin, which prevents a jerky tug-of-war between upper and lower scroll margins", -> + setEditorHeightInLines(editorView, 5) + + _.times 3, -> editorView.moveCursorDown() + + expect(editorView.scrollTop()).toBe(editorView.lineHeight) + + editorView.moveCursorUp() + expect(editorView.renderedLines.css('top')).toBe "0px" + + describe "when the last cursor exceeds the right or left scroll margins", -> + describe "when soft-wrap is disabled", -> + describe "when the editor view is wider than twice the horizontal scroll margin", -> + it "sets the scrollView's scrollLeft so the cursor remains within the scroll margin", -> + setEditorWidthInChars(editorView, 30) + + # moving right + editorView.setCursorScreenPosition([2, 24]) + expect(editorView.scrollLeft()).toBe 0 + + editorView.setCursorScreenPosition([2, 25]) + expect(editorView.scrollLeft()).toBe charWidth + + editorView.setCursorScreenPosition([2, 28]) + expect(editorView.scrollLeft()).toBe charWidth * 4 + + # moving left + editorView.setCursorScreenPosition([2, 9]) + expect(editorView.scrollLeft()).toBe charWidth * 4 + + editorView.setCursorScreenPosition([2, 8]) + expect(editorView.scrollLeft()).toBe charWidth * 3 + + editorView.setCursorScreenPosition([2, 5]) + expect(editorView.scrollLeft()).toBe 0 + + describe "when the editor view is narrower than twice the horizontal scroll margin", -> + it "sets the scrollView's scrollLeft based on a reduced horizontal scroll margin, to prevent a jerky tug-of-war between right and left scroll margins", -> + editorView.hScrollMargin = 6 + setEditorWidthInChars(editorView, 7) + + editorView.setCursorScreenPosition([2, 3]) + window.advanceClock() + expect(editorView.scrollLeft()).toBe(0) + + editorView.setCursorScreenPosition([2, 4]) + window.advanceClock() + expect(editorView.scrollLeft()).toBe(charWidth) + + editorView.setCursorScreenPosition([2, 3]) + window.advanceClock() + expect(editorView.scrollLeft()).toBe(0) + + describe "when soft-wrap is enabled", -> + beforeEach -> + editor.setSoftWrap(true) + + it "does not scroll the buffer horizontally", -> + editorView.width(charWidth * 30) + + # moving right + editorView.setCursorScreenPosition([2, 24]) + expect(editorView.scrollLeft()).toBe 0 + + editorView.setCursorScreenPosition([2, 25]) + expect(editorView.scrollLeft()).toBe 0 + + editorView.setCursorScreenPosition([2, 28]) + expect(editorView.scrollLeft()).toBe 0 + + # moving left + editorView.setCursorScreenPosition([2, 9]) + expect(editorView.scrollLeft()).toBe 0 + + editorView.setCursorScreenPosition([2, 8]) + expect(editorView.scrollLeft()).toBe 0 + + editorView.setCursorScreenPosition([2, 5]) + expect(editorView.scrollLeft()).toBe 0 + + describe "when editor:toggle-soft-wrap is toggled", -> + describe "when the text exceeds the editor view width and the scroll-view is horizontally scrolled", -> + it "wraps the text and renders properly", -> + editorView.attachToDom(heightInLines: 30, widthInChars: 30) + editorView.setWidthInChars(100) + editorView.setText("Fashion axe umami jean shorts retro hashtag carles mumblecore. Photo booth skateboard Austin gentrify occupy ethical. Food truck gastropub keffiyeh, squid deep v pinterest literally sustainable salvia scenester messenger bag. Neutra messenger bag flexitarian four loko, shoreditch VHS pop-up tumblr seitan synth master cleanse. Marfa selvage ugh, raw denim authentic try-hard mcsweeney's trust fund fashion axe actually polaroid viral sriracha. Banh mi marfa plaid single-origin coffee. Pickled mumblecore lomo ugh bespoke.") + editorView.scrollLeft(editorView.charWidth * 30) + editorView.trigger "editor:toggle-soft-wrap" + expect(editorView.scrollLeft()).toBe 0 + expect(editorView.activeEditSession.getSoftWrapColumn()).not.toBe 100 + + describe "text rendering", -> + describe "when all lines in the buffer are visible on screen", -> + beforeEach -> + editorView.attachToDom() + expect(editorView.trueHeight()).toBeCloseTo buffer.getLineCount() * editorView.lineHeight + + it "creates a line element for each line in the buffer with the html-escaped text of the line", -> + expect(editorView.renderedLines.find('.line').length).toEqual(buffer.getLineCount()) + expect(buffer.lineForRow(2)).toContain('<') + expect(editorView.renderedLines.find('.line:eq(2)').html()).toContain '<' + + # renders empty lines with a non breaking space + expect(buffer.lineForRow(10)).toBe '' + expect(editorView.renderedLines.find('.line:eq(10)').html()).toBe ' ' + + it "syntax highlights code based on the file type", -> + line0 = editorView.renderedLines.find('.line:first') + span0 = line0.children('span:eq(0)') + expect(span0).toMatchSelector '.source.js' + expect(span0.children('span:eq(0)')).toMatchSelector '.storage.modifier.js' + expect(span0.children('span:eq(0)').text()).toBe 'var' + + span0_1 = span0.children('span:eq(1)') + expect(span0_1).toMatchSelector '.meta.function.js' + expect(span0_1.text()).toBe 'quicksort = function ()' + expect(span0_1.children('span:eq(0)')).toMatchSelector '.entity.name.function.js' + expect(span0_1.children('span:eq(0)').text()).toBe "quicksort" + expect(span0_1.children('span:eq(1)')).toMatchSelector '.keyword.operator.js' + expect(span0_1.children('span:eq(1)').text()).toBe "=" + expect(span0_1.children('span:eq(2)')).toMatchSelector '.storage.type.function.js' + expect(span0_1.children('span:eq(2)').text()).toBe "function" + expect(span0_1.children('span:eq(3)')).toMatchSelector '.punctuation.definition.parameters.begin.js' + expect(span0_1.children('span:eq(3)').text()).toBe "(" + expect(span0_1.children('span:eq(4)')).toMatchSelector '.punctuation.definition.parameters.end.js' + expect(span0_1.children('span:eq(4)').text()).toBe ")" + + expect(span0.children('span:eq(2)')).toMatchSelector '.meta.brace.curly.js' + expect(span0.children('span:eq(2)').text()).toBe "{" + + line12 = editorView.renderedLines.find('.line:eq(11)').children('span:eq(0)') + expect(line12.children('span:eq(1)')).toMatchSelector '.keyword' + + it "wraps hard tabs in a span", -> + editorView.setText('\t<- hard tab') + line0 = editorView.renderedLines.find('.line:first') + span0_0 = line0.children('span:eq(0)').children('span:eq(0)') + expect(span0_0).toMatchSelector '.hard-tab' + expect(span0_0.text()).toBe ' ' + + it "wraps leading whitespace in a span", -> + line1 = editorView.renderedLines.find('.line:eq(1)') + span0_0 = line1.children('span:eq(0)').children('span:eq(0)') + expect(span0_0).toMatchSelector '.leading-whitespace' + expect(span0_0.text()).toBe ' ' + + describe "when the line has trailing whitespace", -> + it "wraps trailing whitespace in a span", -> + editorView.setText('trailing whitespace -> ') + line0 = editorView.renderedLines.find('.line:first') + span0_last = line0.children('span:eq(0)').children('span:last') + expect(span0_last).toMatchSelector '.trailing-whitespace' + expect(span0_last.text()).toBe ' ' + + describe "when lines are updated in the buffer", -> + it "syntax highlights the updated lines", -> + expect(editorView.renderedLines.find('.line:eq(0) > span:first > span:first')).toMatchSelector '.storage.modifier.js' + buffer.insert([0, 0], "q") + expect(editorView.renderedLines.find('.line:eq(0) > span:first > span:first')).not.toMatchSelector '.storage.modifier.js' + + # verify that re-highlighting can occur below the changed line + buffer.insert([5,0], "/* */") + buffer.insert([1,0], "/*") + expect(editorView.renderedLines.find('.line:eq(2) > span:first > span:first')).toMatchSelector '.comment' + + describe "when some lines at the end of the buffer are not visible on screen", -> + beforeEach -> + editorView.attachToDom(heightInLines: 5.5) + + it "only renders the visible lines plus the overdrawn lines, setting the padding-bottom of the lines element to account for the missing lines", -> + expect(editorView.renderedLines.find('.line').length).toBe 8 + expectedPaddingBottom = (buffer.getLineCount() - 8) * editorView.lineHeight + expect(editorView.renderedLines.css('padding-bottom')).toBe "#{expectedPaddingBottom}px" + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) + + it "renders additional lines when the editor view is resized", -> + setEditorHeightInLines(editorView, 10) + $(window).trigger 'resize' + + expect(editorView.renderedLines.find('.line').length).toBe 12 + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(11) + + it "renders correctly when scrolling after text is added to the buffer", -> + editorView.insertText("1\n") + _.times 4, -> editorView.moveCursorDown() + expect(editorView.renderedLines.find('.line:eq(2)').text()).toBe editorView.lineForBufferRow(2) + expect(editorView.renderedLines.find('.line:eq(7)').text()).toBe editorView.lineForBufferRow(7) + + it "renders correctly when scrolling after text is removed from buffer", -> + editorView.getBuffer().delete([[0,0],[1,0]]) + expect(editorView.renderedLines.find('.line:eq(0)').text()).toBe editorView.lineForBufferRow(0) + expect(editorView.renderedLines.find('.line:eq(5)').text()).toBe editorView.lineForBufferRow(5) + + editorView.scrollTop(3 * editorView.lineHeight) + expect(editorView.renderedLines.find('.line:first').text()).toBe editorView.lineForBufferRow(1) + expect(editorView.renderedLines.find('.line:last').text()).toBe editorView.lineForBufferRow(10) + + describe "when creating and destroying folds that are longer than the visible lines", -> + describe "when the cursor precedes the fold when it is destroyed", -> + it "renders lines and line numbers correctly", -> + scrollHeightBeforeFold = editorView.scrollView.prop('scrollHeight') + fold = editorView.createFold(1, 9) + fold.destroy() + expect(editorView.scrollView.prop('scrollHeight')).toBe scrollHeightBeforeFold + + expect(editorView.renderedLines.find('.line').length).toBe 8 + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) + + expect(editorView.gutter.find('.line-number').length).toBe 8 + expect(editorView.gutter.find('.line-number:last').intValue()).toBe 8 + + editorView.scrollTop(4 * editorView.lineHeight) + expect(editorView.renderedLines.find('.line').length).toBe 10 + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(11) + + describe "when the cursor follows the fold when it is destroyed", -> + it "renders lines and line numbers correctly", -> + fold = editorView.createFold(1, 9) + editorView.setCursorBufferPosition([10, 0]) + fold.destroy() + + expect(editorView.renderedLines.find('.line').length).toBe 8 + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(12) + + expect(editorView.gutter.find('.line-number').length).toBe 8 + expect(editorView.gutter.find('.line-number:last').text()).toBe '13' + + editorView.scrollTop(4 * editorView.lineHeight) + + expect(editorView.renderedLines.find('.line').length).toBe 10 + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(11) + + describe "when scrolling vertically", -> + describe "when scrolling less than the editor view's height", -> + it "draws new lines and removes old lines when the last visible line will exceed the last rendered line", -> + expect(editorView.renderedLines.find('.line').length).toBe 8 + + editorView.scrollTop(editorView.lineHeight * 1.5) + expect(editorView.renderedLines.find('.line').length).toBe 8 + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) + + editorView.scrollTop(editorView.lineHeight * 3.5) # first visible row will be 3, last will be 8 + expect(editorView.renderedLines.find('.line').length).toBe 10 + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(1) + expect(editorView.renderedLines.find('.line:last').html()).toBe ' ' # line 10 is blank + expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 + expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 + + # here we don't scroll far enough to trigger additional rendering + editorView.scrollTop(editorView.lineHeight * 5.5) # first visible row will be 5, last will be 10 + expect(editorView.renderedLines.find('.line').length).toBe 10 + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(1) + expect(editorView.renderedLines.find('.line:last').html()).toBe ' ' # line 10 is blank + expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 + expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 + + editorView.scrollTop(editorView.lineHeight * 7.5) # first visible row is 7, last will be 12 + expect(editorView.renderedLines.find('.line').length).toBe 8 + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(5) + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(12) + + editorView.scrollTop(editorView.lineHeight * 3.5) # first visible row will be 3, last will be 8 + expect(editorView.renderedLines.find('.line').length).toBe 10 + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(1) + expect(editorView.renderedLines.find('.line:last').html()).toBe ' ' # line 10 is blank + + editorView.scrollTop(0) + expect(editorView.renderedLines.find('.line').length).toBe 8 + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) + + describe "when scrolling more than the editors height", -> + it "removes lines that are offscreen and not in range of the overdraw and builds lines that become visible", -> + editorView.scrollTop(editorView.layerHeight - editorView.scrollView.height()) + expect(editorView.renderedLines.find('.line').length).toBe 8 + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(5) + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(12) + + editorView.verticalScrollbar.scrollBottom(0) + editorView.verticalScrollbar.trigger 'scroll' + expect(editorView.renderedLines.find('.line').length).toBe 8 + expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) + expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) + + it "adjusts the vertical padding of the lines element to account for non-rendered lines", -> + editorView.scrollTop(editorView.lineHeight * 3) + firstVisibleBufferRow = 3 + expectedPaddingTop = (firstVisibleBufferRow - editorView.lineOverdraw) * editorView.lineHeight + expect(editorView.renderedLines.css('padding-top')).toBe "#{expectedPaddingTop}px" + + lastVisibleBufferRow = Math.ceil(3 + 5.5) # scroll top in lines + height in lines + lastOverdrawnRow = lastVisibleBufferRow + editorView.lineOverdraw + expectedPaddingBottom = ((buffer.getLineCount() - lastOverdrawnRow) * editorView.lineHeight) + expect(editorView.renderedLines.css('padding-bottom')).toBe "#{expectedPaddingBottom}px" + + editorView.scrollToBottom() + # scrolled to bottom, first visible row is 5 and first rendered row is 3 + firstVisibleBufferRow = Math.floor(buffer.getLineCount() - 5.5) + firstOverdrawnBufferRow = firstVisibleBufferRow - editorView.lineOverdraw + expectedPaddingTop = firstOverdrawnBufferRow * editorView.lineHeight + expect(editorView.renderedLines.css('padding-top')).toBe "#{expectedPaddingTop}px" + expect(editorView.renderedLines.css('padding-bottom')).toBe "0px" + + describe "when lines are added", -> + beforeEach -> + editorView.attachToDom(heightInLines: 5) + + describe "when the change precedes the first rendered row", -> + it "inserts and removes rendered lines to account for upstream change", -> + editorView.scrollToBottom() + expect(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + buffer.change([[1,0], [3,0]], "1\n2\n3\n") + expect(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) + expect(editorView.renderedLines.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", -> + editorView.scrollToBottom() + + expect(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + buffer.change([[2,0], [7,0]], "2\n3\n4\n5\n6\n7\n8\n9\n") + expect(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + describe "when the change 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(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) + + 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(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) + + it "increases the width of the rendered lines element to be either the width of the longest line or the width of the scrollView (whichever is longer)", -> + maxLineLength = editorView.getMaxScreenLineLength() + setEditorWidthInChars(editorView, maxLineLength) + widthBefore = editorView.renderedLines.width() + expect(widthBefore).toBe editorView.scrollView.width() + 20 + buffer.change([[12,0], [12,0]], [1..maxLineLength*2].join('')) + expect(editorView.renderedLines.width()).toBeGreaterThan widthBefore + + describe "when lines are removed", -> + beforeEach -> + editorView.attachToDom(heightInLines: 5) + + it "sets the rendered screen line's width to either the max line length or the scollView's width (whichever is greater)", -> + maxLineLength = editorView.getMaxScreenLineLength() + setEditorWidthInChars(editorView, maxLineLength) + buffer.change([[12,0], [12,0]], [1..maxLineLength*2].join('')) + expect(editorView.renderedLines.width()).toBeGreaterThan editorView.scrollView.width() + widthBefore = editorView.renderedLines.width() + buffer.delete([[12, 0], [12, Infinity]]) + expect(editorView.renderedLines.width()).toBe editorView.scrollView.width() + 20 + + describe "when the change the precedes the first rendered row", -> + it "removes rendered lines to account for upstream change", -> + editorView.scrollToBottom() + expect(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + buffer.change([[1,0], [2,0]], "") + expect(editorView.renderedLines.find(".line").length).toBe 6 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(11) + + describe "when the change straddles the first rendered row", -> + it "renders the correct rows", -> + editorView.scrollToBottom() + expect(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) + + buffer.change([[7,0], [11,0]], "1\n2\n") + expect(editorView.renderedLines.find(".line").length).toBe 5 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(10) + + describe "when the change straddles the last rendered row", -> + it "renders the correct rows", -> + buffer.change([[2,0], [7,0]], "") + expect(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) + + describe "when the change the follows the last rendered row", -> + it "does not change the rendered lines", -> + buffer.change([[10,0], [12,0]], "") + expect(editorView.renderedLines.find(".line").length).toBe 7 + expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) + expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) + + describe "when the last line is removed when the editor view is scrolled to the bottom", -> + it "reduces the editor view's scrollTop (due to the reduced total scroll height) and renders the correct screen lines", -> + editorView.setCursorScreenPosition([Infinity, Infinity]) + editorView.insertText('\n\n\n') + editorView.scrollToBottom() + + expect(buffer.getLineCount()).toBe 16 + + initialScrollTop = editorView.scrollTop() + expect(editorView.firstRenderedScreenRow).toBe 9 + expect(editorView.lastRenderedScreenRow).toBe 15 + + editorView.backspace() + + expect(editorView.scrollTop()).toBeLessThan initialScrollTop + expect(editorView.firstRenderedScreenRow).toBe 9 + expect(editorView.lastRenderedScreenRow).toBe 14 + + expect(editorView.find('.line').length).toBe 6 + + editorView.backspace() + expect(editorView.firstRenderedScreenRow).toBe 9 + expect(editorView.lastRenderedScreenRow).toBe 13 + + expect(editorView.find('.line').length).toBe 5 + + editorView.backspace() + expect(editorView.firstRenderedScreenRow).toBe 6 + expect(editorView.lastRenderedScreenRow).toBe 12 + + expect(editorView.find('.line').length).toBe 7 + + describe "when folding leaves less then a screen worth of text (regression)", -> + it "renders lines properly", -> + editorView.lineOverdraw = 1 + editorView.attachToDom(heightInLines: 5) + editorView.activeEditSession.foldBufferRow(4) + editorView.activeEditSession.foldBufferRow(0) + + expect(editorView.renderedLines.find('.line').length).toBe 1 + expect(editorView.renderedLines.find('.line').text()).toBe buffer.lineForRow(0) + + describe "when folding leaves fewer screen lines than the first rendered screen line (regression)", -> + it "clears all screen lines and does not throw any exceptions", -> + editorView.lineOverdraw = 1 + editorView.attachToDom(heightInLines: 5) + editorView.scrollToBottom() + editorView.activeEditSession.foldBufferRow(0) + expect(editorView.renderedLines.find('.line').length).toBe 1 + expect(editorView.renderedLines.find('.line').text()).toBe buffer.lineForRow(0) + + describe "when autoscrolling at the end of the document", -> + it "renders lines properly", -> + editorView.edit(project.openSync('two-hundred.txt')) + editorView.attachToDom(heightInLines: 5.5) + + expect(editorView.renderedLines.find('.line').length).toBe 8 + + editorView.moveCursorToBottom() + + expect(editorView.renderedLines.find('.line').length).toBe 8 + + describe "when line has a character that could push it to be too tall (regression)", -> + it "does renders the line at a consistent height", -> + editorView.attachToDom() + buffer.insert([0, 0], "–") + expect(editorView.find('.line:eq(0)').outerHeight()).toBe editorView.find('.line:eq(1)').outerHeight() + + describe "when editor.showInvisibles config is set to true", -> + it "displays spaces, tabs, and newlines using visible non-empty values", -> + editorView.setText " a line with tabs\tand spaces " + editorView.attachToDom() + + expect(atom.config.get("editor.showInvisibles")).toBeFalsy() + expect(editorView.renderedLines.find('.line').text()).toBe " a line with tabs and spaces " + + atom.config.set("editor.showInvisibles", true) + space = editorView.invisibles?.space + expect(space).toBeTruthy() + tab = editorView.invisibles?.tab + expect(tab).toBeTruthy() + eol = editorView.invisibles?.eol + expect(eol).toBeTruthy() + expect(editorView.renderedLines.find('.line').text()).toBe "#{space}a line with tabs#{tab} and spaces#{space}#{eol}" + + atom.config.set("editor.showInvisibles", false) + expect(editorView.renderedLines.find('.line').text()).toBe " a line with tabs and spaces " + + it "displays newlines as their own token outside of the other tokens scope", -> + editorView.setShowInvisibles(true) + editorView.attachToDom() + editorView.setText "var" + expect(editorView.find('.line').html()).toBe 'var¬' + + it "allows invisible glyphs to be customized via the editor.invisibles config", -> + editorView.setText(" \t ") + editorView.attachToDom() + atom.config.set("editor.showInvisibles", true) + atom.config.set("editor.invisibles", eol: ";", space: "_", tab: "tab") + expect(editorView.find(".line:first").text()).toBe "_tab _;" + + it "displays trailing carriage return using a visible non-empty value", -> + editorView.setText "a line that ends with a carriage return\r\n" + editorView.attachToDom() + + expect(atom.config.get("editor.showInvisibles")).toBeFalsy() + expect(editorView.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return" + + atom.config.set("editor.showInvisibles", true) + cr = editorView.invisibles?.cr + expect(cr).toBeTruthy() + eol = editorView.invisibles?.eol + expect(eol).toBeTruthy() + expect(editorView.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return#{cr}#{eol}" + + describe "when wrapping is on", -> + beforeEach -> + editor.setSoftWrap(true) + + it "doesn't show the end of line invisible at the end of lines broken due to wrapping", -> + editorView.setText "a line that wraps" + editorView.attachToDom() + editorView.setWidthInChars(6) + atom.config.set "editor.showInvisibles", true + space = editorView.invisibles?.space + expect(space).toBeTruthy() + eol = editorView.invisibles?.eol + expect(eol).toBeTruthy() + expect(editorView.renderedLines.find('.line:first').text()).toBe "a line#{space}" + expect(editorView.renderedLines.find('.line:last').text()).toBe "wraps#{eol}" + + it "displays trailing carriage return using a visible non-empty value", -> + editorView.setText "a line that\r\n" + editorView.attachToDom() + editorView.setWidthInChars(6) + atom.config.set "editor.showInvisibles", true + space = editorView.invisibles?.space + expect(space).toBeTruthy() + cr = editorView.invisibles?.cr + expect(cr).toBeTruthy() + eol = editorView.invisibles?.eol + expect(eol).toBeTruthy() + expect(editorView.renderedLines.find('.line:first').text()).toBe "a line#{space}" + expect(editorView.renderedLines.find('.line:eq(1)').text()).toBe "that#{cr}#{eol}" + expect(editorView.renderedLines.find('.line:last').text()).toBe "#{eol}" + + describe "when editor.showIndentGuide is set to true", -> + it "adds an indent-guide class to each leading whitespace span", -> + editorView.attachToDom() + + expect(atom.config.get("editor.showIndentGuide")).toBeFalsy() + atom.config.set("editor.showIndentGuide", true) + expect(editorView.showIndentGuide).toBeTruthy() + + expect(editorView.renderedLines.find('.line:eq(0) .indent-guide').length).toBe 0 + + expect(editorView.renderedLines.find('.line:eq(1) .indent-guide').length).toBe 1 + expect(editorView.renderedLines.find('.line:eq(1) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(2) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(2) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(3) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(3) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(4) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(4) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(5) .indent-guide').length).toBe 3 + expect(editorView.renderedLines.find('.line:eq(5) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(6) .indent-guide').length).toBe 3 + expect(editorView.renderedLines.find('.line:eq(6) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(7) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(7) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(8) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(8) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(9) .indent-guide').length).toBe 1 + expect(editorView.renderedLines.find('.line:eq(9) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(11) .indent-guide').length).toBe 1 + expect(editorView.renderedLines.find('.line:eq(11) .indent-guide').text()).toBe ' ' + + expect(editorView.renderedLines.find('.line:eq(12) .indent-guide').length).toBe 0 + + describe "when the indentation level on a line before an empty line is changed", -> + it "updates the indent guide on the empty line", -> + editorView.attachToDom() + atom.config.set("editor.showIndentGuide", true) + + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' + + editorView.setCursorBufferPosition([9]) + editorView.indentSelectedRows() + + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' + + describe "when the indentation level on a line after an empty line is changed", -> + it "updates the indent guide on the empty line", -> + editorView.attachToDom() + atom.config.set("editor.showIndentGuide", true) + + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' + + editorView.setCursorBufferPosition([11]) + editorView.indentSelectedRows() + + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' + + describe "when a line contains only whitespace", -> + it "displays an indent guide on the line", -> + editorView.attachToDom() + atom.config.set("editor.showIndentGuide", true) + + editorView.setCursorBufferPosition([10]) + editorView.indent() + editorView.indent() + expect(editorView.getCursorBufferPosition()).toEqual [10, 4] + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' + + it "uses the highest indent guide level from the next or previous non-empty line", -> + editorView.attachToDom() + atom.config.set("editor.showIndentGuide", true) + + editorView.setCursorBufferPosition([1, Infinity]) + editorView.insertNewline() + expect(editorView.getCursorBufferPosition()).toEqual [2, 0] + expect(editorView.renderedLines.find('.line:eq(2) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(2) .indent-guide').text()).toBe ' ' + + describe "when the line has leading and trailing whitespace", -> + it "does not display the indent guide in the trailing whitespace", -> + editorView.attachToDom() + atom.config.set("editor.showIndentGuide", true) + + editorView.insertText("/*\n * \n*/") + expect(editorView.renderedLines.find('.line:eq(1) .indent-guide').length).toBe 1 + expect(editorView.renderedLines.find('.line:eq(1) .indent-guide')).toHaveClass('leading-whitespace') + + describe "when the line is empty and end of show invisibles are enabled", -> + it "renders the indent guides interleaved with the end of line invisibles", -> + editorView.attachToDom() + atom.config.set("editor.showIndentGuide", true) + atom.config.set("editor.showInvisibles", true) + eol = editorView.invisibles?.eol + + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe "#{eol} " + expect(editorView.renderedLines.find('.line:eq(10) .invisible-character').text()).toBe eol + + editorView.setCursorBufferPosition([9]) + editorView.indent() + + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 + expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe "#{eol} " + expect(editorView.renderedLines.find('.line:eq(10) .invisible-character').text()).toBe eol + + describe "when soft-wrap is enabled", -> + beforeEach -> + jasmine.unspy(window, 'setTimeout') + editor.setSoftWrap(true) + editorView.attachToDom() + setEditorHeightInLines(editorView, 20) + setEditorWidthInChars(editorView, 50) + expect(editorView.activeEditSession.getSoftWrapColumn()).toBe 50 + + it "wraps lines that are too long to fit within the editor view's width, adjusting cursor positioning accordingly", -> + expect(editorView.renderedLines.find('.line').length).toBe 16 + expect(editorView.renderedLines.find('.line:eq(3)').text()).toBe " var pivot = items.shift(), current, left = [], " + expect(editorView.renderedLines.find('.line:eq(4)').text()).toBe "right = [];" + + editorView.setCursorBufferPosition([3, 51], wrapAtSoftNewlines: true) + expect(editorView.find('.cursor').offset()).toEqual(editorView.renderedLines.find('.line:eq(4)').offset()) + + editorView.setCursorBufferPosition([4, 0]) + expect(editorView.find('.cursor').offset()).toEqual(editorView.renderedLines.find('.line:eq(5)').offset()) + + editorView.getSelection().setBufferRange([[6, 30], [6, 55]]) + [region1, region2] = editorView.getSelectionView().regions + expect(region1.offset().top).toBeCloseTo(editorView.renderedLines.find('.line:eq(7)').offset().top) + expect(region2.offset().top).toBeCloseTo(editorView.renderedLines.find('.line:eq(8)').offset().top) + + it "handles changes to wrapped lines correctly", -> + buffer.insert([6, 28], '1234567') + expect(editorView.renderedLines.find('.line:eq(7)').text()).toBe ' current < pivot ? left1234567.push(current) ' + expect(editorView.renderedLines.find('.line:eq(8)').text()).toBe ': right.push(current);' + expect(editorView.renderedLines.find('.line:eq(9)').text()).toBe ' }' + + it "changes the max line length and repositions the cursor when the window size changes", -> + editorView.setCursorBufferPosition([3, 60]) + setEditorWidthInChars(editorView, 40) + expect(editorView.renderedLines.find('.line').length).toBe 19 + expect(editorView.renderedLines.find('.line:eq(4)').text()).toBe "left = [], right = [];" + expect(editorView.renderedLines.find('.line:eq(5)').text()).toBe " while(items.length > 0) {" + expect(editorView.bufferPositionForScreenPosition(editorView.getCursorScreenPosition())).toEqual [3, 60] + + it "does not wrap the lines of any newly assigned buffers", -> + otherEditSession = project.openSync() + otherEditSession.buffer.setText([1..100].join('')) + editorView.edit(otherEditSession) + expect(editorView.renderedLines.find('.line').length).toBe(1) + + it "unwraps lines when softwrap is disabled", -> + editorView.toggleSoftWrap() + expect(editorView.renderedLines.find('.line:eq(3)').text()).toBe ' var pivot = items.shift(), current, left = [], right = [];' + + it "allows the cursor to move down to the last line", -> + _.times editorView.getLastScreenRow(), -> editorView.moveCursorDown() + expect(editorView.getCursorScreenPosition()).toEqual [editorView.getLastScreenRow(), 0] + editorView.moveCursorDown() + expect(editorView.getCursorScreenPosition()).toEqual [editorView.getLastScreenRow(), 2] + + it "allows the cursor to move up to a shorter soft wrapped line", -> + editorView.setCursorScreenPosition([11, 15]) + editorView.moveCursorUp() + expect(editorView.getCursorScreenPosition()).toEqual [10, 10] + editorView.moveCursorUp() + editorView.moveCursorUp() + expect(editorView.getCursorScreenPosition()).toEqual [8, 15] + + it "it allows the cursor to wrap when moving horizontally past the beginning / end of a wrapped line", -> + editorView.setCursorScreenPosition([11, 0]) + editorView.moveCursorLeft() + expect(editorView.getCursorScreenPosition()).toEqual [10, 10] + + editorView.moveCursorRight() + expect(editorView.getCursorScreenPosition()).toEqual [11, 0] + + it "calls .setWidthInChars() when the editor view is attached because now its dimensions are available to calculate it", -> + otherEditor = new EditorView(editor: project.openSync('sample.js')) + spyOn(otherEditor, 'setWidthInChars') + + otherEditor.activeEditSession.setSoftWrap(true) + expect(otherEditor.setWidthInChars).not.toHaveBeenCalled() + + otherEditor.simulateDomAttachment() + expect(otherEditor.setWidthInChars).toHaveBeenCalled() + otherEditor.remove() + + describe "when the editor view's width changes", -> + it "updates the width in characters on the edit session", -> + previousSoftWrapColumn = editor.getSoftWrapColumn() + + spyOn(editorView, 'setWidthInChars').andCallThrough() + editorView.width(editorView.width() / 2) + + waitsFor -> + editorView.setWidthInChars.callCount > 0 + + runs -> + expect(editor.getSoftWrapColumn()).toBeLessThan previousSoftWrapColumn + + describe "gutter rendering", -> + beforeEach -> + editorView.attachToDom(heightInLines: 5.5) + + it "creates a line number element for each visible line with   padding to the left of the number", -> + expect(editorView.gutter.find('.line-number').length).toBe 8 + expect(editorView.find('.line-number:first').html()).toBe " 1" + expect(editorView.gutter.find('.line-number:last').html()).toBe " 8" + + # here we don't scroll far enough to trigger additional rendering + editorView.scrollTop(editorView.lineHeight * 1.5) + expect(editorView.renderedLines.find('.line').length).toBe 8 + expect(editorView.gutter.find('.line-number:first').html()).toBe " 1" + expect(editorView.gutter.find('.line-number:last').html()).toBe " 8" + + editorView.scrollTop(editorView.lineHeight * 3.5) + expect(editorView.renderedLines.find('.line').length).toBe 10 + expect(editorView.gutter.find('.line-number:first').html()).toBe " 2" + expect(editorView.gutter.find('.line-number:last').html()).toBe "11" + + describe "when lines are inserted", -> + it "re-renders the correct line number range in the gutter", -> + editorView.scrollTop(3 * editorView.lineHeight) + expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 + expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 + + buffer.insert([6, 0], '\n') + + expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 + expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 + + it "re-renders the correct line number range when there are folds", -> + editorView.activeEditSession.foldBufferRow(1) + expect(editorView.gutter.find('.line-number-1')).toHaveClass 'fold' + + buffer.insert([0, 0], '\n') + + expect(editorView.gutter.find('.line-number-2')).toHaveClass 'fold' + + describe "when wrapping is on", -> + it "renders a • instead of line number for wrapped portions of lines", -> + editor.setSoftWrap(true) + editorView.setWidthInChars(50) + expect(editorView.gutter.find('.line-number').length).toEqual(8) + expect(editorView.gutter.find('.line-number:eq(3)').intValue()).toBe 4 + expect(editorView.gutter.find('.line-number:eq(4)').html()).toBe ' â€¢' + expect(editorView.gutter.find('.line-number:eq(5)').intValue()).toBe 5 + + describe "when there are folds", -> + it "skips line numbers covered by the fold and updates them when the fold changes", -> + editorView.createFold(3, 5) + expect(editorView.gutter.find('.line-number:eq(3)').intValue()).toBe 4 + expect(editorView.gutter.find('.line-number:eq(4)').intValue()).toBe 7 + + buffer.insert([4,0], "\n\n") + expect(editorView.gutter.find('.line-number:eq(3)').intValue()).toBe 4 + expect(editorView.gutter.find('.line-number:eq(4)').intValue()).toBe 9 + + buffer.delete([[3,0], [6,0]]) + expect(editorView.gutter.find('.line-number:eq(3)').intValue()).toBe 4 + expect(editorView.gutter.find('.line-number:eq(4)').intValue()).toBe 6 + + it "redraws gutter numbers when lines are unfolded", -> + setEditorHeightInLines(editorView, 20) + fold = editorView.createFold(2, 12) + expect(editorView.gutter.find('.line-number').length).toBe 3 + + fold.destroy() + expect(editorView.gutter.find('.line-number').length).toBe 13 + + it "styles folded line numbers", -> + editorView.createFold(3, 5) + expect(editorView.gutter.find('.line-number.fold').length).toBe 1 + expect(editorView.gutter.find('.line-number.fold:eq(0)').intValue()).toBe 4 + + describe "when the scrollView is scrolled to the right", -> + it "adds a drop shadow to the gutter", -> + editorView.attachToDom() + editorView.width(100) + + expect(editorView.gutter).not.toHaveClass('drop-shadow') + + editorView.scrollLeft(10) + editorView.scrollView.trigger('scroll') + + expect(editorView.gutter).toHaveClass('drop-shadow') + + editorView.scrollLeft(0) + editorView.scrollView.trigger('scroll') + + expect(editorView.gutter).not.toHaveClass('drop-shadow') + + describe "when the editor view is scrolled vertically", -> + it "adjusts the padding-top to account for non-rendered line numbers", -> + editorView.scrollTop(editorView.lineHeight * 3.5) + expect(editorView.gutter.lineNumbers.css('padding-top')).toBe "#{editorView.lineHeight * 1}px" + expect(editorView.gutter.lineNumbers.css('padding-bottom')).toBe "#{editorView.lineHeight * 2}px" + expect(editorView.renderedLines.find('.line').length).toBe 10 + expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 + expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 + + describe "when the switching from an edit session for a long buffer to an edit session for a short buffer", -> + it "updates the line numbers to reflect the shorter buffer", -> + emptyEditSession = project.openSync(null) + editorView.edit(emptyEditSession) + expect(editorView.gutter.lineNumbers.find('.line-number').length).toBe 1 + + editorView.edit(editor) + expect(editorView.gutter.lineNumbers.find('.line-number').length).toBeGreaterThan 1 + + editorView.edit(emptyEditSession) + expect(editorView.gutter.lineNumbers.find('.line-number').length).toBe 1 + + describe "when the editor view is mini", -> + it "hides the gutter", -> + miniEditor = new EditorView(mini: true) + miniEditor.attachToDom() + expect(miniEditor.gutter).toBeHidden() + + it "doesn't highlight the only line", -> + miniEditor = new EditorView(mini: true) + miniEditor.attachToDom() + expect(miniEditor.getCursorBufferPosition().row).toBe 0 + expect(miniEditor.find('.line.cursor-line').length).toBe 0 + + it "doesn't show the end of line invisible", -> + atom.config.set "editor.showInvisibles", true + miniEditor = new EditorView(mini: true) + miniEditor.attachToDom() + space = miniEditor.invisibles?.space + expect(space).toBeTruthy() + tab = miniEditor.invisibles?.tab + expect(tab).toBeTruthy() + miniEditor.setText(" a line with tabs\tand spaces ") + expect(miniEditor.renderedLines.find('.line').text()).toBe "#{space}a line with tabs#{tab} and spaces#{space}" + + it "doesn't show the indent guide", -> + atom.config.set "editor.showIndentGuide", true + miniEditor = new EditorView(mini: true) + miniEditor.attachToDom() + miniEditor.setText(" and indented line") + expect(miniEditor.renderedLines.find('.indent-guide').length).toBe 0 + + + it "lets you set the grammar", -> + miniEditor = new EditorView(mini: true) + miniEditor.setText("var something") + previousTokens = miniEditor.lineForScreenRow(0).tokens + miniEditor.setGrammar(atom.syntax.selectGrammar('something.js')) + expect(miniEditor.getGrammar().name).toBe "JavaScript" + expect(previousTokens).not.toEqual miniEditor.lineForScreenRow(0).tokens + + # doesn't allow regular editors to set grammars + expect(-> editorView.setGrammar()).toThrow() + + describe "when the editor.showLineNumbers config is false", -> + it "doesn't render any line numbers", -> + expect(editorView.gutter.lineNumbers).toBeVisible() + atom.config.set("editor.showLineNumbers", false) + expect(editorView.gutter.lineNumbers).not.toBeVisible() + + describe "using gutter's api", -> + it "can get all the line number elements", -> + elements = editorView.gutter.getLineNumberElements() + len = editorView.gutter.lastScreenRow - editorView.gutter.firstScreenRow + 1 + expect(elements).toHaveLength(len) + + it "can get a single line number element", -> + element = editorView.gutter.getLineNumberElement(3) + expect(element).toBeTruthy() + + it "returns falsy when there is no line element", -> + expect(editorView.gutter.getLineNumberElement(42)).toHaveLength 0 + + it "can add and remove classes to all the line numbers", -> + wasAdded = editorView.gutter.addClassToAllLines('heyok') + expect(wasAdded).toBe true + + elements = editorView.gutter.getLineNumberElementsForClass('heyok') + expect($(elements)).toHaveClass('heyok') + + editorView.gutter.removeClassFromAllLines('heyok') + expect($(editorView.gutter.getLineNumberElements())).not.toHaveClass('heyok') + + it "can add and remove classes from a single line number", -> + wasAdded = editorView.gutter.addClassToLine(3, 'heyok') + expect(wasAdded).toBe true + + element = editorView.gutter.getLineNumberElement(2) + expect($(element)).not.toHaveClass('heyok') + + it "can fetch line numbers by their class", -> + editorView.gutter.addClassToLine(1, 'heyok') + editorView.gutter.addClassToLine(3, 'heyok') + + elements = editorView.gutter.getLineNumberElementsForClass('heyok') + expect(elements.length).toBe 2 + + expect($(elements[0])).toHaveClass 'line-number-1' + expect($(elements[0])).toHaveClass 'heyok' + + expect($(elements[1])).toHaveClass 'line-number-3' + expect($(elements[1])).toHaveClass 'heyok' + + describe "gutter line highlighting", -> + beforeEach -> + editorView.attachToDom(heightInLines: 5.5) + + describe "when there is no wrapping", -> + it "highlights the line where the initial cursor position is", -> + expect(editorView.getCursorBufferPosition().row).toBe 0 + expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 + expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 1 + + it "updates the highlighted line when the cursor position changes", -> + editorView.setCursorBufferPosition([1,0]) + expect(editorView.getCursorBufferPosition().row).toBe 1 + expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 + expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 2 + + describe "when there is wrapping", -> + beforeEach -> + editorView.attachToDom(30) + editor.setSoftWrap(true) + setEditorWidthInChars(editorView, 20) + + it "highlights the line where the initial cursor position is", -> + expect(editorView.getCursorBufferPosition().row).toBe 0 + expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 + expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 1 + + it "updates the highlighted line when the cursor position changes", -> + editorView.setCursorBufferPosition([1,0]) + expect(editorView.getCursorBufferPosition().row).toBe 1 + expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 + expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 2 + + describe "when the selection spans multiple lines", -> + beforeEach -> + editorView.attachToDom(30) + + it "highlights the foreground of the gutter", -> + editorView.getSelection().setBufferRange([[0,0],[2,2]]) + expect(editorView.getSelection().isSingleScreenLine()).toBe false + expect(editorView.find('.line-number.cursor-line').length).toBe 3 + + it "doesn't highlight the background of the gutter", -> + editorView.getSelection().setBufferRange([[0,0],[2,0]]) + expect(editorView.getSelection().isSingleScreenLine()).toBe false + expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 0 + + it "doesn't highlight the last line if it ends at the beginning of a line", -> + editorView.getSelection().setBufferRange([[0,0],[1,0]]) + expect(editorView.getSelection().isSingleScreenLine()).toBe false + expect(editorView.find('.line-number.cursor-line').length).toBe 1 + expect(editorView.find('.line-number.cursor-line').intValue()).toBe 1 + + it "when a newline is deleted with backspace, the line number of the new cursor position is highlighted", -> + editorView.setCursorScreenPosition([1,0]) + editorView.backspace() + expect(editorView.find('.line-number.cursor-line').length).toBe 1 + expect(editorView.find('.line-number.cursor-line').intValue()).toBe 1 + + describe "line highlighting", -> + beforeEach -> + editorView.attachToDom(30) + + describe "when there is no wrapping", -> + it "highlights the line where the initial cursor position is", -> + expect(editorView.getCursorBufferPosition().row).toBe 0 + expect(editorView.find('.line.cursor-line').length).toBe 1 + expect(editorView.find('.line.cursor-line').text()).toBe buffer.lineForRow(0) + + it "updates the highlighted line when the cursor position changes", -> + editorView.setCursorBufferPosition([1,0]) + expect(editorView.getCursorBufferPosition().row).toBe 1 + expect(editorView.find('.line.cursor-line').length).toBe 1 + expect(editorView.find('.line.cursor-line').text()).toBe buffer.lineForRow(1) + + it "when a newline is deleted with backspace, the line of the new cursor position is highlighted", -> + editorView.setCursorScreenPosition([1,0]) + editorView.backspace() + expect(editorView.find('.line.cursor-line').length).toBe 1 + + describe "when there is wrapping", -> + beforeEach -> + editor.setSoftWrap(true) + setEditorWidthInChars(editorView, 20) + + it "highlights the line where the initial cursor position is", -> + expect(editorView.getCursorBufferPosition().row).toBe 0 + expect(editorView.find('.line.cursor-line').length).toBe 1 + expect(editorView.find('.line.cursor-line').text()).toBe 'var quicksort = ' + + it "updates the highlighted line when the cursor position changes", -> + editorView.setCursorBufferPosition([1,0]) + expect(editorView.getCursorBufferPosition().row).toBe 1 + expect(editorView.find('.line.cursor-line').length).toBe 1 + expect(editorView.find('.line.cursor-line').text()).toBe ' var sort = ' + + describe "when there is a non-empty selection", -> + it "does not highlight the line", -> + editorView.setSelectedBufferRange([[1, 0], [1, 1]]) + expect(editorView.find('.line.cursor-line').length).toBe 0 + + describe "folding", -> + beforeEach -> + editor = project.openSync('two-hundred.txt') + buffer = editor.buffer + editorView.edit(editor) + editorView.attachToDom() + + describe "when a fold-selection event is triggered", -> + it "folds the lines covered by the selection into a single line with a fold class and marker", -> + editorView.getSelection().setBufferRange([[4,29],[7,4]]) + editorView.trigger 'editor:fold-selection' + + expect(editorView.renderedLines.find('.line:eq(4)')).toHaveClass('fold') + expect(editorView.renderedLines.find('.line:eq(4) > .fold-marker')).toExist() + expect(editorView.renderedLines.find('.line:eq(5)').text()).toBe '8' + + expect(editorView.getSelection().isEmpty()).toBeTruthy() + expect(editorView.getCursorScreenPosition()).toEqual [5, 0] + + it "keeps the gutter line and the editor view line the same heights (regression)", -> + editorView.getSelection().setBufferRange([[4,29],[7,4]]) + editorView.trigger 'editor:fold-selection' + + expect(editorView.gutter.find('.line-number:eq(4)').height()).toBe editorView.renderedLines.find('.line:eq(4)').height() + + describe "when a fold placeholder line is clicked", -> + it "removes the associated fold and places the cursor at its beginning", -> + editorView.setCursorBufferPosition([3,0]) + editor.createFold(3, 5) + + foldLine = editorView.find('.line.fold') + expect(foldLine).toExist() + foldLine.mousedown() + + expect(editorView.find('.fold')).not.toExist() + expect(editorView.find('.fold-marker')).not.toExist() + expect(editorView.renderedLines.find('.line:eq(4)').text()).toMatch /4-+/ + expect(editorView.renderedLines.find('.line:eq(5)').text()).toMatch /5/ + + expect(editorView.getCursorBufferPosition()).toEqual [3, 0] + + describe "when the unfold-current-row event is triggered when the cursor is on a fold placeholder line", -> + it "removes the associated fold and places the cursor at its beginning", -> + editorView.setCursorBufferPosition([3,0]) + editorView.trigger 'editor:fold-current-row' + + editorView.setCursorBufferPosition([3,0]) + editorView.trigger 'editor:unfold-current-row' + + expect(editorView.find('.fold')).not.toExist() + expect(editorView.renderedLines.find('.line:eq(4)').text()).toMatch /4-+/ + expect(editorView.renderedLines.find('.line:eq(5)').text()).toMatch /5/ + + expect(editorView.getCursorBufferPosition()).toEqual [3, 0] + + describe "when a selection starts/stops intersecting a fold", -> + it "adds/removes the 'fold-selected' class to the fold's line element and hides the cursor if it is on the fold line", -> + editorView.createFold(2, 4) + + editorView.setSelectedBufferRange([[1, 0], [2, 0]], preserveFolds: true, isReversed: true) + expect(editorView.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') + + editorView.setSelectedBufferRange([[1, 0], [1, 1]], preserveFolds: true) + expect(editorView.lineElementForScreenRow(2)).not.toMatchSelector('.fold.fold-selected') + + editorView.setSelectedBufferRange([[1, 0], [5, 0]], preserveFolds: true) + expect(editorView.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') + + editorView.setCursorScreenPosition([3,0]) + expect(editorView.lineElementForScreenRow(2)).not.toMatchSelector('.fold.fold-selected') + + editorView.setCursorScreenPosition([2,0]) + expect(editorView.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') + expect(editorView.find('.cursor')).toBeHidden() + + editorView.setCursorScreenPosition([3,0]) + expect(editorView.find('.cursor')).toBeVisible() + + 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 'fold-selected' class", -> + setEditorHeightInLines(editorView, 5) + editorView.resetDisplay() + + editorView.createFold(2, 4) + editorView.setSelectedBufferRange([[1, 0], [5, 0]], preserveFolds: true) + expect(editorView.renderedLines.find('.fold.fold-selected')).toExist() + + editorView.scrollToBottom() + expect(editorView.renderedLines.find('.fold.fold-selected')).not.toExist() + + editorView.scrollTop(0) + expect(editorView.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') + + describe "paging up and down", -> + beforeEach -> + editorView.attachToDom() + + it "moves to the last line when page down is repeated from the first line", -> + rows = editorView.getLineCount() - 1 + expect(rows).toBeGreaterThan(0) + row = editorView.getCursor().getScreenPosition().row + expect(row).toBe(0) + while row < rows + editorView.pageDown() + newRow = editorView.getCursor().getScreenPosition().row + expect(newRow).toBeGreaterThan(row) + if (newRow <= row) + break + row = newRow + expect(row).toBe(rows) + expect(editorView.getLastVisibleScreenRow()).toBe(rows) + + it "moves to the first line when page up is repeated from the last line", -> + editorView.moveCursorToBottom() + row = editorView.getCursor().getScreenPosition().row + expect(row).toBeGreaterThan(0) + while row > 0 + editorView.pageUp() + newRow = editorView.getCursor().getScreenPosition().row + expect(newRow).toBeLessThan(row) + if (newRow >= row) + break + row = newRow + expect(row).toBe(0) + expect(editorView.getFirstVisibleScreenRow()).toBe(0) + + it "resets to original position when down is followed by up", -> + expect(editorView.getCursor().getScreenPosition().row).toBe(0) + editorView.pageDown() + expect(editorView.getCursor().getScreenPosition().row).toBeGreaterThan(0) + editorView.pageUp() + expect(editorView.getCursor().getScreenPosition().row).toBe(0) + expect(editorView.getFirstVisibleScreenRow()).toBe(0) + + describe ".checkoutHead()", -> + [filePath, originalPathText] = [] + + beforeEach -> + filePath = project.resolve('git/working-dir/file.txt') + originalPathText = fs.readFileSync(filePath, 'utf8') + editorView.edit(project.openSync(filePath)) + + afterEach -> + fs.writeFileSync(filePath, originalPathText) + + it "restores the contents of the editor view to the HEAD revision", -> + editorView.setText('') + editorView.getBuffer().save() + + fileChangeHandler = jasmine.createSpy('fileChange') + editorView.getBuffer().file.on 'contents-changed', fileChangeHandler + + editorView.checkoutHead() + + waitsFor "file to trigger contents-changed event", -> + fileChangeHandler.callCount > 0 + + runs -> + expect(editorView.getText()).toBe(originalPathText) + + describe ".pixelPositionForBufferPosition(position)", -> + describe "when the editor view is detached", -> + it "returns top and left values of 0", -> + expect(editorView.isOnDom()).toBeFalsy() + expect(editorView.pixelPositionForBufferPosition([2,7])).toEqual top: 0, left: 0 + + describe "when the editor view is invisible", -> + it "returns top and left values of 0", -> + editorView.attachToDom() + editorView.hide() + expect(editorView.isVisible()).toBeFalsy() + expect(editorView.pixelPositionForBufferPosition([2,7])).toEqual top: 0, left: 0 + + describe "when the editor view is attached and visible", -> + beforeEach -> + editorView.attachToDom() + + it "returns the top and left pixel positions", -> + expect(editorView.pixelPositionForBufferPosition([2,7])).toEqual top: 40, left: 70 + + it "caches the left position", -> + editorView.renderedLines.css('font-size', '16px') + expect(editorView.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 + + # make characters smaller + editorView.renderedLines.css('font-size', '15px') + + expect(editorView.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 + + describe "when clicking in the gutter", -> + beforeEach -> + editorView.attachToDom() + + describe "when single clicking", -> + it "moves the cursor to the start of the selected line", -> + expect(editorView.getCursorScreenPosition()).toEqual [0,0] + event = $.Event("mousedown") + event.pageY = editorView.gutter.find(".line-number:eq(1)").offset().top + event.originalEvent = {detail: 1} + editorView.gutter.find(".line-number:eq(1)").trigger event + expect(editorView.getCursorScreenPosition()).toEqual [1,0] + + describe "when shift-clicking", -> + it "selects to the start of the selected line", -> + expect(editorView.getSelection().getScreenRange()).toEqual [[0,0], [0,0]] + event = $.Event("mousedown") + event.pageY = editorView.gutter.find(".line-number:eq(1)").offset().top + event.originalEvent = {detail: 1} + event.shiftKey = true + editorView.gutter.find(".line-number:eq(1)").trigger event + expect(editorView.getSelection().getScreenRange()).toEqual [[0,0], [2,0]] + + describe "when mousing down and then moving across multiple lines before mousing up", -> + describe "when selecting from top to bottom", -> + it "selects the lines", -> + mousedownEvent = $.Event("mousedown") + mousedownEvent.pageY = editorView.gutter.find(".line-number:eq(1)").offset().top + mousedownEvent.originalEvent = {detail: 1} + editorView.gutter.find(".line-number:eq(1)").trigger mousedownEvent + + mousemoveEvent = $.Event("mousemove") + mousemoveEvent.pageY = editorView.gutter.find(".line-number:eq(5)").offset().top + mousemoveEvent.originalEvent = {detail: 1} + editorView.gutter.find(".line-number:eq(5)").trigger mousemoveEvent + + $(document).trigger 'mouseup' + + expect(editorView.getSelection().getScreenRange()).toEqual [[1,0], [6,0]] + + describe "when selecting from bottom to top", -> + it "selects the lines", -> + mousedownEvent = $.Event("mousedown") + mousedownEvent.pageY = editorView.gutter.find(".line-number:eq(5)").offset().top + mousedownEvent.originalEvent = {detail: 1} + editorView.gutter.find(".line-number:eq(5)").trigger mousedownEvent + + mousemoveEvent = $.Event("mousemove") + mousemoveEvent.pageY = editorView.gutter.find(".line-number:eq(1)").offset().top + mousemoveEvent.originalEvent = {detail: 1} + editorView.gutter.find(".line-number:eq(1)").trigger mousemoveEvent + + $(document).trigger 'mouseup' + + expect(editorView.getSelection().getScreenRange()).toEqual [[1,0], [6,0]] + + describe "when clicking below the last line", -> + beforeEach -> + editorView.attachToDom() + + it "move the cursor to the end of the file", -> + expect(editorView.getCursorScreenPosition()).toEqual [0,0] + event = mousedownEvent(editorView: editorView, point: [Infinity, 10]) + editorView.underlayer.trigger event + expect(editorView.getCursorScreenPosition()).toEqual [12,2] + + it "selects to the end of the files when shift is pressed", -> + expect(editorView.getSelection().getScreenRange()).toEqual [[0,0], [0,0]] + event = mousedownEvent(editorView: editorView, point: [Infinity, 10], shiftKey: true) + editorView.underlayer.trigger event + expect(editorView.getSelection().getScreenRange()).toEqual [[0,0], [12,2]] + + describe ".reloadGrammar()", -> + [filePath] = [] + + beforeEach -> + tmpdir = fs.absolute(temp.dir) + filePath = path.join(tmpdir, "grammar-change.txt") + fs.writeFileSync(filePath, "var i;") + + afterEach -> + fs.removeSync(filePath) if fs.existsSync(filePath) + + it "updates all the rendered lines when the grammar changes", -> + editorView.edit(project.openSync(filePath)) + expect(editorView.getGrammar().name).toBe 'Plain Text' + atom.syntax.setGrammarOverrideForPath(filePath, 'source.js') + editorView.reloadGrammar() + expect(editorView.getGrammar().name).toBe 'JavaScript' + + tokenizedBuffer = editorView.activeEditSession.displayBuffer.tokenizedBuffer + line0 = tokenizedBuffer.lineForScreenRow(0) + expect(line0.tokens.length).toBe 3 + expect(line0.tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.modifier.js']) + + it "doesn't update the rendered lines when the grammar doesn't change", -> + expect(editorView.getGrammar().name).toBe 'JavaScript' + spyOn(editorView, 'updateDisplay').andCallThrough() + editorView.reloadGrammar() + expect(editorView.reloadGrammar()).toBeFalsy() + expect(editorView.updateDisplay).not.toHaveBeenCalled() + expect(editorView.getGrammar().name).toBe 'JavaScript' + + it "emits an editor:grammar-changed event when updated", -> + editorView.edit(project.openSync(filePath)) + + eventHandler = jasmine.createSpy('eventHandler') + editorView.on('editor:grammar-changed', eventHandler) + editorView.reloadGrammar() + + expect(eventHandler).not.toHaveBeenCalled() + + atom.syntax.setGrammarOverrideForPath(filePath, 'source.js') + editorView.reloadGrammar() + expect(eventHandler).toHaveBeenCalled() + + describe ".replaceSelectedText()", -> + it "doesn't call the replace function when the selection is empty", -> + replaced = false + edited = false + replacer = (text) -> + replaced = true + 'new' + + editorView.moveCursorToTop() + edited = editorView.replaceSelectedText(replacer) + expect(replaced).toBe false + expect(edited).toBe false + + it "returns true when transformed text is non-empty", -> + replaced = false + edited = false + replacer = (text) -> + replaced = true + 'new' + + editorView.moveCursorToTop() + editorView.selectToEndOfLine() + edited = editorView.replaceSelectedText(replacer) + expect(replaced).toBe true + expect(edited).toBe true + + it "returns false when transformed text is null", -> + replaced = false + edited = false + replacer = (text) -> + replaced = true + null + + editorView.moveCursorToTop() + editorView.selectToEndOfLine() + edited = editorView.replaceSelectedText(replacer) + expect(replaced).toBe true + expect(edited).toBe false + + it "returns false when transformed text is undefined", -> + replaced = false + edited = false + replacer = (text) -> + replaced = true + undefined + + editorView.moveCursorToTop() + editorView.selectToEndOfLine() + edited = editorView.replaceSelectedText(replacer) + expect(replaced).toBe true + expect(edited).toBe false + + describe "when editor:copy-path is triggered", -> + it "copies the absolute path to the editor view's file to the pasteboard", -> + editorView.trigger 'editor:copy-path' + expect(atom.pasteboard.read()[0]).toBe editorView.getPath() + + describe "when editor:move-line-up is triggered", -> + describe "when there is no selection", -> + it "moves the line where the cursor is up", -> + editorView.setCursorBufferPosition([1,0]) + editorView.trigger 'editor:move-line-up' + expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' + expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' + + it "moves the cursor to the new row and the same column", -> + editorView.setCursorBufferPosition([1,2]) + editorView.trigger 'editor:move-line-up' + expect(editorView.getCursorBufferPosition()).toEqual [0,2] + + describe "where there is a selection", -> + describe "when the selection falls inside the line", -> + it "maintains the selection", -> + editorView.setSelectedBufferRange([[1, 2], [1, 5]]) + expect(editorView.getSelectedText()).toBe 'var' + editorView.trigger 'editor:move-line-up' + expect(editorView.getSelectedBufferRange()).toEqual [[0, 2], [0, 5]] + expect(editorView.getSelectedText()).toBe 'var' + + describe "where there are multiple lines selected", -> + it "moves the selected lines up", -> + editorView.setSelectedBufferRange([[2, 0], [3, Infinity]]) + editorView.trigger 'editor:move-line-up' + expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' + expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;' + expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(buffer.lineForRow(3)).toBe ' var sort = function(items) {' + + it "maintains the selection", -> + editorView.setSelectedBufferRange([[2, 0], [3, 62]]) + editorView.trigger 'editor:move-line-up' + expect(editorView.getSelectedBufferRange()).toEqual [[1, 0], [2, 62]] + + describe "when the last line is selected", -> + it "moves the selected line up", -> + editorView.setSelectedBufferRange([[12, 0], [12, Infinity]]) + editorView.trigger 'editor:move-line-up' + expect(buffer.lineForRow(11)).toBe '};' + expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));' + + describe "when the last two lines are selected", -> + it "moves the selected lines up", -> + editorView.setSelectedBufferRange([[11, 0], [12, Infinity]]) + editorView.trigger 'editor:move-line-up' + expect(buffer.lineForRow(10)).toBe ' return sort(Array.apply(this, arguments));' + expect(buffer.lineForRow(11)).toBe '};' + expect(buffer.lineForRow(12)).toBe '' + + describe "when the cursor is on the first line", -> + it "does not move the line", -> + editorView.setCursorBufferPosition([0,0]) + originalText = editorView.getText() + editorView.trigger 'editor:move-line-up' + expect(editorView.getText()).toBe originalText + + describe "when the cursor is on the trailing newline", -> + it "does not move the line", -> + editorView.moveCursorToBottom() + editorView.insertNewline() + editorView.moveCursorToBottom() + originalText = editorView.getText() + editorView.trigger 'editor:move-line-up' + expect(editorView.getText()).toBe originalText + + describe "when the cursor is on a folded line", -> + it "moves all lines in the fold up and preserves the fold", -> + editorView.setCursorBufferPosition([4, 0]) + editorView.foldCurrentRow() + editorView.trigger 'editor:move-line-up' + expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' + expect(buffer.lineForRow(7)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(editorView.getSelectedBufferRange()).toEqual [[3, 0], [3, 0]] + expect(editorView.isFoldedAtScreenRow(3)).toBeTruthy() + + describe "when the selection contains a folded and unfolded line", -> + it "moves the selected lines up and preserves the fold", -> + editorView.setCursorBufferPosition([4, 0]) + editorView.foldCurrentRow() + editorView.setCursorBufferPosition([3, 4]) + editorView.selectDown() + expect(editorView.isFoldedAtScreenRow(4)).toBeTruthy() + editorView.trigger 'editor:move-line-up' + expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' + expect(editorView.getSelectedBufferRange()).toEqual [[2, 4], [3, 0]] + expect(editorView.isFoldedAtScreenRow(3)).toBeTruthy() + + describe "when an entire line is selected including the newline", -> + it "moves the selected line up", -> + editorView.setCursorBufferPosition([1]) + editorView.selectToEndOfLine() + editorView.selectRight() + editorView.trigger 'editor:move-line-up' + expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' + expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' + + describe "when editor:move-line-down is triggered", -> + describe "when there is no selection", -> + it "moves the line where the cursor is down", -> + editorView.setCursorBufferPosition([0, 0]) + editorView.trigger 'editor:move-line-down' + expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' + expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' + + it "moves the cursor to the new row and the same column", -> + editorView.setCursorBufferPosition([0, 2]) + editorView.trigger 'editor:move-line-down' + expect(editorView.getCursorBufferPosition()).toEqual [1, 2] + + describe "when the cursor is on the last line", -> + it "does not move the line", -> + editorView.moveCursorToBottom() + editorView.trigger 'editor:move-line-down' + expect(buffer.lineForRow(12)).toBe '};' + expect(editorView.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]] + + describe "when the cursor is on the second to last line", -> + it "moves the line down", -> + editorView.setCursorBufferPosition([11, 0]) + editorView.trigger 'editor:move-line-down' + expect(buffer.lineForRow(11)).toBe '};' + expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));' + expect(buffer.lineForRow(13)).toBeUndefined() + + describe "when the cursor is on the second to last line and the last line is empty", -> + it "does not move the line", -> + editorView.moveCursorToBottom() + editorView.insertNewline() + editorView.setCursorBufferPosition([12, 2]) + editorView.trigger 'editor:move-line-down' + expect(buffer.lineForRow(12)).toBe '};' + expect(buffer.lineForRow(13)).toBe '' + expect(editorView.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]] + + describe "where there is a selection", -> + describe "when the selection falls inside the line", -> + it "maintains the selection", -> + editorView.setSelectedBufferRange([[1, 2], [1, 5]]) + expect(editorView.getSelectedText()).toBe 'var' + editorView.trigger 'editor:move-line-down' + expect(editorView.getSelectedBufferRange()).toEqual [[2, 2], [2, 5]] + expect(editorView.getSelectedText()).toBe 'var' + + describe "where there are multiple lines selected", -> + it "moves the selected lines down", -> + editorView.setSelectedBufferRange([[2, 0], [3, Infinity]]) + editorView.trigger 'editor:move-line-down' + expect(buffer.lineForRow(2)).toBe ' while(items.length > 0) {' + expect(buffer.lineForRow(3)).toBe ' if (items.length <= 1) return items;' + expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(buffer.lineForRow(5)).toBe ' current = items.shift();' + + it "maintains the selection", -> + editorView.setSelectedBufferRange([[2, 0], [3, 62]]) + editorView.trigger 'editor:move-line-down' + expect(editorView.getSelectedBufferRange()).toEqual [[3, 0], [4, 62]] + + describe "when the cursor is on a folded line", -> + it "moves all lines in the fold down and preserves the fold", -> + editorView.setCursorBufferPosition([4, 0]) + editorView.foldCurrentRow() + editorView.trigger 'editor:move-line-down' + expect(buffer.lineForRow(4)).toBe ' return sort(left).concat(pivot).concat(sort(right));' + expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {' + expect(editorView.getSelectedBufferRange()).toEqual [[5, 0], [5, 0]] + expect(editorView.isFoldedAtScreenRow(5)).toBeTruthy() + + describe "when the selection contains a folded and unfolded line", -> + it "moves the selected lines down and preserves the fold", -> + editorView.setCursorBufferPosition([4, 0]) + editorView.foldCurrentRow() + editorView.setCursorBufferPosition([3, 4]) + editorView.selectDown() + expect(editorView.isFoldedAtScreenRow(4)).toBeTruthy() + editorView.trigger 'editor:move-line-down' + expect(buffer.lineForRow(3)).toBe ' return sort(left).concat(pivot).concat(sort(right));' + expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {' + expect(editorView.getSelectedBufferRange()).toEqual [[4, 4], [5, 0]] + expect(editorView.isFoldedAtScreenRow(5)).toBeTruthy() + + describe "when an entire line is selected including the newline", -> + it "moves the selected line down", -> + editorView.setCursorBufferPosition([1]) + editorView.selectToEndOfLine() + editorView.selectRight() + editorView.trigger 'editor:move-line-down' + expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;' + expect(buffer.lineForRow(2)).toBe ' var sort = function(items) {' + + describe "when editor:duplicate-line is triggered", -> + describe "where there is no selection", -> + describe "when the cursor isn't on a folded line", -> + it "duplicates the current line below and moves the cursor down one row", -> + editorView.setCursorBufferPosition([0, 5]) + editorView.trigger 'editor:duplicate-line' + expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' + expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' + expect(editorView.getCursorBufferPosition()).toEqual [1, 5] + + describe "when the cursor is on a folded line", -> + it "duplicates the entire fold before and moves the cursor to the new fold", -> + editorView.setCursorBufferPosition([4]) + editorView.foldCurrentRow() + editorView.trigger 'editor:duplicate-line' + expect(editorView.getCursorScreenPosition()).toEqual [5] + expect(editorView.isFoldedAtScreenRow(4)).toBeTruthy() + expect(editorView.isFoldedAtScreenRow(5)).toBeTruthy() + expect(buffer.lineForRow(8)).toBe ' while(items.length > 0) {' + expect(buffer.lineForRow(9)).toBe ' current = items.shift();' + expect(buffer.lineForRow(10)).toBe ' current < pivot ? left.push(current) : right.push(current);' + expect(buffer.lineForRow(11)).toBe ' }' + + describe "when the cursor is on the last line and it doesn't have a trailing newline", -> + it "inserts a newline and the duplicated line", -> + editorView.moveCursorToBottom() + editorView.trigger 'editor:duplicate-line' + expect(buffer.lineForRow(12)).toBe '};' + expect(buffer.lineForRow(13)).toBe '};' + expect(buffer.lineForRow(14)).toBeUndefined() + expect(editorView.getCursorBufferPosition()).toEqual [13, 2] + + describe "when the cursor in on the last line and it is only a newline", -> + it "duplicates the current line below and moves the cursor down one row", -> + editorView.moveCursorToBottom() + editorView.insertNewline() + editorView.moveCursorToBottom() + editorView.trigger 'editor:duplicate-line' + expect(buffer.lineForRow(13)).toBe '' + expect(buffer.lineForRow(14)).toBe '' + expect(buffer.lineForRow(15)).toBeUndefined() + expect(editorView.getCursorBufferPosition()).toEqual [14, 0] + + describe "when the cursor is on the second to last line and the last line only a newline", -> + it "duplicates the current line below and moves the cursor down one row", -> + editorView.moveCursorToBottom() + editorView.insertNewline() + editorView.setCursorBufferPosition([12]) + editorView.trigger 'editor:duplicate-line' + expect(buffer.lineForRow(12)).toBe '};' + expect(buffer.lineForRow(13)).toBe '};' + expect(buffer.lineForRow(14)).toBe '' + expect(buffer.lineForRow(15)).toBeUndefined() + expect(editorView.getCursorBufferPosition()).toEqual [13, 0] + + describe "editor:save-debug-snapshot", -> + it "saves the state of the rendered lines, the display buffer, and the buffer to a file of the user's choosing", -> + saveDialogCallback = null + spyOn(atom, 'showSaveDialog').andCallFake (callback) -> saveDialogCallback = callback + spyOn(fs, 'writeFileSync') + + editorView.trigger 'editor:save-debug-snapshot' + + statePath = path.join(temp.dir, 'state') + expect(atom.showSaveDialog).toHaveBeenCalled() + saveDialogCallback(statePath) + expect(fs.writeFileSync).toHaveBeenCalled() + expect(fs.writeFileSync.argsForCall[0][0]).toBe statePath + expect(typeof fs.writeFileSync.argsForCall[0][1]).toBe 'string' + + describe "when the escape key is pressed on the editor view", -> + it "clears multiple selections if there are any, and otherwise allows other bindings to be handled", -> + keymap.bindKeys 'name', '.editor', 'escape': 'test-event' + testEventHandler = jasmine.createSpy("testEventHandler") + + editorView.on 'test-event', testEventHandler + editorView.activeEditSession.addSelectionForBufferRange([[3, 0], [3, 0]]) + expect(editorView.activeEditSession.getSelections().length).toBe 2 + + editorView.trigger(keydownEvent('escape')) + expect(editorView.activeEditSession.getSelections().length).toBe 1 + expect(testEventHandler).not.toHaveBeenCalled() + + editorView.trigger(keydownEvent('escape')) + expect(testEventHandler).toHaveBeenCalled() + + describe "when the editor view is attached but invisible", -> + describe "when the editor view's text is changed", -> + it "redraws the editor view when it is next shown", -> + window.rootView = new RootView + rootView.openSync('sample.js') + rootView.attachToDom() + editorView = rootView.getActiveView() + + view = $$ -> @div id: 'view', tabindex: -1, 'View' + editorView.getPane().showItem(view) + expect(editorView.isVisible()).toBeFalsy() + + editorView.setText('hidden changes') + editorView.setCursorBufferPosition([0,4]) + + displayUpdatedHandler = jasmine.createSpy("displayUpdatedHandler") + editorView.on 'editor:display-updated', displayUpdatedHandler + editorView.getPane().showItem(editorView.getModel()) + expect(editorView.isVisible()).toBeTruthy() + + waitsFor -> + displayUpdatedHandler.callCount is 1 + + runs -> + expect(editorView.renderedLines.find('.line').text()).toBe 'hidden changes' + + it "redraws the editor view when it is next reattached", -> + editorView.attachToDom() + editorView.hide() + editorView.setText('hidden changes') + editorView.setCursorBufferPosition([0,4]) + editorView.detach() + + displayUpdatedHandler = jasmine.createSpy("displayUpdatedHandler") + editorView.on 'editor:display-updated', displayUpdatedHandler + editorView.show() + editorView.attachToDom() + + waitsFor -> + displayUpdatedHandler.callCount is 1 + + runs -> + expect(editorView.renderedLines.find('.line').text()).toBe 'hidden changes' + + describe "editor:scroll-to-cursor", -> + it "scrolls to and centers the editor view on the cursor's position", -> + editorView.attachToDom(heightInLines: 3) + editorView.setCursorBufferPosition([1, 2]) + editorView.scrollToBottom() + expect(editorView.getFirstVisibleScreenRow()).not.toBe 0 + expect(editorView.getLastVisibleScreenRow()).not.toBe 2 + editorView.trigger('editor:scroll-to-cursor') + expect(editorView.getFirstVisibleScreenRow()).toBe 0 + expect(editorView.getLastVisibleScreenRow()).toBe 2 + + describe "when the editor view is removed", -> + it "fires a editor:will-be-removed event", -> + window.rootView = new RootView + rootView.openSync('sample.js') + rootView.attachToDom() + editorView = rootView.getActiveView() + + willBeRemovedHandler = jasmine.createSpy('fileChange') + editorView.on 'editor:will-be-removed', willBeRemovedHandler + editorView.getPane().destroyActiveItem() + expect(willBeRemovedHandler).toHaveBeenCalled() + + describe "when setInvisibles is toggled (regression)", -> + it "renders inserted newlines properly", -> + editorView.setShowInvisibles(true) + editorView.setCursorBufferPosition([0, 0]) + editorView.attachToDom(heightInLines: 20) + editorView.setShowInvisibles(false) + editorView.insertText("\n") + + for rowNumber in [1..5] + expect(editorView.lineElementForScreenRow(rowNumber).text()).toBe buffer.lineForRow(rowNumber) + + describe "when the window is resized", -> + it "updates the active edit session with the current soft wrap column", -> + editorView.attachToDom() + setEditorWidthInChars(editorView, 50) + expect(editorView.activeEditSession.getSoftWrapColumn()).toBe 50 + setEditorWidthInChars(editorView, 100) + $(window).trigger 'resize' + expect(editorView.activeEditSession.getSoftWrapColumn()).toBe 100 diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index a1bd31db4..b28f1094e 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -208,44 +208,44 @@ describe "Git", -> expect(repo.isStatusModified(statuses[modifiedPath])).toBeTruthy() describe "buffer events", -> - [originalContent, editSession] = [] + [originalContent, editor] = [] beforeEach -> - editSession = project.openSync('sample.js') - originalContent = editSession.getText() + editor = project.openSync('sample.js') + originalContent = editor.getText() afterEach -> - fs.writeFileSync(editSession.getPath(), originalContent) + fs.writeFileSync(editor.getPath(), originalContent) it "emits a status-changed event when a buffer is saved", -> - editSession.insertNewline() + editor.insertNewline() statusHandler = jasmine.createSpy('statusHandler') project.getRepo().on 'status-changed', statusHandler - editSession.save() + editor.save() expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith editSession.getPath(), 256 + expect(statusHandler).toHaveBeenCalledWith editor.getPath(), 256 it "emits a status-changed event when a buffer is reloaded", -> - fs.writeFileSync(editSession.getPath(), 'changed') + fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') project.getRepo().on 'status-changed', statusHandler - editSession.getBuffer().reload() + editor.getBuffer().reload() expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith editSession.getPath(), 256 - editSession.getBuffer().reload() + expect(statusHandler).toHaveBeenCalledWith editor.getPath(), 256 + editor.getBuffer().reload() expect(statusHandler.callCount).toBe 1 it "emits a status-changed event when a buffer's path changes", -> - fs.writeFileSync(editSession.getPath(), 'changed') + fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') project.getRepo().on 'status-changed', statusHandler - editSession.getBuffer().emit 'path-changed' + editor.getBuffer().emit 'path-changed' expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith editSession.getPath(), 256 - editSession.getBuffer().emit 'path-changed' + expect(statusHandler).toHaveBeenCalledWith editor.getPath(), 256 + editor.getBuffer().emit 'path-changed' expect(statusHandler.callCount).toBe 1 describe "when a project is deserialized", -> diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee index b27a15c68..3d1db09d0 100644 --- a/spec/language-mode-spec.coffee +++ b/spec/language-mode-spec.coffee @@ -1,14 +1,14 @@ describe "LanguageMode", -> - [editSession, buffer, languageMode] = [] + [editor, buffer, languageMode] = [] afterEach -> - editSession.destroy() + editor.destroy() describe "javascript", -> beforeEach -> atom.activatePackage('language-javascript', sync: true) - editSession = project.openSync('sample.js', autoIndent: false) - {buffer, languageMode} = editSession + editor = project.openSync('sample.js', autoIndent: false) + {buffer, languageMode} = editor describe ".minIndentLevelForRowRange(startRow, endRow)", -> it "returns the minimum indent level for the given row range", -> @@ -110,8 +110,8 @@ describe "LanguageMode", -> describe "coffeescript", -> beforeEach -> atom.activatePackage('language-coffee-script', sync: true) - editSession = project.openSync('coffee.coffee', autoIndent: false) - {buffer, languageMode} = editSession + editor = project.openSync('coffee.coffee', autoIndent: false) + {buffer, languageMode} = editor describe ".toggleLineCommentsForBufferRows(start, end)", -> it "comments/uncomments lines in the given range", -> @@ -157,8 +157,8 @@ describe "LanguageMode", -> describe "css", -> beforeEach -> atom.activatePackage('language-css', sync: true) - editSession = project.openSync('css.css', autoIndent: false) - {buffer, languageMode} = editSession + editor = project.openSync('css.css', autoIndent: false) + {buffer, languageMode} = editor describe ".toggleLineCommentsForBufferRows(start, end)", -> it "comments/uncomments lines in the given range", -> @@ -199,8 +199,8 @@ describe "LanguageMode", -> beforeEach -> atom.activatePackage('language-less', sync: true) atom.activatePackage('language-css', sync: true) - editSession = project.openSync('sample.less', autoIndent: false) - {buffer, languageMode} = editSession + editor = project.openSync('sample.less', autoIndent: false) + {buffer, languageMode} = editor describe "when commenting lines", -> it "only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`", -> @@ -210,67 +210,67 @@ describe "LanguageMode", -> describe "folding", -> beforeEach -> atom.activatePackage('language-javascript', sync: true) - editSession = project.openSync('sample.js', autoIndent: false) - {buffer, languageMode} = editSession + editor = project.openSync('sample.js', autoIndent: false) + {buffer, languageMode} = editor it "maintains cursor buffer position when a folding/unfolding", -> - editSession.setCursorBufferPosition([5,5]) + editor.setCursorBufferPosition([5,5]) languageMode.foldAll() - expect(editSession.getCursorBufferPosition()).toEqual([5,5]) + expect(editor.getCursorBufferPosition()).toEqual([5,5]) describe ".unfoldAll()", -> it "unfolds every folded line", -> - initialScreenLineCount = editSession.getScreenLineCount() + initialScreenLineCount = editor.getScreenLineCount() languageMode.foldBufferRow(0) languageMode.foldBufferRow(1) - expect(editSession.getScreenLineCount()).toBeLessThan initialScreenLineCount + expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount languageMode.unfoldAll() - expect(editSession.getScreenLineCount()).toBe initialScreenLineCount + expect(editor.getScreenLineCount()).toBe initialScreenLineCount describe ".foldAll()", -> it "folds every foldable line", -> languageMode.foldAll() - fold1 = editSession.lineForScreenRow(0).fold + fold1 = editor.lineForScreenRow(0).fold expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 12] fold1.destroy() - fold2 = editSession.lineForScreenRow(1).fold + fold2 = editor.lineForScreenRow(1).fold expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 9] fold2.destroy() - fold3 = editSession.lineForScreenRow(4).fold + fold3 = editor.lineForScreenRow(4).fold expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [4, 7] describe ".foldBufferRow(bufferRow)", -> describe "when bufferRow can be folded", -> it "creates a fold based on the syntactic region starting at the given row", -> languageMode.foldBufferRow(1) - fold = editSession.lineForScreenRow(1).fold + fold = editor.lineForScreenRow(1).fold expect(fold.getStartRow()).toBe 1 expect(fold.getEndRow()).toBe 9 describe "when bufferRow can't be folded", -> it "searches upward for the first row that begins a syntatic region containing the given buffer row (and folds it)", -> languageMode.foldBufferRow(8) - fold = editSession.lineForScreenRow(1).fold + fold = editor.lineForScreenRow(1).fold expect(fold.getStartRow()).toBe 1 expect(fold.getEndRow()).toBe 9 describe "when the bufferRow is already folded", -> it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", -> languageMode.foldBufferRow(2) - expect(editSession.lineForScreenRow(1).fold).toBeDefined() - expect(editSession.lineForScreenRow(0).fold).not.toBeDefined() + expect(editor.lineForScreenRow(1).fold).toBeDefined() + expect(editor.lineForScreenRow(0).fold).not.toBeDefined() languageMode.foldBufferRow(1) - expect(editSession.lineForScreenRow(0).fold).toBeDefined() + expect(editor.lineForScreenRow(0).fold).toBeDefined() describe "when the bufferRow is in a multi-line comment", -> it "searches upward and downward for surrounding comment lines and folds them as a single fold", -> buffer.insert([1,0], " //this is a comment\n // and\n //more docs\n\n//second comment") languageMode.foldBufferRow(1) - fold = editSession.lineForScreenRow(1).fold + fold = editor.lineForScreenRow(1).fold expect(fold.getStartRow()).toBe 1 expect(fold.getEndRow()).toBe 3 @@ -278,7 +278,7 @@ describe "LanguageMode", -> it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", -> buffer.insert([1,0], " //this is a single line comment\n") languageMode.foldBufferRow(1) - fold = editSession.lineForScreenRow(0).fold + fold = editor.lineForScreenRow(0).fold expect(fold.getStartRow()).toBe 0 expect(fold.getEndRow()).toBe 13 @@ -286,77 +286,77 @@ describe "LanguageMode", -> describe "when bufferRow can be unfolded", -> it "destroys a fold based on the syntactic region starting at the given row", -> languageMode.foldBufferRow(1) - expect(editSession.lineForScreenRow(1).fold).toBeDefined() + expect(editor.lineForScreenRow(1).fold).toBeDefined() languageMode.unfoldBufferRow(1) - expect(editSession.lineForScreenRow(1).fold).toBeUndefined() + expect(editor.lineForScreenRow(1).fold).toBeUndefined() describe "when bufferRow can't be unfolded", -> it "does not throw an error", -> - expect(editSession.lineForScreenRow(1).fold).toBeUndefined() + expect(editor.lineForScreenRow(1).fold).toBeUndefined() languageMode.unfoldBufferRow(1) - expect(editSession.lineForScreenRow(1).fold).toBeUndefined() + expect(editor.lineForScreenRow(1).fold).toBeUndefined() describe "folding with comments", -> beforeEach -> atom.activatePackage('language-javascript', sync: true) - editSession = project.openSync('sample-with-comments.js', autoIndent: false) - {buffer, languageMode} = editSession + editor = project.openSync('sample-with-comments.js', autoIndent: false) + {buffer, languageMode} = editor describe ".unfoldAll()", -> it "unfolds every folded line", -> - initialScreenLineCount = editSession.getScreenLineCount() + initialScreenLineCount = editor.getScreenLineCount() languageMode.foldBufferRow(0) languageMode.foldBufferRow(5) - expect(editSession.getScreenLineCount()).toBeLessThan initialScreenLineCount + expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount languageMode.unfoldAll() - expect(editSession.getScreenLineCount()).toBe initialScreenLineCount + expect(editor.getScreenLineCount()).toBe initialScreenLineCount describe ".foldAll()", -> it "folds every foldable line", -> languageMode.foldAll() - fold1 = editSession.lineForScreenRow(0).fold + fold1 = editor.lineForScreenRow(0).fold expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 19] fold1.destroy() - fold2 = editSession.lineForScreenRow(1).fold + fold2 = editor.lineForScreenRow(1).fold expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 4] - fold3 = editSession.lineForScreenRow(2).fold.destroy() + fold3 = editor.lineForScreenRow(2).fold.destroy() - fold4 = editSession.lineForScreenRow(3).fold + fold4 = editor.lineForScreenRow(3).fold expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [6, 8] describe ".foldAllAtIndentLevel()", -> it "folds every foldable range at a given indentLevel", -> languageMode.foldAllAtIndentLevel(2) - fold1 = editSession.lineForScreenRow(6).fold + fold1 = editor.lineForScreenRow(6).fold expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [6, 8] fold1.destroy() - fold2 = editSession.lineForScreenRow(11).fold + fold2 = editor.lineForScreenRow(11).fold expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 14] fold2.destroy() it "does not fold anything but the indentLevel", -> languageMode.foldAllAtIndentLevel(0) - fold1 = editSession.lineForScreenRow(0).fold + fold1 = editor.lineForScreenRow(0).fold expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 19] fold1.destroy() - fold2 = editSession.lineForScreenRow(5).fold + fold2 = editor.lineForScreenRow(5).fold expect(fold2).toBeFalsy() describe "css", -> beforeEach -> atom.activatePackage('language-source', sync: true) atom.activatePackage('language-css', sync: true) - editSession = project.openSync('css.css', autoIndent: true) + editor = project.openSync('css.css', autoIndent: true) describe "suggestedIndentForBufferRow", -> it "does not return negative values (regression)", -> - editSession.setText('.test {\npadding: 0;\n}') - expect(editSession.suggestedIndentForBufferRow(2)).toBe 0 + editor.setText('.test {\npadding: 0;\n}') + expect(editor.suggestedIndentForBufferRow(2)).toBe 0 diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index d4ae4e057..d1f14541e 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -111,11 +111,11 @@ describe "PaneContainer", -> describe "when the last-closed pane item is an edit session", -> it "reopens the edit session (regression)", -> - editSession = project.openSync('sample.js') - pane3.showItem(editSession) - pane3.destroyItem(editSession) + editor = project.openSync('sample.js') + pane3.showItem(editor) + pane3.destroyItem(editor) expect(container.reopenItem()).toBeTruthy() - expect(pane3.activeItem.getPath()).toBe editSession.getPath() + expect(pane3.activeItem.getPath()).toBe editor.getPath() expect(container.reopenItem()).toBeFalsy() describe "when there is no active pane", -> diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index b892995c7..29d40105d 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -5,7 +5,7 @@ path = require 'path' temp = require 'temp' describe "Pane", -> - [container, view1, view2, editSession1, editSession2, pane] = [] + [container, view1, view2, editor1, editor2, pane] = [] class TestView extends View @deserialize: ({id, text}) -> new TestView({id, text}) @@ -20,9 +20,9 @@ describe "Pane", -> container = new PaneContainer view1 = new TestView(id: 'view-1', text: 'View 1') view2 = new TestView(id: 'view-2', text: 'View 2') - editSession1 = project.openSync('sample.js') - editSession2 = project.openSync('sample.txt') - pane = new Pane(view1, editSession1, view2, editSession2) + editor1 = project.openSync('sample.js') + editor2 = project.openSync('sample.txt') + pane = new Pane(view1, editor1, view2, editor2) container.setRoot(pane) afterEach -> @@ -52,9 +52,9 @@ describe "Pane", -> expect(itemChangedHandler.argsForCall[0][1]).toBe view2 itemChangedHandler.reset() - pane.showItem(editSession1) + pane.showItem(editor1) expect(itemChangedHandler).toHaveBeenCalled() - expect(itemChangedHandler.argsForCall[0][1]).toBe editSession1 + expect(itemChangedHandler.argsForCall[0][1]).toBe editor1 itemChangedHandler.reset() describe "if the pane's active view is focused before calling showItem", -> @@ -70,12 +70,12 @@ describe "Pane", -> view3 = null beforeEach -> view3 = new TestView(id: 'view-3', text: "View 3") - pane.showItem(editSession1) + pane.showItem(editor1) expect(pane.getActiveItemIndex()).toBe 1 it "adds it to the items list after the active item", -> pane.showItem(view3) - expect(pane.getItems()).toEqual [view1, editSession1, view3, view2, editSession2] + expect(pane.getItems()).toEqual [view1, editor1, view3, view2, editor2] expect(pane.activeItem).toBe view3 expect(pane.getActiveItemIndex()).toBe 2 @@ -89,19 +89,19 @@ describe "Pane", -> describe "when showing a model item", -> describe "when no view has yet been appended for that item", -> it "appends and shows a view to display the item based on its `.getViewClass` method", -> - pane.showItem(editSession1) - editor = pane.activeView - expect(editor.css('display')).not.toBe 'none' - expect(editor.activeEditSession).toBe editSession1 + pane.showItem(editor1) + editorView = pane.activeView + expect(editorView.css('display')).not.toBe 'none' + expect(editorView.activeEditSession).toBe editor1 describe "when a valid view has already been appended for another item", -> it "multiple views are created for multiple items", -> - pane.showItem(editSession1) - pane.showItem(editSession2) + pane.showItem(editor1) + pane.showItem(editor2) expect(pane.itemViews.find('.editor').length).toBe 2 - editor = pane.activeView - expect(editor.css('display')).not.toBe 'none' - expect(editor.activeEditSession).toBe editSession2 + editorView = pane.activeView + expect(editorView.css('display')).not.toBe 'none' + expect(editorView.activeEditSession).toBe editor2 it "creates a new view with the item", -> initialViewCount = pane.itemViews.find('.test-view').length @@ -141,77 +141,77 @@ describe "Pane", -> describe ".destroyItem(item)", -> describe "if the item is not modified", -> it "removes the item and tries to call destroy on it", -> - pane.destroyItem(editSession2) - expect(pane.getItems().indexOf(editSession2)).toBe -1 - expect(editSession2.destroyed).toBeTruthy() + pane.destroyItem(editor2) + expect(pane.getItems().indexOf(editor2)).toBe -1 + expect(editor2.destroyed).toBeTruthy() describe "if the item is modified", -> beforeEach -> - spyOn(editSession2, 'save') - spyOn(editSession2, 'saveAs') + spyOn(editor2, 'save') + spyOn(editor2, 'saveAs') - editSession2.insertText('a') - expect(editSession2.isModified()).toBeTruthy() + editor2.insertText('a') + expect(editor2.isModified()).toBeTruthy() describe "if the [Save] option is selected", -> describe "when the item has a uri", -> it "saves the item before removing and destroying it", -> spyOn(atom, 'confirmSync').andReturn(0) - pane.destroyItem(editSession2) + pane.destroyItem(editor2) - expect(editSession2.save).toHaveBeenCalled() - expect(pane.getItems().indexOf(editSession2)).toBe -1 - expect(editSession2.destroyed).toBeTruthy() + expect(editor2.save).toHaveBeenCalled() + expect(pane.getItems().indexOf(editor2)).toBe -1 + expect(editor2.destroyed).toBeTruthy() describe "when the item has no uri", -> it "presents a save-as dialog, then saves the item with the given uri before removing and destroying it", -> - editSession2.buffer.setPath(undefined) + editor2.buffer.setPath(undefined) spyOn(atom, 'showSaveDialogSync').andReturn("/selected/path") spyOn(atom, 'confirmSync').andReturn(0) - pane.destroyItem(editSession2) + pane.destroyItem(editor2) expect(atom.showSaveDialogSync).toHaveBeenCalled() - expect(editSession2.saveAs).toHaveBeenCalledWith("/selected/path") - expect(pane.getItems().indexOf(editSession2)).toBe -1 - expect(editSession2.destroyed).toBeTruthy() + expect(editor2.saveAs).toHaveBeenCalledWith("/selected/path") + expect(pane.getItems().indexOf(editor2)).toBe -1 + expect(editor2.destroyed).toBeTruthy() describe "if the [Don't Save] option is selected", -> it "removes and destroys the item without saving it", -> spyOn(atom, 'confirmSync').andReturn(2) - pane.destroyItem(editSession2) + pane.destroyItem(editor2) - expect(editSession2.save).not.toHaveBeenCalled() - expect(pane.getItems().indexOf(editSession2)).toBe -1 - expect(editSession2.destroyed).toBeTruthy() + expect(editor2.save).not.toHaveBeenCalled() + expect(pane.getItems().indexOf(editor2)).toBe -1 + expect(editor2.destroyed).toBeTruthy() describe "if the [Cancel] option is selected", -> it "does not save, remove, or destroy the item", -> spyOn(atom, 'confirmSync').andReturn(1) - pane.destroyItem(editSession2) + pane.destroyItem(editor2) - expect(editSession2.save).not.toHaveBeenCalled() - expect(pane.getItems().indexOf(editSession2)).not.toBe -1 - expect(editSession2.destroyed).toBeFalsy() + expect(editor2.save).not.toHaveBeenCalled() + expect(pane.getItems().indexOf(editor2)).not.toBe -1 + expect(editor2.destroyed).toBeFalsy() describe ".removeItem(item)", -> it "removes the item from the items list and shows the next item if it was showing", -> pane.removeItem(view1) - expect(pane.getItems()).toEqual [editSession1, view2, editSession2] - expect(pane.activeItem).toBe editSession1 + expect(pane.getItems()).toEqual [editor1, view2, editor2] + expect(pane.activeItem).toBe editor1 - pane.showItem(editSession2) - pane.removeItem(editSession2) - expect(pane.getItems()).toEqual [editSession1, view2] - expect(pane.activeItem).toBe editSession1 + pane.showItem(editor2) + pane.removeItem(editor2) + expect(pane.getItems()).toEqual [editor1, view2] + expect(pane.activeItem).toBe editor1 it "triggers 'pane:item-removed' with the item and its former index", -> itemRemovedHandler = jasmine.createSpy("itemRemovedHandler") pane.on 'pane:item-removed', itemRemovedHandler - pane.removeItem(editSession1) + pane.removeItem(editor1) expect(itemRemovedHandler).toHaveBeenCalled() - expect(itemRemovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 1] + expect(itemRemovedHandler.argsForCall[0][1..2]).toEqual [editor1, 1] describe "when removing the last item", -> it "removes the pane", -> @@ -236,11 +236,11 @@ describe "Pane", -> describe "when the item is a model", -> it "removes the associated view only when all items that require it have been removed", -> - pane.showItem(editSession1) - pane.showItem(editSession2) - pane.removeItem(editSession2) + pane.showItem(editor1) + pane.showItem(editor2) + pane.removeItem(editor2) expect(pane.itemViews.find('.editor')).toExist() - pane.removeItem(editSession1) + pane.removeItem(editor1) expect(pane.itemViews.find('.editor')).not.toExist() describe ".moveItem(item, index)", -> @@ -249,21 +249,21 @@ describe "Pane", -> pane.on 'pane:item-moved', itemMovedHandler pane.moveItem(view1, 2) - expect(pane.getItems()).toEqual [editSession1, view2, view1, editSession2] + expect(pane.getItems()).toEqual [editor1, view2, view1, editor2] expect(itemMovedHandler).toHaveBeenCalled() expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [view1, 2] itemMovedHandler.reset() - pane.moveItem(editSession1, 3) - expect(pane.getItems()).toEqual [view2, view1, editSession2, editSession1] + pane.moveItem(editor1, 3) + expect(pane.getItems()).toEqual [view2, view1, editor2, editor1] expect(itemMovedHandler).toHaveBeenCalled() - expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 3] + expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editor1, 3] itemMovedHandler.reset() - pane.moveItem(editSession1, 1) - expect(pane.getItems()).toEqual [view2, editSession1, view1, editSession2] + pane.moveItem(editor1, 1) + expect(pane.getItems()).toEqual [view2, editor1, view1, editor2] expect(itemMovedHandler).toHaveBeenCalled() - expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 1] + expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editor1, 1] itemMovedHandler.reset() describe ".moveItemToPane(item, pane, index)", -> @@ -275,21 +275,21 @@ describe "Pane", -> it "moves the item to the given pane at the given index", -> pane.moveItemToPane(view1, pane2, 1) - expect(pane.getItems()).toEqual [editSession1, view2, editSession2] + expect(pane.getItems()).toEqual [editor1, view2, editor2] expect(pane2.getItems()).toEqual [view3, view1] describe "when it is the last item on the source pane", -> it "removes the source pane, but does not destroy the item", -> pane.removeItem(view1) pane.removeItem(view2) - pane.removeItem(editSession2) + pane.removeItem(editor2) - expect(pane.getItems()).toEqual [editSession1] - pane.moveItemToPane(editSession1, pane2, 1) + expect(pane.getItems()).toEqual [editor1] + pane.moveItemToPane(editor1, pane2, 1) expect(pane.hasParent()).toBeFalsy() - expect(pane2.getItems()).toEqual [view3, editSession1] - expect(editSession1.destroyed).toBeFalsy() + expect(pane2.getItems()).toEqual [view3, editor1] + expect(editor1.destroyed).toBeFalsy() describe "when the item is a jQuery object", -> it "preserves data by detaching instead of removing", -> @@ -303,37 +303,37 @@ describe "Pane", -> containerCloseHandler = jasmine.createSpy("containerCloseHandler") container.on 'core:close', containerCloseHandler - pane.showItem(editSession1) + pane.showItem(editor1) initialItemCount = pane.getItems().length pane.trigger 'core:close' expect(pane.getItems().length).toBe initialItemCount - 1 - expect(editSession1.destroyed).toBeTruthy() + expect(editor1.destroyed).toBeTruthy() expect(containerCloseHandler).not.toHaveBeenCalled() describe "pane:close", -> it "destroys all items and removes the pane", -> - pane.showItem(editSession1) + pane.showItem(editor1) pane.trigger 'pane:close' expect(pane.hasParent()).toBeFalsy() - expect(editSession2.destroyed).toBeTruthy() - expect(editSession1.destroyed).toBeTruthy() + expect(editor2.destroyed).toBeTruthy() + expect(editor1.destroyed).toBeTruthy() describe "pane:close-other-items", -> it "destroys all items except the current", -> - pane.showItem(editSession1) + pane.showItem(editor1) pane.trigger 'pane:close-other-items' - expect(editSession2.destroyed).toBeTruthy() - expect(pane.getItems()).toEqual [editSession1] + expect(editor2.destroyed).toBeTruthy() + expect(pane.getItems()).toEqual [editor1] describe "core:save", -> describe "when the current item has a uri", -> describe "when the current item has a save method", -> it "saves the current item", -> - spyOn(editSession2, 'save') - pane.showItem(editSession2) + spyOn(editor2, 'save') + pane.showItem(editor2) pane.trigger 'core:save' - expect(editSession2.save).toHaveBeenCalled() + expect(editor2.save).toHaveBeenCalled() describe "when the current item has no save method", -> it "does nothing", -> @@ -347,14 +347,14 @@ describe "Pane", -> describe "when the current item has a saveAs method", -> it "opens a save dialog and saves the current item as the selected path", -> - spyOn(editSession2, 'saveAs') - editSession2.buffer.setPath(undefined) - pane.showItem(editSession2) + spyOn(editor2, 'saveAs') + editor2.buffer.setPath(undefined) + pane.showItem(editor2) pane.trigger 'core:save' expect(atom.showSaveDialogSync).toHaveBeenCalled() - expect(editSession2.saveAs).toHaveBeenCalledWith('/selected/path') + expect(editor2.saveAs).toHaveBeenCalledWith('/selected/path') describe "when the current item has no saveAs method", -> it "does nothing", -> @@ -368,13 +368,13 @@ describe "Pane", -> describe "when the current item has a saveAs method", -> it "opens the save dialog and calls saveAs on the item with the selected path", -> - spyOn(editSession2, 'saveAs') - pane.showItem(editSession2) + spyOn(editor2, 'saveAs') + pane.showItem(editor2) pane.trigger 'core:save-as' - expect(atom.showSaveDialogSync).toHaveBeenCalledWith(path.dirname(editSession2.getPath())) - expect(editSession2.saveAs).toHaveBeenCalledWith('/selected/path') + expect(atom.showSaveDialogSync).toHaveBeenCalledWith(path.dirname(editor2.getPath())) + expect(editor2.saveAs).toHaveBeenCalledWith('/selected/path') describe "when the current item does not have a saveAs method", -> it "does nothing", -> @@ -386,11 +386,11 @@ describe "Pane", -> it "advances forward/backward through the pane's items, looping around at either end", -> expect(pane.activeItem).toBe view1 pane.trigger 'pane:show-previous-item' - expect(pane.activeItem).toBe editSession2 + expect(pane.activeItem).toBe editor2 pane.trigger 'pane:show-previous-item' expect(pane.activeItem).toBe view2 pane.trigger 'pane:show-next-item' - expect(pane.activeItem).toBe editSession2 + expect(pane.activeItem).toBe editor2 pane.trigger 'pane:show-next-item' expect(pane.activeItem).toBe view1 @@ -424,8 +424,8 @@ describe "Pane", -> describe ".remove()", -> it "destroys all the pane's items", -> pane.remove() - expect(editSession1.destroyed).toBeTruthy() - expect(editSession2.destroyed).toBeTruthy() + expect(editor1.destroyed).toBeTruthy() + expect(editor2.destroyed).toBeTruthy() it "triggers a 'pane:removed' event with the pane", -> removedHandler = jasmine.createSpy("removedHandler") @@ -438,7 +438,7 @@ describe "Pane", -> [paneToLeft, paneToRight] = [] beforeEach -> - pane.showItem(editSession1) + pane.showItem(editor1) paneToLeft = pane.splitLeft(pane.copyActiveItem()) paneToRight = pane.splitRight(pane.copyActiveItem()) container.attachToDom() @@ -492,7 +492,7 @@ describe "Pane", -> describe ".getNextPane()", -> it "returns the next pane if one exists, wrapping around from the last pane to the first", -> - pane.showItem(editSession1) + pane.showItem(editor1) expect(pane.getNextPane()).toBeUndefined pane2 = pane.splitRight(pane.copyActiveItem()) expect(pane.getNextPane()).toBe pane2 @@ -538,7 +538,7 @@ describe "Pane", -> [pane1, view3, view4] = [] beforeEach -> pane1 = pane - pane.showItem(editSession1) + pane.showItem(editor1) view3 = new TestView(id: 'view-3', text: 'View 3') view4 = new TestView(id: 'view-4', text: 'View 4') @@ -547,8 +547,8 @@ describe "Pane", -> # creates the new pane with a copy of the active item if none are given pane2 = pane1.splitRight(pane1.copyActiveItem()) expect(container.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] - expect(pane2.items).toEqual [editSession1] - expect(pane2.activeItem).not.toBe editSession1 # it's a copy + expect(pane2.items).toEqual [editor1] + expect(pane2.activeItem).not.toBe editor1 # it's a copy pane3 = pane2.splitRight(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] @@ -571,8 +571,8 @@ describe "Pane", -> # creates the new pane with a copy of the active item if none are given pane2 = pane.splitLeft(pane1.copyActiveItem()) expect(container.find('.row .pane').toArray()).toEqual [pane2[0], pane[0]] - expect(pane2.items).toEqual [editSession1] - expect(pane2.activeItem).not.toBe editSession1 # it's a copy + expect(pane2.items).toEqual [editor1] + expect(pane2.activeItem).not.toBe editor1 # it's a copy pane3 = pane2.splitLeft(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] @@ -583,8 +583,8 @@ describe "Pane", -> # creates the new pane with a copy of the active item if none are given pane2 = pane.splitDown(pane1.copyActiveItem()) expect(container.find('.column .pane').toArray()).toEqual [pane[0], pane2[0]] - expect(pane2.items).toEqual [editSession1] - expect(pane2.activeItem).not.toBe editSession1 # it's a copy + expect(pane2.items).toEqual [editor1] + expect(pane2.activeItem).not.toBe editor1 # it's a copy pane3 = pane2.splitDown(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] @@ -595,8 +595,8 @@ describe "Pane", -> # creates the new pane with a copy of the active item if none are given pane2 = pane.splitUp(pane1.copyActiveItem()) expect(container.find('.column .pane').toArray()).toEqual [pane2[0], pane[0]] - expect(pane2.items).toEqual [editSession1] - expect(pane2.activeItem).not.toBe editSession1 # it's a copy + expect(pane2.items).toEqual [editor1] + expect(pane2.activeItem).not.toBe editor1 # it's a copy pane3 = pane2.splitUp(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] @@ -673,18 +673,18 @@ describe "Pane", -> describe ".itemForUri(uri)", -> it "returns the item for which a call to .getUri() returns the given uri", -> - expect(pane.itemForUri(editSession1.getUri())).toBe editSession1 - expect(pane.itemForUri(editSession2.getUri())).toBe editSession2 + expect(pane.itemForUri(editor1.getUri())).toBe editor1 + expect(pane.itemForUri(editor2.getUri())).toBe editor2 describe "serialization", -> it "can serialize and deserialize the pane and all its items", -> newPane = deserialize(pane.serialize()) - expect(newPane.getItems()).toEqual [view1, editSession1, view2, editSession2] + expect(newPane.getItems()).toEqual [view1, editor1, view2, editor2] it "restores the active item on deserialization", -> - pane.showItem(editSession2) + pane.showItem(editor2) newPane = deserialize(pane.serialize()) - expect(newPane.activeItem).toEqual editSession2 + expect(newPane.activeItem).toEqual editor2 it "does not show items that cannot be deserialized", -> spyOn(console, 'warn') diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index ec04ade66..a74f4bb5e 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -36,58 +36,58 @@ describe "Project", -> describe "when an edit session is destroyed", -> it "removes edit session and calls destroy on buffer (if buffer is not referenced by other edit sessions)", -> - editSession = project.openSync("a") + editor = project.openSync("a") anotherEditSession = project.openSync("a") - expect(project.editSessions.length).toBe 2 - expect(editSession.buffer).toBe anotherEditSession.buffer + expect(project.editors.length).toBe 2 + expect(editor.buffer).toBe anotherEditSession.buffer - editSession.destroy() - expect(project.editSessions.length).toBe 1 + editor.destroy() + expect(project.editors.length).toBe 1 anotherEditSession.destroy() - expect(project.editSessions.length).toBe 0 + expect(project.editors.length).toBe 0 describe "when an edit session is saved and the project has no path", -> it "sets the project's path to the saved file's parent directory", -> tempFile = temp.openSync().path project.setPath(undefined) expect(project.getPath()).toBeUndefined() - editSession = project.openSync() - editSession.saveAs(tempFile) + editor = project.openSync() + editor.saveAs(tempFile) expect(project.getPath()).toBe path.dirname(tempFile) describe "when an edit session is deserialized", -> - it "emits an 'edit-session-created' event and stores the edit session", -> - handler = jasmine.createSpy('editSessionCreatedHandler') - project.on 'edit-session-created', handler + it "emits an 'editor-created' event and stores the edit session", -> + handler = jasmine.createSpy('editorCreatedHandler') + project.on 'editor-created', handler - editSession1 = project.openSync("a") + editor1 = project.openSync("a") expect(handler.callCount).toBe 1 expect(project.getEditSessions().length).toBe 1 - expect(project.getEditSessions()[0]).toBe editSession1 + expect(project.getEditSessions()[0]).toBe editor1 - editSession2 = deserialize(editSession1.serialize()) + editor2 = deserialize(editor1.serialize()) expect(handler.callCount).toBe 2 expect(project.getEditSessions().length).toBe 2 - expect(project.getEditSessions()[0]).toBe editSession1 - expect(project.getEditSessions()[1]).toBe editSession2 + expect(project.getEditSessions()[0]).toBe editor1 + expect(project.getEditSessions()[1]).toBe editor2 describe "when an edit session is copied", -> - it "emits an 'edit-session-created' event and stores the edit session", -> - handler = jasmine.createSpy('editSessionCreatedHandler') - project.on 'edit-session-created', handler + it "emits an 'editor-created' event and stores the edit session", -> + handler = jasmine.createSpy('editorCreatedHandler') + project.on 'editor-created', handler - editSession1 = project.openSync("a") + editor1 = project.openSync("a") expect(handler.callCount).toBe 1 expect(project.getEditSessions().length).toBe 1 - expect(project.getEditSessions()[0]).toBe editSession1 + expect(project.getEditSessions()[0]).toBe editor1 - editSession2 = editSession1.copy() + editor2 = editor1.copy() expect(handler.callCount).toBe 2 expect(project.getEditSessions().length).toBe 2 - expect(project.getEditSessions()[0]).toBe editSession1 - expect(project.getEditSessions()[1]).toBe editSession2 + expect(project.getEditSessions()[0]).toBe editor1 + expect(project.getEditSessions()[1]).toBe editor2 describe ".openSync(path)", -> [fooOpener, barOpener, absolutePath, newBufferHandler, newEditSessionHandler] = [] @@ -96,7 +96,7 @@ describe "Project", -> newBufferHandler = jasmine.createSpy('newBufferHandler') project.on 'buffer-created', newBufferHandler newEditSessionHandler = jasmine.createSpy('newEditSessionHandler') - project.on 'edit-session-created', newEditSessionHandler + project.on 'editor-created', newEditSessionHandler fooOpener = (pathToOpen, options) -> { foo: pathToOpen, options } if pathToOpen?.match(/\.foo/) barOpener = (pathToOpen) -> { bar: pathToOpen } if pathToOpen?.match(/^bar:\/\//) @@ -109,34 +109,34 @@ describe "Project", -> describe "when passed a path that doesn't match a custom opener", -> describe "when given an absolute path that hasn't been opened previously", -> - it "returns a new edit session for the given path and emits 'buffer-created' and 'edit-session-created' events", -> - editSession = project.openSync(absolutePath) - expect(editSession.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer - expect(newEditSessionHandler).toHaveBeenCalledWith editSession + it "returns a new edit session for the given path and emits 'buffer-created' and 'editor-created' events", -> + editor = project.openSync(absolutePath) + expect(editor.buffer.getPath()).toBe absolutePath + expect(newBufferHandler).toHaveBeenCalledWith editor.buffer + expect(newEditSessionHandler).toHaveBeenCalledWith editor describe "when given a relative path that hasn't been opened previously", -> - it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'edit-session-created' events", -> - editSession = project.openSync('a') - expect(editSession.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer - expect(newEditSessionHandler).toHaveBeenCalledWith editSession + it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'editor-created' events", -> + editor = project.openSync('a') + expect(editor.buffer.getPath()).toBe absolutePath + expect(newBufferHandler).toHaveBeenCalledWith editor.buffer + expect(newEditSessionHandler).toHaveBeenCalledWith editor describe "when passed the path to a buffer that has already been opened", -> - it "returns a new edit session containing previously opened buffer and emits a 'edit-session-created' event", -> - editSession = project.openSync(absolutePath) + it "returns a new edit session containing previously opened buffer and emits a 'editor-created' event", -> + editor = project.openSync(absolutePath) newBufferHandler.reset() - expect(project.openSync(absolutePath).buffer).toBe editSession.buffer - expect(project.openSync('a').buffer).toBe editSession.buffer + expect(project.openSync(absolutePath).buffer).toBe editor.buffer + expect(project.openSync('a').buffer).toBe editor.buffer expect(newBufferHandler).not.toHaveBeenCalled() - expect(newEditSessionHandler).toHaveBeenCalledWith editSession + expect(newEditSessionHandler).toHaveBeenCalledWith editor describe "when not passed a path", -> - it "returns a new edit session and emits 'buffer-created' and 'edit-session-created' events", -> - editSession = project.openSync() - expect(editSession.buffer.getPath()).toBeUndefined() - expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer) - expect(newEditSessionHandler).toHaveBeenCalledWith editSession + it "returns a new edit session and emits 'buffer-created' and 'editor-created' events", -> + editor = project.openSync() + expect(editor.buffer.getPath()).toBeUndefined() + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + expect(newEditSessionHandler).toHaveBeenCalledWith editor describe "when passed a path that matches a custom opener", -> it "returns the resource returned by the custom opener", -> @@ -152,7 +152,7 @@ describe "Project", -> newBufferHandler = jasmine.createSpy('newBufferHandler') project.on 'buffer-created', newBufferHandler newEditSessionHandler = jasmine.createSpy('newEditSessionHandler') - project.on 'edit-session-created', newEditSessionHandler + project.on 'editor-created', newEditSessionHandler fooOpener = (pathToOpen, options) -> { foo: pathToOpen, options } if pathToOpen?.match(/\.foo/) barOpener = (pathToOpen) -> { bar: pathToOpen } if pathToOpen?.match(/^bar:\/\//) @@ -165,50 +165,50 @@ describe "Project", -> describe "when passed a path that doesn't match a custom opener", -> describe "when given an absolute path that isn't currently open", -> - it "returns a new edit session for the given path and emits 'buffer-created' and 'edit-session-created' events", -> - editSession = null + it "returns a new edit session for the given path and emits 'buffer-created' and 'editor-created' events", -> + editor = null waitsForPromise -> - project.open(absolutePath).then (o) -> editSession = o + project.open(absolutePath).then (o) -> editor = o runs -> - expect(editSession.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer - expect(newEditSessionHandler).toHaveBeenCalledWith editSession + expect(editor.buffer.getPath()).toBe absolutePath + expect(newBufferHandler).toHaveBeenCalledWith editor.buffer + expect(newEditSessionHandler).toHaveBeenCalledWith editor describe "when given a relative path that isn't currently opened", -> - it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'edit-session-created' events", -> - editSession = null + it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'editor-created' events", -> + editor = null waitsForPromise -> - project.open(absolutePath).then (o) -> editSession = o + project.open(absolutePath).then (o) -> editor = o runs -> - expect(editSession.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer - expect(newEditSessionHandler).toHaveBeenCalledWith editSession + expect(editor.buffer.getPath()).toBe absolutePath + expect(newBufferHandler).toHaveBeenCalledWith editor.buffer + expect(newEditSessionHandler).toHaveBeenCalledWith editor describe "when passed the path to a buffer that is currently opened", -> - it "returns a new edit session containing currently opened buffer and emits a 'edit-session-created' event", -> - editSession = null + it "returns a new edit session containing currently opened buffer and emits a 'editor-created' event", -> + editor = null waitsForPromise -> - project.open(absolutePath).then (o) -> editSession = o + project.open(absolutePath).then (o) -> editor = o runs -> newBufferHandler.reset() - expect(project.openSync(absolutePath).buffer).toBe editSession.buffer - expect(project.openSync('a').buffer).toBe editSession.buffer + expect(project.openSync(absolutePath).buffer).toBe editor.buffer + expect(project.openSync('a').buffer).toBe editor.buffer expect(newBufferHandler).not.toHaveBeenCalled() - expect(newEditSessionHandler).toHaveBeenCalledWith editSession + expect(newEditSessionHandler).toHaveBeenCalledWith editor describe "when not passed a path", -> - it "returns a new edit session and emits 'buffer-created' and 'edit-session-created' events", -> - editSession = null + it "returns a new edit session and emits 'buffer-created' and 'editor-created' events", -> + editor = null waitsForPromise -> - project.open().then (o) -> editSession = o + project.open().then (o) -> editor = o runs -> - expect(editSession.buffer.getPath()).toBeUndefined() - expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer) - expect(newEditSessionHandler).toHaveBeenCalledWith editSession + expect(editor.buffer.getPath()).toBeUndefined() + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + expect(newEditSessionHandler).toHaveBeenCalledWith editor describe "when passed a path that matches a custom opener", -> it "returns the resource returned by the custom opener", -> @@ -336,8 +336,8 @@ describe "Project", -> describe "when a buffer is already open", -> it "replaces properly and saves when not modified", -> - editSession = project.openSync('sample.js') - expect(editSession.isModified()).toBeFalsy() + editor = project.openSync('sample.js') + expect(editor.isModified()).toBeFalsy() results = [] waitsForPromise -> @@ -349,12 +349,12 @@ describe "Project", -> expect(results[0].filePath).toBe filePath expect(results[0].replacements).toBe 6 - expect(editSession.isModified()).toBeFalsy() + expect(editor.isModified()).toBeFalsy() it "does NOT save when modified", -> - editSession = project.openSync('sample.js') - editSession.buffer.change([[0,0],[0,0]], 'omg') - expect(editSession.isModified()).toBeTruthy() + editor = project.openSync('sample.js') + editor.buffer.change([[0,0],[0,0]], 'omg') + expect(editor.isModified()).toBeTruthy() results = [] waitsForPromise -> @@ -366,7 +366,7 @@ describe "Project", -> expect(results[0].filePath).toBe filePath expect(results[0].replacements).toBe 6 - expect(editSession.isModified()).toBeTruthy() + expect(editor.isModified()).toBeTruthy() describe ".scan(options, callback)", -> describe "when called with a regex", -> @@ -519,8 +519,8 @@ describe "Project", -> expect(resultHandler).not.toHaveBeenCalled() it "scans buffer contents if the buffer is modified", -> - editSession = project.openSync("a") - editSession.setText("Elephant") + editor = project.openSync("a") + editor.setText("Elephant") results = [] waitsForPromise -> project.scan /a|Elephant/, (result) -> results.push result diff --git a/spec/root-view-spec.coffee b/spec/root-view-spec.coffee index 29396f67a..0782c25d7 100644 --- a/spec/root-view-spec.coffee +++ b/spec/root-view-spec.coffee @@ -99,10 +99,10 @@ describe "RootView", -> describe "when there is an active view", -> it "hands off focus to the active view", -> - editor = rootView.getActiveView() - editor.isFocused = false + editorView = rootView.getActiveView() + editorView.isFocused = false rootView.focus() - expect(editor.isFocused).toBeTruthy() + expect(editorView.isFocused).toBeTruthy() describe "when there is no active view", -> beforeEach -> @@ -163,9 +163,9 @@ describe "RootView", -> describe "when the title of the active pane item changes", -> it "updates the window title based on the item's new title", -> - editSession = rootView.getActivePaneItem() - editSession.buffer.setPath(path.join(temp.dir, 'hi')) - expect(rootView.title).toBe "#{editSession.getTitle()} - #{project.getPath()}" + editor = rootView.getActivePaneItem() + editor.buffer.setPath(path.join(temp.dir, 'hi')) + expect(rootView.title).toBe "#{editor.getTitle()} - #{project.getPath()}" describe "when the active pane's item changes", -> it "updates the title to the new item's title plus the project path", -> @@ -220,34 +220,34 @@ describe "RootView", -> describe "when called with no path", -> it "creates a empty edit session as an item on a new pane, and focuses the pane", -> - editSession = rootView.openSync() - expect(rootView.getActivePane().activeItem).toBe editSession - expect(editSession.getPath()).toBeUndefined() + editor = rootView.openSync() + expect(rootView.getActivePane().activeItem).toBe editor + expect(editor.getPath()).toBeUndefined() expect(rootView.getActivePane().focus).toHaveBeenCalled() it "can create multiple empty edit sessions as an item on a new pane", -> - editSession = rootView.openSync() - editSession2 = rootView.openSync() + editor = rootView.openSync() + editor2 = rootView.openSync() expect(rootView.getActivePane().getItems().length).toBe 2 - expect(editSession).not.toBe editSession2 + expect(editor).not.toBe editor2 describe "when called with a path", -> it "creates an edit session for the given path as an item on a new pane, and focuses the pane", -> - editSession = rootView.openSync('b') - expect(rootView.getActivePane().activeItem).toBe editSession - expect(editSession.getPath()).toBe require.resolve('./fixtures/dir/b') + editor = rootView.openSync('b') + expect(rootView.getActivePane().activeItem).toBe editor + expect(editor.getPath()).toBe require.resolve('./fixtures/dir/b') expect(rootView.getActivePane().focus).toHaveBeenCalled() describe "when the changeFocus option is false", -> it "does not focus the new pane", -> - editSession = rootView.openSync('b', changeFocus: false) + editor = rootView.openSync('b', changeFocus: false) expect(rootView.getActivePane().focus).not.toHaveBeenCalled() describe "when the split option is 'right'", -> it "creates a new pane and opens the file in said pane", -> - editSession = rootView.openSync('b', split: 'right') - expect(rootView.getActivePane().activeItem).toBe editSession - expect(editSession.getPath()).toBe require.resolve('./fixtures/dir/b') + editor = rootView.openSync('b', split: 'right') + expect(rootView.getActivePane().activeItem).toBe editor + expect(editor.getPath()).toBe require.resolve('./fixtures/dir/b') describe "when there is an active pane", -> [activePane, initialItemCount] = [] @@ -258,10 +258,10 @@ describe "RootView", -> describe "when called with no path", -> it "opens an edit session with an empty buffer as an item in the active pane and focuses it", -> - editSession = rootView.openSync() + editor = rootView.openSync() expect(activePane.getItems().length).toBe initialItemCount + 1 - expect(activePane.activeItem).toBe editSession - expect(editSession.getPath()).toBeUndefined() + expect(activePane.activeItem).toBe editor + expect(editor.getPath()).toBeUndefined() expect(activePane.focus).toHaveBeenCalled() describe "when called with a path", -> @@ -269,43 +269,43 @@ describe "RootView", -> it "shows the existing edit session in the pane", -> previousEditSession = activePane.activeItem - editSession = rootView.openSync('b') - expect(activePane.activeItem).toBe editSession - expect(editSession).not.toBe previousEditSession + editor = rootView.openSync('b') + expect(activePane.activeItem).toBe editor + expect(editor).not.toBe previousEditSession - editSession = rootView.openSync(previousEditSession.getPath()) - expect(editSession).toBe previousEditSession - expect(activePane.activeItem).toBe editSession + editor = rootView.openSync(previousEditSession.getPath()) + expect(editor).toBe previousEditSession + expect(activePane.activeItem).toBe editor expect(activePane.focus).toHaveBeenCalled() describe "when the active pane does not have an edit session item for the path being opened", -> it "creates a new edit session for the given path in the active editor", -> - editSession = rootView.openSync('b') + editor = rootView.openSync('b') expect(activePane.items.length).toBe 2 - expect(activePane.activeItem).toBe editSession + expect(activePane.activeItem).toBe editor expect(activePane.focus).toHaveBeenCalled() describe "when the changeFocus option is false", -> it "does not focus the active pane", -> - editSession = rootView.openSync('b', changeFocus: false) + editor = rootView.openSync('b', changeFocus: false) expect(activePane.focus).not.toHaveBeenCalled() describe "when the split option is 'right'", -> it "creates a new pane and opens the file in said pane", -> pane1 = rootView.getActivePane() - editSession = rootView.openSync('b', split: 'right') + editor = rootView.openSync('b', split: 'right') pane2 = rootView.getActivePane() expect(pane2[0]).not.toBe pane1[0] - expect(editSession.getPath()).toBe require.resolve('./fixtures/dir/b') + expect(editor.getPath()).toBe require.resolve('./fixtures/dir/b') expect(rootView.panes.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] - editSession = rootView.openSync('file1', split: 'right') + editor = rootView.openSync('file1', split: 'right') pane3 = rootView.getActivePane() expect(pane3[0]).toBe pane2[0] - expect(editSession.getPath()).toBe require.resolve('./fixtures/dir/file1') + expect(editor.getPath()).toBe require.resolve('./fixtures/dir/file1') expect(rootView.panes.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] @@ -372,41 +372,41 @@ describe "RootView", -> describe "when called with no path", -> it "creates a empty edit session as an item on a new pane, and focuses the pane", -> - editSession = null + editor = null waitsForPromise -> - rootView.open().then (o) -> editSession = o + rootView.open().then (o) -> editor = o runs -> - expect(rootView.getActivePane().activeItem).toBe editSession - expect(editSession.getPath()).toBeUndefined() + expect(rootView.getActivePane().activeItem).toBe editor + expect(editor.getPath()).toBeUndefined() expect(rootView.getActivePane().focus).toHaveBeenCalled() it "can create multiple empty edit sessions as items on a pane", -> - editSession1 = null - editSession2 = null + editor1 = null + editor2 = null waitsForPromise -> rootView.open() .then (o) -> - editSession1 = o + editor1 = o rootView.open() .then (o) -> - editSession2 = o + editor2 = o runs -> expect(rootView.getActivePane().getItems().length).toBe 2 - expect(editSession1).not.toBe editSession2 + expect(editor1).not.toBe editor2 describe "when called with a path", -> it "creates an edit session for the given path as an item on a new pane, and focuses the pane", -> - editSession = null + editor = null waitsForPromise -> - rootView.open('b').then (o) -> editSession = o + rootView.open('b').then (o) -> editor = o runs -> - expect(rootView.getActivePane().activeItem).toBe editSession - expect(editSession.getPath()).toBe require.resolve('./fixtures/dir/b') + expect(rootView.getActivePane().activeItem).toBe editor + expect(editor.getPath()).toBe require.resolve('./fixtures/dir/b') expect(rootView.getActivePane().focus).toHaveBeenCalled() describe "when there is an active pane", -> @@ -417,15 +417,15 @@ describe "RootView", -> describe "when called with no path", -> it "opens an edit session with an empty buffer as an item in the active pane and focuses it", -> - editSession = null + editor = null waitsForPromise -> - rootView.open().then (o) -> editSession = o + rootView.open().then (o) -> editor = o runs -> expect(activePane.getItems().length).toBe 2 - expect(activePane.activeItem).toBe editSession - expect(editSession.getPath()).toBeUndefined() + expect(activePane.activeItem).toBe editor + expect(editor.getPath()).toBeUndefined() expect(activePane.focus).toHaveBeenCalled() describe "when called with a path", -> @@ -433,31 +433,31 @@ describe "RootView", -> it "shows the existing edit session in the pane", -> previousEditSession = activePane.activeItem - editSession = null + editor = null waitsForPromise -> - rootView.open('b').then (o) -> editSession = o + rootView.open('b').then (o) -> editor = o runs -> - expect(activePane.activeItem).toBe editSession - expect(editSession).not.toBe previousEditSession + expect(activePane.activeItem).toBe editor + expect(editor).not.toBe previousEditSession waitsForPromise -> - rootView.open(previousEditSession.getPath()).then (o) -> editSession = o + rootView.open(previousEditSession.getPath()).then (o) -> editor = o runs -> - expect(editSession).toBe previousEditSession - expect(activePane.activeItem).toBe editSession + expect(editor).toBe previousEditSession + expect(activePane.activeItem).toBe editor expect(activePane.focus).toHaveBeenCalled() describe "when the active pane does not have an existing item for the given path", -> it "creates a new edit session for the given path in the active pane", -> - editSession = null + editor = null waitsForPromise -> - rootView.open('b').then (o) -> editSession = o + rootView.open('b').then (o) -> editor = o runs -> - expect(activePane.activeItem).toBe editSession + expect(activePane.activeItem).toBe editor expect(activePane.getItems().length).toBe 2 expect(activePane.focus).toHaveBeenCalled() diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index e5aa2c055..e5b5bb913 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -1,12 +1,12 @@ -EditSession = require '../src/edit-session' +Editor = require '../src/editor' describe "Selection", -> - [buffer, editSession, selection] = [] + [buffer, editor, selection] = [] beforeEach -> buffer = project.bufferForPathSync('sample.js') - editSession = new EditSession(buffer: buffer, tabLength: 2) - selection = editSession.getSelection() + editor = new Editor(buffer: buffer, tabLength: 2) + selection = editor.getSelection() afterEach -> buffer.destroy() diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 0c07dd1af..4bab88b58 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -9,7 +9,7 @@ Keymap = require '../src/keymap' Config = require '../src/config' {Point} = require 'telepath' Project = require '../src/project' -Editor = require '../src/editor' +EditorView = require '../src/editor-view' TokenizedBuffer = require '../src/tokenized-buffer' pathwatcher = require 'pathwatcher' platform = require './spec-helper-platform' @@ -75,7 +75,7 @@ beforeEach -> spyOn(config, 'load') spyOn(config, 'save') config.setDefaults('core', RootView.configDefaults) - config.setDefaults('editor', Editor.configDefaults) + config.setDefaults('editor', EditorView.configDefaults) config.set "editor.fontFamily", "Courier" config.set "editor.fontSize", 16 config.set "editor.autoIndent", false @@ -86,7 +86,7 @@ beforeEach -> window.config = config # make editor display updates synchronous - spyOn(Editor.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay() + spyOn(EditorView.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay() spyOn(RootView.prototype, 'setTitle').andCallFake (@title) -> spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout @@ -174,8 +174,8 @@ window.keydownEvent = (key, properties={}) -> window.mouseEvent = (type, properties) -> if properties.point - {point, editor} = properties - {top, left} = @pagePixelPositionForPoint(editor, point) + {point, editorView} = properties + {top, left} = @pagePixelPositionForPoint(editorView, point) properties.pageX = left + 1 properties.pageY = top + 1 properties.originalEvent ?= {detail: 1} @@ -236,22 +236,22 @@ window.advanceClock = (delta=1) -> callback() for callback in callbacks -window.pagePixelPositionForPoint = (editor, point) -> +window.pagePixelPositionForPoint = (editorView, point) -> point = Point.fromObject point - top = editor.renderedLines.offset().top + point.row * editor.lineHeight - left = editor.renderedLines.offset().left + point.column * editor.charWidth - editor.renderedLines.scrollLeft() + top = editorView.renderedLines.offset().top + point.row * editorView.lineHeight + left = editorView.renderedLines.offset().left + point.column * editorView.charWidth - editorView.renderedLines.scrollLeft() { top, left } window.tokensText = (tokens) -> _.pluck(tokens, 'value').join('') -window.setEditorWidthInChars = (editor, widthInChars, charWidth=editor.charWidth) -> - editor.width(charWidth * widthInChars + editor.gutter.outerWidth()) - $(window).trigger 'resize' # update width of editor's on-screen lines +window.setEditorWidthInChars = (editorView, widthInChars, charWidth=editorView.charWidth) -> + editorView.width(charWidth * widthInChars + editorView.gutter.outerWidth()) + $(window).trigger 'resize' # update width of editor view's on-screen lines -window.setEditorHeightInLines = (editor, heightInChars, charHeight=editor.lineHeight) -> - editor.height(charHeight * heightInChars + editor.renderedLines.position().top) - $(window).trigger 'resize' # update editor's on-screen lines +window.setEditorHeightInLines = (editorView, heightInChars, charHeight=editorView.lineHeight) -> + editorView.height(charHeight * heightInChars + editorView.renderedLines.position().top) + $(window).trigger 'resize' # update editor view's on-screen lines $.fn.resultOfTrigger = (type) -> event = $.Event(type) diff --git a/spec/text-mate-grammar-spec.coffee b/spec/text-mate-grammar-spec.coffee index 00198afff..6d539f29d 100644 --- a/spec/text-mate-grammar-spec.coffee +++ b/spec/text-mate-grammar-spec.coffee @@ -447,25 +447,25 @@ describe "TextMateGrammar", -> describe "when the grammar is added", -> it "retokenizes existing buffers that contain tokens that match the injection selector", -> - editSession = project.openSync('sample.js') - editSession.setText("// http://github.com") + editor = project.openSync('sample.js') + editor.setText("// http://github.com") - {tokens} = editSession.lineForScreenRow(0) + {tokens} = editor.lineForScreenRow(0) expect(tokens[1].value).toBe " http://github.com" expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"] atom.activatePackage('language-hyperlink', sync: true) - {tokens} = editSession.lineForScreenRow(0) + {tokens} = editor.lineForScreenRow(0) expect(tokens[2].value).toBe "http://github.com" expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] describe "when the grammar is updated", -> it "retokenizes existing buffers that contain tokens that match the injection selector", -> - editSession = project.openSync('sample.js') - editSession.setText("// SELECT * FROM OCTOCATS") + editor = project.openSync('sample.js') + editor.setText("// SELECT * FROM OCTOCATS") - {tokens} = editSession.lineForScreenRow(0) + {tokens} = editor.lineForScreenRow(0) expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS" expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"] @@ -477,13 +477,13 @@ describe "TextMateGrammar", -> patterns: [ { include: "source.sql" } ] )) - {tokens} = editSession.lineForScreenRow(0) + {tokens} = editor.lineForScreenRow(0) expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS" expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"] atom.activatePackage('language-sql', sync: true) - {tokens} = editSession.lineForScreenRow(0) + {tokens} = editor.lineForScreenRow(0) expect(tokens[2].value).toBe "SELECT" expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "keyword.other.DML.sql"] diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index f5b278729..b053ec8ed 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -61,23 +61,23 @@ describe "Window", -> it "prompts user to save and and calls rootView.confirmClose", -> spyOn(rootView, 'confirmClose').andCallThrough() spyOn(atom, "confirmSync").andReturn(2) - editSession = rootView.openSync("sample.js") - editSession.insertText("I look different, I feel different.") + editor = rootView.openSync("sample.js") + editor.insertText("I look different, I feel different.") $(window).trigger(beforeUnloadEvent) expect(rootView.confirmClose).toHaveBeenCalled() expect(atom.confirmSync).toHaveBeenCalled() it "prompts user to save and handler returns true if don't save", -> spyOn(atom, "confirmSync").andReturn(2) - editSession = rootView.openSync("sample.js") - editSession.insertText("I look different, I feel different.") + editor = rootView.openSync("sample.js") + editor.insertText("I look different, I feel different.") $(window).trigger(beforeUnloadEvent) expect(atom.confirmSync).toHaveBeenCalled() it "prompts user to save and handler returns false if dialog is canceled", -> spyOn(atom, "confirmSync").andReturn(1) - editSession = rootView.openSync("sample.js") - editSession.insertText("I look different, I feel different.") + editor = rootView.openSync("sample.js") + editor.insertText("I look different, I feel different.") $(window).trigger(beforeUnloadEvent) expect(atom.confirmSync).toHaveBeenCalled() diff --git a/src/cursor-view.coffee b/src/cursor-view.coffee index 5276965b5..1af9e2204 100644 --- a/src/cursor-view.coffee +++ b/src/cursor-view.coffee @@ -9,14 +9,14 @@ class CursorView extends View @div class: 'cursor idle', => @raw ' ' blinkPeriod: 800 - editor: null + editorView: null visible: true needsUpdate: true needsRemoval: false shouldPauseBlinking: false - initialize: (@cursor, @editor) -> + initialize: (@cursor, @editorView) -> @cursor.on 'moved.cursor-view', => @needsUpdate = true @shouldPauseBlinking = true @@ -25,7 +25,7 @@ class CursorView extends View @needsUpdate = true @cursor.on 'autoscrolled.cursor-view', => - @editor.requestDisplayUpdate() + @editorView.requestDisplayUpdate() @cursor.on 'destroyed.cursor-view', => @needsRemoval = true @@ -34,7 +34,7 @@ class CursorView extends View @addClass("site-#{@cursor.marker.getOriginSiteId()}") beforeRemove: -> - @editor.removeCursorView(this) + @editorView.removeCursorView(this) @cursor.off('.cursor-view') @stopBlinking() @@ -52,7 +52,7 @@ class CursorView extends View else if !@startBlinkingTimeout @startBlinking() - @setVisible(@cursor.isVisible() and not @editor.isFoldedAtScreenRow(screenPosition.row)) + @setVisible(@cursor.isVisible() and not @editorView.isFoldedAtScreenRow(screenPosition.row)) # Override for speed. The base function checks the computedStyle isHidden: -> @@ -69,7 +69,7 @@ class CursorView extends View @cursor.clearAutoscroll() getPixelPosition: -> - @editor.pixelPositionForScreenPosition(@getScreenPosition()) + @editorView.pixelPositionForScreenPosition(@getScreenPosition()) setVisible: (visible) -> unless @visible == visible diff --git a/src/cursor.coffee b/src/cursor.coffee index 551edef0e..a78d6f5e1 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -5,7 +5,7 @@ _ = require 'underscore-plus' # Public: The `Cursor` class represents the little blinking line identifying # where text can be inserted. # -# Cursors belong to {EditSession}s and have some metadata attached in the form +# Cursors belong to {Editor}s and have some metadata attached in the form # of a {StringMarker}. module.exports = class Cursor @@ -17,8 +17,8 @@ class Cursor visible: true needsAutoscroll: null - # Private: Instantiated by an {EditSession} - constructor: ({@editSession, @marker}) -> + # Private: Instantiated by an {Editor} + constructor: ({@editor, @marker}) -> @updateVisibility() @marker.on 'changed', (e) => @updateVisibility() @@ -37,10 +37,10 @@ class Cursor textChanged: textChanged @emit 'moved', movedEvent - @editSession.emit 'cursor-moved', movedEvent + @editor.emit 'cursor-moved', movedEvent @marker.on 'destroyed', => @destroyed = true - @editSession.removeCursor(this) + @editor.removeCursor(this) @emit 'destroyed' @needsAutoscroll = true @@ -62,7 +62,7 @@ class Cursor # An {Array} of two numbers: the screen row, and the screen column. # * options: # + autoscroll: - # A Boolean which, if `true`, scrolls the {EditSession} to wherever the + # A Boolean which, if `true`, scrolls the {Editor} to wherever the # cursor moves to. setScreenPosition: (screenPosition, options={}) -> @changePosition options, => @@ -78,7 +78,7 @@ class Cursor # An {Array} of two numbers: the buffer row, and the buffer column. # * options: # + autoscroll: - # A Boolean which, if `true`, scrolls the {EditSession} to wherever the + # A Boolean which, if `true`, scrolls the {Editor} to wherever the # cursor moves to. setBufferPosition: (bufferPosition, options={}) -> @changePosition options, => @@ -118,13 +118,13 @@ class Cursor segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+") new RegExp(segments.join("|"), "g") - # Public: Identifies if this cursor is the last in the {EditSession}. + # Public: Identifies if this cursor is the last in the {Editor}. # # "Last" is defined as the most recently added cursor. # # Returns a Boolean. isLastCursor: -> - this == @editSession.getCursor() + this == @editor.getCursor() # Public: Identifies if the cursor is surrounded by whitespace. # @@ -135,7 +135,7 @@ class Cursor isSurroundedByWhitespace: -> {row, column} = @getBufferPosition() range = [[row, Math.min(0, column - 1)], [row, Math.max(0, column + 1)]] - /^\s+$/.test @editSession.getTextInBufferRange(range) + /^\s+$/.test @editor.getTextInBufferRange(range) # Public: Returns whether the cursor is currently between a word and non-word # character. The non-word characters are defined by the @@ -150,7 +150,7 @@ class Cursor {row, column} = @getBufferPosition() range = [[row, column - 1], [row, column + 1]] - [before, after] = @editSession.getTextInBufferRange(range) + [before, after] = @editor.getTextInBufferRange(range) return false if /\s/.test(before) or /\s/.test(after) nonWordCharacters = atom.config.get('editor.nonWordCharacters').split('') @@ -160,7 +160,7 @@ class Cursor isInsideWord: -> {row, column} = @getBufferPosition() range = [[row, column], [row, Infinity]] - @editSession.getTextInBufferRange(range).search(@wordRegExp()) == 0 + @editor.getTextInBufferRange(range).search(@wordRegExp()) == 0 # Public: Prevents this cursor from causing scrolling. clearAutoscroll: -> @@ -189,7 +189,7 @@ class Cursor # Public: Returns the cursor's current buffer row of text excluding its line # ending. getCurrentBufferLine: -> - @editSession.lineForBufferRow(@getBufferRow()) + @editor.lineForBufferRow(@getBufferRow()) # Public: Moves the cursor up one screen row. moveUp: (rowCount = 1, {moveToEndOfSelection}={}) -> @@ -248,7 +248,7 @@ class Cursor # Public: Moves the cursor to the bottom of the buffer. moveToBottom: -> - @setBufferPosition(@editSession.getEofBufferPosition()) + @setBufferPosition(@editor.getEofBufferPosition()) # Public: Moves the cursor to the beginning of the screen line. moveToBeginningOfLine: -> @@ -258,7 +258,7 @@ class Cursor # line. moveToFirstCharacterOfLine: -> {row, column} = @getScreenPosition() - screenline = @editSession.lineForScreenRow(row) + screenline = @editor.lineForScreenRow(row) goalColumn = screenline.text.search(/\S/) return if goalColumn == -1 @@ -272,7 +272,7 @@ class Cursor position = @getBufferPosition() scanRange = @getCurrentLineBufferRange() endOfLeadingWhitespace = null - @editSession.scanInBufferRange /^[ \t]*/, scanRange, ({range}) => + @editor.scanInBufferRange /^[ \t]*/, scanRange, ({range}) => endOfLeadingWhitespace = range.end @setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position) @@ -318,11 +318,11 @@ class Cursor getBeginningOfCurrentWordBufferPosition: (options = {}) -> allowPrevious = options.allowPrevious ? true currentBufferPosition = @getBufferPosition() - previousNonBlankRow = @editSession.buffer.previousNonBlankRow(currentBufferPosition.row) + previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) scanRange = [[previousNonBlankRow, 0], currentBufferPosition] beginningOfWordPosition = null - @editSession.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) => + @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) => if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious beginningOfWordPosition = range.start if not beginningOfWordPosition?.isEqual(currentBufferPosition) @@ -334,11 +334,11 @@ class Cursor # the current word, or the previous word. getPreviousWordBoundaryBufferPosition: (options = {}) -> currentBufferPosition = @getBufferPosition() - previousNonBlankRow = @editSession.buffer.previousNonBlankRow(currentBufferPosition.row) + previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) scanRange = [[previousNonBlankRow, 0], currentBufferPosition] beginningOfWordPosition = null - @editSession.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) => + @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) => if range.start.row < currentBufferPosition.row and currentBufferPosition.column > 0 # force it to stop at the beginning of each line beginningOfWordPosition = new Point(currentBufferPosition.row, 0) @@ -356,10 +356,10 @@ class Cursor # the current word, or the previous word. getMoveNextWordBoundaryBufferPosition: (options = {}) -> currentBufferPosition = @getBufferPosition() - scanRange = [currentBufferPosition, @editSession.getEofBufferPosition()] + scanRange = [currentBufferPosition, @editor.getEofBufferPosition()] endOfWordPosition = null - @editSession.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) => + @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) => if range.start.row > currentBufferPosition.row # force it to stop at the beginning of each line endOfWordPosition = new Point(range.start.row, 0) @@ -386,10 +386,10 @@ class Cursor getEndOfCurrentWordBufferPosition: (options = {}) -> allowNext = options.allowNext ? true currentBufferPosition = @getBufferPosition() - scanRange = [currentBufferPosition, @editSession.getEofBufferPosition()] + scanRange = [currentBufferPosition, @editor.getEofBufferPosition()] endOfWordPosition = null - @editSession.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) => + @editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) => if range.start.isLessThanOrEqual(currentBufferPosition) or allowNext endOfWordPosition = range.end if not endOfWordPosition?.isEqual(currentBufferPosition) @@ -407,10 +407,10 @@ class Cursor getBeginningOfNextWordBufferPosition: (options = {}) -> currentBufferPosition = @getBufferPosition() start = if @isInsideWord() then @getEndOfCurrentWordBufferPosition() else currentBufferPosition - scanRange = [start, @editSession.getEofBufferPosition()] + scanRange = [start, @editor.getEofBufferPosition()] beginningOfNextWordPosition = null - @editSession.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) => + @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) => beginningOfNextWordPosition = range.start stop() @@ -432,7 +432,7 @@ class Cursor # + includeNewline: # A boolean which controls whether the Range should include the newline. getCurrentLineBufferRange: (options) -> - @editSession.bufferRangeForBufferRow(@getBufferRow(), options) + @editor.bufferRangeForBufferRow(@getBufferRow(), options) # Public: Retrieves the range for the current paragraph. # @@ -440,11 +440,11 @@ class Cursor # # Returns a {Range}. getCurrentParagraphBufferRange: -> - @editSession.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow()) + @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow()) # Public: Returns the characters preceeding the cursor in the current word. getCurrentWordPrefix: -> - @editSession.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) + @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) # Public: Returns whether the cursor is at the start of a line. isAtBeginningOfLine: -> @@ -452,8 +452,8 @@ class Cursor # Public: Returns the indentation level of the current line. getIndentLevel: -> - if @editSession.getSoftTabs() - @getBufferColumn() / @editSession.getTabLength() + if @editor.getSoftTabs() + @getBufferColumn() / @editor.getTabLength() else @getBufferColumn() @@ -465,13 +465,13 @@ class Cursor # # Returns an {Array} of {String}s. getScopes: -> - @editSession.scopesForBufferPosition(@getBufferPosition()) + @editor.scopesForBufferPosition(@getBufferPosition()) # Public: Returns true if this cursor has no non-whitespace characters before # its current position. hasPrecedingCharactersOnLine: -> bufferPosition = @getBufferPosition() - line = @editSession.lineForBufferRow(bufferPosition.row) + line = @editor.lineForBufferRow(bufferPosition.row) firstCharacterColumn = line.search(/\S/) if firstCharacterColumn is -1 diff --git a/src/edit-session.coffee b/src/edit-session.coffee deleted file mode 100644 index b12628ba0..000000000 --- a/src/edit-session.coffee +++ /dev/null @@ -1,1445 +0,0 @@ -_ = require 'underscore-plus' -path = require 'path' -telepath = require 'telepath' -guid = require 'guid' -{Point, Range} = telepath -LanguageMode = require './language-mode' -DisplayBuffer = require './display-buffer' -Cursor = require './cursor' -Selection = require './selection' -{Emitter, Subscriber} = require 'emissary' -TextMateScopeSelector = require('first-mate').ScopeSelector - -# Public: The core model of Atom. -# -# An {EditSession} represents a unique view of each document, with it's own -# {Cursor}s and scroll position. -# -# For instance if a user creates a split, Atom creates a second {EditSession} -# but both {EditSession}s interact with the same buffer underlying buffer. So -# if you type in either buffer it immediately appears in both but if you scroll -# in one it doesn't scroll the other. -# -# Almost all extension will interact primiarily with this class as it provides -# access to objects you'll most commonly interact with. To access it you'll -# want to register a callback on {RootView} which will be fired once for every -# existing {EditSession} as well as any future {EditSession}s. -# -# ## Example -# ```coffeescript -# global.rootView.eachEditSession (editSession) -> -# editSession.insertText('Hello World') -# ``` -# -# ## Collaboration builtin -# -# FIXME: Describe how there are both local and remote cursors and selections and -# why that is. -module.exports = -class EditSession - Emitter.includeInto(this) - Subscriber.includeInto(this) - - @acceptsDocuments: true - - registerDeserializer(this) - - @version: 5 - - @deserialize: (state) -> - new EditSession(state) - - id: null - languageMode: null - displayBuffer: null - cursors: null - remoteCursors: null - selections: null - remoteSelections: null - suppressSelectionMerging: false - - # Private: - constructor: (optionsOrState) -> - @cursors = [] - @remoteCursors = [] - @selections = [] - @remoteSelections = [] - if optionsOrState instanceof telepath.Document - @state = optionsOrState - @id = @state.get('id') - displayBuffer = deserialize(@state.get('displayBuffer')) - @setBuffer(displayBuffer.buffer) - @setDisplayBuffer(displayBuffer) - for marker in @findMarkers(@getSelectionMarkerAttributes()) - marker.setAttributes(preserveFolds: true) - @addSelection(marker) - @setScrollTop(@state.get('scrollTop')) - @setScrollLeft(@state.get('scrollLeft')) - registerEditSession = true - else - {buffer, displayBuffer, tabLength, softTabs, softWrap, suppressCursorCreation, initialLine} = optionsOrState - @id = guid.create().toString() - displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrap}) - @state = atom.site.createDocument - deserializer: @constructor.name - version: @constructor.version - id: @id - displayBuffer: displayBuffer.getState() - softTabs: buffer.usesSoftTabs() ? softTabs ? atom.config.get('editor.softTabs') ? true - scrollTop: 0 - scrollLeft: 0 - @setBuffer(buffer) - @setDisplayBuffer(displayBuffer) - - if @getCursors().length is 0 and not suppressCursorCreation - if initialLine - position = [initialLine, 0] - else - position = _.last(@getRemoteCursors())?.getBufferPosition() ? [0, 0] - @addCursorAtBufferPosition(position) - - @languageMode = new LanguageMode(this, @buffer.getExtension()) - @subscribe @state, 'changed', ({newValues}) => - for key, newValue of newValues - switch key - when 'scrollTop' - @emit 'scroll-top-changed', newValue - when 'scrollLeft' - @emit 'scroll-left-changed', newValue - - project.addEditSession(this) if registerEditSession - - # Private: - setBuffer: (@buffer) -> - @buffer.retain() - @subscribe @buffer, "path-changed", => - project.setPath(path.dirname(@getPath())) unless project.getPath()? - @emit "title-changed" - @emit "path-changed" - @subscribe @buffer, "contents-modified", => @emit "contents-modified" - @subscribe @buffer, "contents-conflicted", => @emit "contents-conflicted" - @subscribe @buffer, "modified-status-changed", => @emit "modified-status-changed" - @preserveCursorPositionOnBufferReload() - - # Private: - setDisplayBuffer: (@displayBuffer) -> - @subscribe @displayBuffer, 'marker-created', @handleMarkerCreated - @subscribe @displayBuffer, "changed", (e) => @emit 'screen-lines-changed', e - @subscribe @displayBuffer, "markers-updated", => @mergeIntersectingSelections() - @subscribe @displayBuffer, 'grammar-changed', => @handleGrammarChange() - @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... - - # Private: - getViewClass: -> - require './editor' - - # Private: - destroy: -> - return if @destroyed - @destroyed = true - @unsubscribe() - selection.destroy() for selection in @getSelections() - @buffer.release() - @displayBuffer.destroy() - @languageMode.destroy() - project?.removeEditSession(this) - @emit 'destroyed' - @off() - - # Private: - serialize: -> @state.clone() - - # Private: - getState: -> @state - - # Private: Creates an {EditSession} with the same initial state - copy: -> - tabLength = @getTabLength() - displayBuffer = @displayBuffer.copy() - softTabs = @getSoftTabs() - newEditSession = new EditSession({@buffer, displayBuffer, tabLength, softTabs, suppressCursorCreation: true}) - newEditSession.setScrollTop(@getScrollTop()) - newEditSession.setScrollLeft(@getScrollLeft()) - for marker in @findMarkers(editSessionId: @id) - marker.copy(editSessionId: newEditSession.id, preserveFolds: true) - project.addEditSession(newEditSession) - newEditSession - - # Public: Retrieves the filename of the open file. - # - # This is `'untitled'` if the file is new and not saved to the disk. - # - # Returns a {String}. - getTitle: -> - if sessionPath = @getPath() - path.basename(sessionPath) - else - 'untitled' - - # Public: Retrieves the filename and path of the open file. - # - # It has the follows the following format, ` - `. If the - # file is brand new, the title is `untitled`. - # - # Returns a {String}. - getLongTitle: -> - if sessionPath = @getPath() - fileName = path.basename(sessionPath) - directory = path.basename(path.dirname(sessionPath)) - "#{fileName} - #{directory}" - else - 'untitled' - - # Public: Compares two `EditSession`s to determine equality. - # - # Equality is based on the condition that: - # - # * the two {TextBuffer}s are the same - # * the two `scrollTop` and `scrollLeft` property are the same - # * the two {Cursor} screen positions are the same - # - # Returns a {Boolean}. - isEqual: (other) -> - return false unless other instanceof EditSession - @buffer == other.buffer and - @getScrollTop() == other.getScrollTop() and - @getScrollLeft() == other.getScrollLeft() and - @getCursorScreenPosition().isEqual(other.getCursorScreenPosition()) - - # Public: Controls visiblity based on the given Boolean. - setVisible: (visible) -> @displayBuffer.setVisible(visible) - - # Public: FIXME: I don't understand this. - setScrollTop: (scrollTop) -> @state.set('scrollTop', scrollTop) - - # Public: Returns the current `scrollTop` value - getScrollTop: -> @state.get('scrollTop') ? 0 - - # Public: FIXME: I don't understand this. - setScrollLeft: (scrollLeft) -> @state.set('scrollLeft', scrollLeft) - - # Public: Returns the current `scrollLeft` value - getScrollLeft: -> @state.get('scrollLeft') - - # Set the number of characters that can be displayed horizontally in the - # editor that contains this edit session. - # - # editorWidthInChars - A {Number} of characters - setEditorWidthInChars: (editorWidthInChars) -> - @displayBuffer.setEditorWidthInChars(editorWidthInChars) - - # Public: Sets the column at which columsn will soft wrap - getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() - - # Public: Returns whether soft tabs are enabled or not. - getSoftTabs: -> @state.get('softTabs') - - # Public: Controls whether soft tabs are enabled or not. - setSoftTabs: (softTabs) -> - @state.set('softTabs', softTabs) - - # Public: Returns whether soft wrap is enabled or not. - getSoftWrap: -> @displayBuffer.getSoftWrap() - - # Public: Controls whether soft tabs are enabled or not. - setSoftWrap: (softWrap) -> @displayBuffer.setSoftWrap(softWrap) - - # Public: Returns that String used to indicate a tab. - # - # If soft tabs are enabled, this is a space (`" "`) times the {.getTabLength} value. - # Otherwise, it's a tab (`\t`). - getTabText: -> @buildIndentString(1) - - # Public: Returns the current tab length. - getTabLength: -> @displayBuffer.getTabLength() - - # Public: Sets the current tab length. - setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) - - # Public: Given a position, this clips it to a real position. - # - # For example, if `position`'s row exceeds the row count of the buffer, - # or if its column goes beyond a line's length, this "sanitizes" the value - # to a real position. - # - # * position: - # The {Point} to clip - # - # Returns the new, clipped {Point}. Note that this could be the same as - # `position` if no clipping was performed. - clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) - - # Public: Given a range, this clips it to a real range. - # - # For example, if `range`'s row exceeds the row count of the buffer, - # or if its column goes beyond a line's length, this "sanitizes" the value - # to a real range. - # - # * range: - # The {Range} to clip - # - # Returns the new, clipped {Range}. Note that this could be the same as - # `range` if no clipping was performed. - clipBufferRange: (range) -> @buffer.clipRange(range) - - # Public: Returns the indentation level of the given a buffer row - # - # * bufferRow: - # A Number indicating the buffer row. - indentationForBufferRow: (bufferRow) -> - @indentLevelForLine(@lineForBufferRow(bufferRow)) - - # Public: Sets the indentation level for the given buffer row. - # - # * bufferRow: - # A {Number} indicating the buffer row. - # * newLevel: - # A {Number} indicating the new indentation level. - setIndentationForBufferRow: (bufferRow, newLevel) -> - currentLevel = @indentationForBufferRow(bufferRow) - currentIndentString = @buildIndentString(currentLevel) - newIndentString = @buildIndentString(newLevel) - @buffer.change([[bufferRow, 0], [bufferRow, currentIndentString.length]], newIndentString) - - # Public: Returns the indentation level of the given line of text. - # - # * line: - # A {String} in the current buffer. - # - # Returns a {Number} or 0 if the text isn't found within the buffer. - indentLevelForLine: (line) -> - if match = line.match(/^[\t ]+/) - leadingWhitespace = match[0] - tabCount = leadingWhitespace.match(/\t/g)?.length ? 0 - spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0 - tabCount + (spaceCount / @getTabLength()) - else - 0 - - # Private: Constructs the string used for tabs. - buildIndentString: (number) -> - if @getSoftTabs() - _.multiplyString(" ", number * @getTabLength()) - else - _.multiplyString("\t", Math.floor(number)) - - # {Delegates to: TextBuffer.save} - save: -> @buffer.save() - - # {Delegates to: TextBuffer.saveAs} - saveAs: (path) -> @buffer.saveAs(path) - - # {Delegates to: TextBuffer.getExtension} - getFileExtension: -> @buffer.getExtension() - - # {Delegates to: TextBuffer.getPath} - getPath: -> @buffer.getPath() - - # {Delegates to: TextBuffer.getText} - getText: -> @buffer.getText() - - # {Delegates to: TextBuffer.setText} - setText: (text) -> @buffer.setText(text) - - # Private: Retrieves the current {TextBuffer}. - getBuffer: -> @buffer - - # Public: Retrieves the current buffer's URI. - getUri: -> @buffer.getUri() - - # {Delegates to: TextBuffer.isRowBlank} - isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) - - # Public: Determine if the given row is entirely a comment - isBufferRowCommented: (bufferRow) -> - if match = @lineForBufferRow(bufferRow).match(/\S/) - scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes - new TextMateScopeSelector('comment.*').matches(scopes) - - # {Delegates to: TextBuffer.nextNonBlankRow} - nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) - - # {Delegates to: TextBuffer.getEofPosition} - getEofBufferPosition: -> @buffer.getEofPosition() - - # {Delegates to: TextBuffer.getLastRow} - getLastBufferRow: -> @buffer.getLastRow() - - # {Delegates to: TextBuffer.rangeForRow} - bufferRangeForBufferRow: (row, options) -> @buffer.rangeForRow(row, options) - - # {Delegates to: TextBuffer.lineForRow} - lineForBufferRow: (row) -> @buffer.lineForRow(row) - - # {Delegates to: TextBuffer.lineLengthForRow} - lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row) - - # {Delegates to: TextBuffer.scan} - scan: (args...) -> @buffer.scan(args...) - - # {Delegates to: TextBuffer.scanInRange} - scanInBufferRange: (args...) -> @buffer.scanInRange(args...) - - # {Delegates to: TextBuffer.backwardsScanInRange} - backwardsScanInBufferRange: (args...) -> @buffer.backwardsScanInRange(args...) - - # {Delegates to: TextBuffer.isModified} - isModified: -> @buffer.isModified() - - # Public: Determines if the user should be prompted to save before closing. - shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() - - # {Delegates to: DisplayBuffer.screenPositionForBufferPosition} - screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options) - - # {Delegates to: DisplayBuffer.bufferPositionForScreenPosition} - bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options) - - # {Delegates to: DisplayBuffer.screenRangeForBufferRange} - screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange) - - # {Delegates to: DisplayBuffer.bufferRangeForScreenRange} - bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange) - - # {Delegates to: DisplayBuffer.clipScreenPosition} - clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options) - - # {Delegates to: DisplayBuffer.lineForRow} - lineForScreenRow: (row) -> @displayBuffer.lineForRow(row) - - # {Delegates to: DisplayBuffer.linesForRows} - linesForScreenRows: (start, end) -> @displayBuffer.linesForRows(start, end) - - # {Delegates to: DisplayBuffer.getLineCount} - getScreenLineCount: -> @displayBuffer.getLineCount() - - # {Delegates to: DisplayBuffer.getMaxLineLength} - getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength() - - # {Delegates to: DisplayBuffer.getLastRow} - getLastScreenRow: -> @displayBuffer.getLastRow() - - # {Delegates to: DisplayBuffer.bufferRowsForScreenRows} - bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow) - - # {Delegates to: DisplayBuffer.scopesForBufferPosition} - scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) - - # Public: ? - bufferRangeForScopeAtCursor: (selector) -> - @displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition()) - - # {Delegates to: DisplayBuffer.tokenForBufferPosition} - tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) - - # Public: Retrieves the grammar's token scopes for the line with the most - # recently added cursor. - # - # Returns an {Array} of {String}s. - getCursorScopes: -> @getCursor().getScopes() - - # Public: Inserts text at the current cursor positions - # - # * text: - # A String representing the text to insert. - # * options: - # + A set of options equivalent to {Selection.insertText} - insertText: (text, options={}) -> - options.autoIndentNewline ?= @shouldAutoIndent() - options.autoDecreaseIndent ?= @shouldAutoIndent() - @mutateSelectedText (selection) -> selection.insertText(text, options) - - # Public: Inserts a new line at the current cursor positions. - insertNewline: -> - @insertText('\n') - - # Public: Inserts a new line below the current cursor positions. - insertNewlineBelow: -> - @transact => - @moveCursorToEndOfLine() - @insertNewline() - - # Public: Inserts a new line above the current cursor positions. - insertNewlineAbove: -> - @transact => - onFirstLine = @getCursorBufferPosition().row is 0 - @moveCursorToBeginningOfLine() - @moveCursorLeft() - @insertNewline() - @moveCursorUp() if onFirstLine - - # Public: Indents the current line. - # - # * options - # + A set of options equivalent to {Selection.indent}. - indent: (options={})-> - options.autoIndent ?= @shouldAutoIndent() - @mutateSelectedText (selection) -> selection.indent(options) - - # Public: Removes the character found behind the current cursor position. - # - # FIXME: Does this remove content from all cursors or the last one? - backspace: -> - @mutateSelectedText (selection) -> selection.backspace() - - # Public: Removes all characters from the current cursor position until the - # beginging of the current word. - backspaceToBeginningOfWord: -> - @mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord() - - # Public: Removes all characters from the current cursor position to the start - # of the line. - backspaceToBeginningOfLine: -> - @mutateSelectedText (selection) -> selection.backspaceToBeginningOfLine() - - # Public: Removes the current selection or the next character after the - # cursor. - delete: -> - @mutateSelectedText (selection) -> selection.delete() - - # Public: Removes all characters from the cursor until the end of the current - # word. - deleteToEndOfWord: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfWord() - - # Public: Deletes the entire line. - deleteLine: -> - @mutateSelectedText (selection) -> selection.deleteLine() - - # Public: Indents the currently selected rows. - # - # FIXME: what does this do if no selection? - indentSelectedRows: -> - @mutateSelectedText (selection) -> selection.indentSelectedRows() - - # Public: Outdents the selected rows. - # - # FIXME: what does this do if no selection? - outdentSelectedRows: -> - @mutateSelectedText (selection) -> selection.outdentSelectedRows() - - # Public: Wraps the lines within a selection in comments. - # - # If the language doesn't have comments, nothing happens. - # - # Returns an {Array} of the commented {Ranges}. - toggleLineCommentsInSelection: -> - @mutateSelectedText (selection) -> selection.toggleLineComments() - - # Public: Indents selected lines based on grammar's suggested indent levels. - autoIndentSelectedRows: -> - @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() - - # Public: Converts all indents to the current {.getTabText} given a {Range}. - normalizeTabsInBufferRange: (bufferRange) -> - return unless @getSoftTabs() - @scanInBufferRange /\t/, bufferRange, ({replace}) => replace(@getTabText()) - - # Public: Copies and removes all characters from cursor to the end of the - # line. - cutToEndOfLine: -> - maintainPasteboard = false - @mutateSelectedText (selection) -> - selection.cutToEndOfLine(maintainPasteboard) - maintainPasteboard = true - - # Public: Cuts the selected text. - cutSelectedText: -> - maintainPasteboard = false - @mutateSelectedText (selection) -> - selection.cut(maintainPasteboard) - maintainPasteboard = true - - # Public: Copies the selected text. - copySelectedText: -> - maintainPasteboard = false - for selection in @getSelections() - selection.copy(maintainPasteboard) - maintainPasteboard = true - - # Public: Pastes the text in the clipboard. - # - # * options: - # + A set of options equivalent to {Selection.insertText}. - pasteText: (options={}) -> - [text, metadata] = atom.pasteboard.read() - - containsNewlines = text.indexOf('\n') isnt -1 - - if atom.config.get('editor.normalizeIndentOnPaste') and metadata - if !@getCursor().hasPrecedingCharactersOnLine() or containsNewlines - options.indentBasis ?= metadata.indentBasis - - @insertText(text, options) - - # Public: Undoes the last change. - undo: -> - @getCursor().needsAutoscroll = true - @buffer.undo(this) - - # Pulic: Redoes the last change. - redo: -> - @getCursor().needsAutoscroll = true - @buffer.redo(this) - - # Public: Folds all the rows. - foldAll: -> - @languageMode.foldAll() - - # Public: Unfolds all the rows. - unfoldAll: -> - @languageMode.unfoldAll() - - # Public: Creates a fold for each section at the given indent level. - foldAllAtIndentLevel: (indentLevel) -> - @languageMode.foldAllAtIndentLevel(indentLevel) - - # Public: Folds the current row. - foldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @foldBufferRow(bufferRow) - - # Public: Folds a give buffer row. - foldBufferRow: (bufferRow) -> - @languageMode.foldBufferRow(bufferRow) - - # Public: Unfolds the current row. - unfoldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @unfoldBufferRow(bufferRow) - - # Public: Unfolds a given a buffer row. - unfoldBufferRow: (bufferRow) -> - @languageMode.unfoldBufferRow(bufferRow) - - # Public: Folds all selections. - foldSelection: -> - selection.fold() for selection in @getSelections() - - # {Delegates to: DisplayBuffer.createFold} - createFold: (startRow, endRow) -> - @displayBuffer.createFold(startRow, endRow) - - # {Delegates to: DisplayBuffer.destroyFoldWithId} - destroyFoldWithId: (id) -> - @displayBuffer.destroyFoldWithId(id) - - # {Delegates to: DisplayBuffer.destroyFoldsContainingBufferRow} - destroyFoldsContainingBufferRow: (bufferRow) -> - @displayBuffer.destroyFoldsContainingBufferRow(bufferRow) - - # Public: Removes any {Fold}s found that intersect the given buffer row. - destroyFoldsIntersectingBufferRange: (bufferRange) -> - for row in [bufferRange.start.row..bufferRange.end.row] - @destroyFoldsContainingBufferRow(row) - - # Public: Returns whether the current row is folded. - isFoldedAtCursorRow: -> - @isFoldedAtScreenRow(@getCursorScreenRow()) - - # Public: Returns whether a given buffer row if folded - isFoldedAtBufferRow: (bufferRow) -> - @displayBuffer.isFoldedAtBufferRow(bufferRow) - - # Public: Returns whether a given screen row if folded - isFoldedAtScreenRow: (screenRow) -> - @displayBuffer.isFoldedAtScreenRow(screenRow) - - # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow} - largestFoldContainingBufferRow: (bufferRow) -> - @displayBuffer.largestFoldContainingBufferRow(bufferRow) - - # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow} - largestFoldStartingAtScreenRow: (screenRow) -> - @displayBuffer.largestFoldStartingAtScreenRow(screenRow) - - # Public: Suggests the indent for the given buffer row. - suggestedIndentForBufferRow: (bufferRow) -> - @languageMode.suggestedIndentForBufferRow(bufferRow) - - # Public: Indents all the rows between two buffer rows. - # - # * startRow: The row {Number} to start at (inclusive) - # * endRow: The row {Number} to end at (inclusive) - autoIndentBufferRows: (startRow, endRow) -> - @languageMode.autoIndentBufferRows(startRow, endRow) - - # Public: Indents the given buffer row to it's suggested level. - autoIndentBufferRow: (bufferRow) -> - @languageMode.autoIndentBufferRow(bufferRow) - - # Public: - # - # FIXME: What does this do? - autoDecreaseIndentForBufferRow: (bufferRow) -> - @languageMode.autoDecreaseIndentForBufferRow(bufferRow) - - # Public: Wraps the lines between two rows in comments. - # - # If the language doesn't have comments, nothing happens. - # - # startRow - The row {Number} to start at (inclusive) - # endRow - The row {Number} to end at (inclusive) - # - # Returns an {Array} of the commented {Ranges}. - toggleLineCommentsForBufferRows: (start, end) -> - @languageMode.toggleLineCommentsForBufferRows(start, end) - - # Public: Moves the selected line up one row. - moveLineUp: -> - selection = @getSelectedBufferRange() - return if selection.start.row is 0 - lastRow = @buffer.getLastRow() - return if selection.isEmpty() and selection.start.row is lastRow and @buffer.getLastLine() is '' - - @transact => - foldedRows = [] - rows = [selection.start.row..selection.end.row] - if selection.start.row isnt selection.end.row and selection.end.column is 0 - rows.pop() unless @isFoldedAtBufferRow(selection.end.row) - for row in rows - screenRow = @screenPositionForBufferPosition([row]).row - if @isFoldedAtScreenRow(screenRow) - bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) - startRow = bufferRange.start.row - endRow = bufferRange.end.row - 1 - foldedRows.push(endRow - 1) - else - startRow = row - endRow = row - - endPosition = Point.min([endRow + 1], @buffer.getEofPosition()) - lines = @buffer.getTextInRange([[startRow], endPosition]) - if endPosition.row is lastRow and endPosition.column > 0 and not @buffer.lineEndingForRow(endPosition.row) - lines = "#{lines}\n" - @buffer.deleteRows(startRow, endRow) - @buffer.insert([startRow - 1], lines) - - @foldBufferRow(foldedRow) for foldedRow in foldedRows - - @setSelectedBufferRange(selection.translate([-1]), preserveFolds: true) - - # Public: Moves the selected line down one row. - moveLineDown: -> - selection = @getSelectedBufferRange() - lastRow = @buffer.getLastRow() - return if selection.end.row is lastRow - return if selection.end.row is lastRow - 1 and @buffer.getLastLine() is '' - - @transact => - foldedRows = [] - rows = [selection.end.row..selection.start.row] - if selection.start.row isnt selection.end.row and selection.end.column is 0 - rows.shift() unless @isFoldedAtBufferRow(selection.end.row) - for row in rows - screenRow = @screenPositionForBufferPosition([row]).row - if @isFoldedAtScreenRow(screenRow) - bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) - startRow = bufferRange.start.row - endRow = bufferRange.end.row - 1 - foldedRows.push(endRow + 1) - else - startRow = row - endRow = row - - if endRow + 1 is lastRow - endPosition = [endRow, @buffer.lineLengthForRow(endRow)] - else - endPosition = [endRow + 1] - lines = @buffer.getTextInRange([[startRow], endPosition]) - @buffer.deleteRows(startRow, endRow) - insertPosition = Point.min([startRow + 1], @buffer.getEofPosition()) - if insertPosition.row is @buffer.getLastRow() and insertPosition.column > 0 - lines = "\n#{lines}" - @buffer.insert(insertPosition, lines) - - @foldBufferRow(foldedRow) for foldedRow in foldedRows - - @setSelectedBufferRange(selection.translate([1]), preserveFolds: true) - - # Public: Duplicates the current line. - # - # If more than one cursor is present, only the most recently added one is - # duplicated. - duplicateLine: -> - return unless @getSelection().isEmpty() - - @transact => - cursorPosition = @getCursorBufferPosition() - cursorRowFolded = @isFoldedAtCursorRow() - if cursorRowFolded - screenRow = @screenPositionForBufferPosition(cursorPosition).row - bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) - else - bufferRange = new Range([cursorPosition.row], [cursorPosition.row + 1]) - - insertPosition = new Point(bufferRange.end.row) - if insertPosition.row > @buffer.getLastRow() - @unfoldCurrentRow() if cursorRowFolded - @buffer.append("\n#{@getTextInBufferRange(bufferRange)}") - @foldCurrentRow() if cursorRowFolded - else - @buffer.insert(insertPosition, @getTextInBufferRange(bufferRange)) - - @setCursorScreenPosition(@getCursorScreenPosition().translate([1])) - @foldCurrentRow() if cursorRowFolded - - # Private: - mutateSelectedText: (fn) -> - @transact => fn(selection) for selection in @getSelections() - - # Private: - replaceSelectedText: (options={}, fn) -> - {selectWordIfEmpty} = options - @mutateSelectedText (selection) -> - range = selection.getBufferRange() - if selectWordIfEmpty and selection.isEmpty() - selection.selectWord() - text = selection.getText() - selection.deleteSelectedText() - selection.insertText(fn(text)) - selection.setBufferRange(range) - - # Public: Returns a valid {DisplayBufferMarker} object for the given id. - getMarker: (id) -> - @displayBuffer.getMarker(id) - - # Public: Returns all {DisplayBufferMarker}s. - getMarkers: -> - @displayBuffer.getMarkers() - - # Public: Returns all {DisplayBufferMarker}s that match all given attributes. - findMarkers: (attributes) -> - @displayBuffer.findMarkers(attributes) - - # {Delegates to: DisplayBuffer.markScreenRange} - markScreenRange: (args...) -> - @displayBuffer.markScreenRange(args...) - - # {Delegates to: DisplayBuffer.markBufferRange} - markBufferRange: (args...) -> - @displayBuffer.markBufferRange(args...) - - # {Delegates to: DisplayBuffer.markScreenPosition} - markScreenPosition: (args...) -> - @displayBuffer.markScreenPosition(args...) - - # {Delegates to: DisplayBuffer.markBufferPosition} - markBufferPosition: (args...) -> - @displayBuffer.markBufferPosition(args...) - - # {Delegates to: DisplayBuffer.destroyMarker} - destroyMarker: (args...) -> - @displayBuffer.destroyMarker(args...) - - # {Delegates to: DisplayBuffer.getMarkerCount} - getMarkerCount: -> - @buffer.getMarkerCount() - - # Public: Determines if there are multiple cursors. - hasMultipleCursors: -> - @getCursors().length > 1 - - # Public: Returns an Array of all {Cursor}s, including cursors representing - # remote users. - getAllCursors: -> - @getCursors().concat(@getRemoteCursors()) - - # Public: Returns an Array of all local {Cursor}s. - getCursors: -> new Array(@cursors...) - - # Public: Returns the most recently added {Cursor}. - getCursor: -> - _.last(@cursors) - - # Public: Returns an Array of all remove {Cursor}s. - getRemoteCursors: -> new Array(@remoteCursors...) - - # Public: Adds and returns a cursor at the given screen position. - addCursorAtScreenPosition: (screenPosition) -> - @markScreenPosition(screenPosition, @getSelectionMarkerAttributes()) - @getLastSelection().cursor - - # Public: Adds and returns a cursor at the given buffer position. - addCursorAtBufferPosition: (bufferPosition) -> - @markBufferPosition(bufferPosition, @getSelectionMarkerAttributes()) - @getLastSelection().cursor - - # Public: Adds and returns a cursor at the given {DisplayBufferMarker} - # position. - addCursor: (marker) -> - cursor = new Cursor(editSession: this, marker: marker) - if marker.isLocal() - @cursors.push(cursor) - else - @remoteCursors.push(cursor) - @emit 'cursor-added', cursor - cursor - - # Public: Removes and returns a cursor from the `EditSession`. - removeCursor: (cursor) -> - _.remove(@cursors, cursor) - - # Public: Creates a new selection at the given marker. - # - # * marker: - # The {DisplayBufferMarker} to highlight - # * options: - # + A hash of options that pertain to the {Selection} constructor. - # - # Returns the new {Selection}. - addSelection: (marker, options={}) -> - unless marker.getAttributes().preserveFolds - @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) - cursor = @addCursor(marker) - selection = new Selection(_.extend({editSession: this, marker, cursor}, options)) - - if marker.isLocal() - @selections.push(selection) - else - @remoteSelections.push(selection) - - selectionBufferRange = selection.getBufferRange() - @mergeIntersectingSelections() - if selection.destroyed - for selection in @getSelections() - if selection.intersectsBufferRange(selectionBufferRange) - return selection - else - @emit 'selection-added', selection - selection - - # Public: Given a buffer range, this adds a new selection for it. - # - # * bufferRange: - # A {Range} in the buffer - # * options: - # + A hash of options for {.markBufferRange} - # - # Returns the new {Selection}. - addSelectionForBufferRange: (bufferRange, options={}) -> - @markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options)) - @getLastSelection() - - # Public: Given a buffer range, this removes all previous selections and - # creates a new selection for it. - # - # * bufferRange: - # A {Range} in the buffer - # * options: - # + A hash of options for {.setSelectedBufferRanges} - setSelectedBufferRange: (bufferRange, options) -> - @setSelectedBufferRanges([bufferRange], options) - - # Public: Given an array of buffer ranges, this removes all previous - # selections and creates new selections for them. - # - # * bufferRange: - # A {Range} in the buffer - # * options: - # + A hash of options for {.setSelectedBufferRanges} - setSelectedBufferRanges: (bufferRanges, options={}) -> - throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length - - selections = @getSelections() - selection.destroy() for selection in selections[bufferRanges.length...] - - @mergeIntersectingSelections options, => - for bufferRange, i in bufferRanges - bufferRange = Range.fromObject(bufferRange) - if selections[i] - selections[i].setBufferRange(bufferRange, options) - else - @addSelectionForBufferRange(bufferRange, options) - - # Public: Unselects a given selection. - # - # * selection - The {Selection} to remove. - removeSelection: (selection) -> - if selection.isLocal() - _.remove(@selections, selection) - else - _.remove(@remoteSelections, selection) - - # Public: Clears every selection. - # - # TODO: Is this still to be done? - clearSelections: -> - @consolidateSelections() - @getSelection().clear() - - # Public: - # - # FIXME: What does this do? - consolidateSelections: -> - selections = @getSelections() - if selections.length > 1 - selection.destroy() for selection in selections[0...-1] - true - else - false - - # Public: Returns all selections, including remote selections. - getAllSelections: -> - @getSelections().concat(@getRemoteSelections()) - - # Public: Gets all local selections. - # - # Returns an {Array} of {Selection}s. - getSelections: -> new Array(@selections...) - - # Public: Returns the selection at the specified index. - getSelection: (index) -> - index ?= @selections.length - 1 - @selections[index] - - # Public: Returns the most recently added {Selection} - getLastSelection: -> - _.last(@selections) - - # Public: Returns all remote selections. - getRemoteSelections: -> new Array(@remoteSelections...) - - # Public: Gets all local selections, ordered by their position in the buffer. - # - # Returns an {Array} of {Selection}s. - getSelectionsOrderedByBufferPosition: -> - @getSelections().sort (a, b) -> a.compare(b) - - # Public: Gets all remote selections, ordered by their position in the buffer. - # - # Returns an {Array} of {Selection}s. - getRemoteSelectionsOrderedByBufferPosition: -> - @getRemoteSelections().sort (a, b) -> a.compare(b) - - # Public: Gets the very last local selection in the buffer. - # - # Returns a {Selection}. - getLastSelectionInBuffer: -> - _.last(@getSelectionsOrderedByBufferPosition()) - - # Public: Determines if a given buffer range is included in a {Selection}. - # - # * bufferRange: - # The {Range} you're checking against - # - # Returns a {Boolean}. - selectionIntersectsBufferRange: (bufferRange) -> - _.any @getSelections(), (selection) -> - selection.intersectsBufferRange(bufferRange) - - # Public: Moves every local cursor to a given screen position. - # - # * position: - # An {Array} of two numbers: the screen row, and the screen column. - # * options: - # An object with properties based on {Cursor.setScreenPosition} - setCursorScreenPosition: (position, options) -> - @moveCursors (cursor) -> cursor.setScreenPosition(position, options) - - # Public: Gets the current screen position of the most recently added - # local {Cursor}. - # - # Returns an {Array} of two numbers: the screen row, and the screen column. - getCursorScreenPosition: -> - @getCursor().getScreenPosition() - - # Public: Gets the screen row of the most recently added local {Cursor}. - # - # Returns the screen row {Number}. - getCursorScreenRow: -> - @getCursor().getScreenRow() - - # Public: Moves every cursor to a given buffer position. - # - # * position: - # An {Array} of two numbers: the buffer row, and the buffer column. - # * options: - # + An object with properties based on {Cursor.setBufferPosition} - setCursorBufferPosition: (position, options) -> - @moveCursors (cursor) -> cursor.setBufferPosition(position, options) - - # Public: Gets the current buffer position of the most recently added {Cursor}. - # - # Returns an {Array} of two numbers: the buffer row, and the buffer column. - getCursorBufferPosition: -> - @getCursor().getBufferPosition() - - # Public: Returns the screen {Range} of the most recently added local - # {Selection}. - getSelectedScreenRange: -> - @getLastSelection().getScreenRange() - - # Public: Returns the buffer {Range} of the most recently added local - # {Selection}. - getSelectedBufferRange: -> - @getLastSelection().getBufferRange() - - # Public: Gets an Array of buffer {Range}s of all the local {Selection}s. - # - # Sorted by their position in the file itself. - getSelectedBufferRanges: -> - selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition() - - # Public: Gets an Array of buffer {Range}s of all the remote {Selection}s. - # - # Sorted by their position in the file itself. - getRemoteSelectedBufferRanges: -> - selection.getBufferRange() for selection in @getRemoteSelectionsOrderedByBufferPosition() - - # Public: Returns the selected text of the most recently added local {Selection}. - getSelectedText: -> - @getLastSelection().getText() - - # Public: Returns the text within a given a buffer {Range} - getTextInBufferRange: (range) -> - @buffer.getTextInRange(range) - - setTextInBufferRange: (range, text) -> @getBuffer().change(range, text) - - # Public: Returns the text of the most recent local cursor's surrounding - # paragraph. - getCurrentParagraphBufferRange: -> - @getCursor().getCurrentParagraphBufferRange() - - # Public: Returns the word under the most recently added local {Cursor}. - # - # * options: - # + An object with properties based on - # {Cursor.getBeginningOfCurrentWordBufferPosition}. - getWordUnderCursor: (options) -> - @getTextInBufferRange(@getCursor().getCurrentWordBufferRange(options)) - - # Public: Moves every local cursor up one row. - moveCursorUp: (lineCount) -> - @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true) - - # Public: Moves every local cursor down one row. - moveCursorDown: (lineCount) -> - @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true) - - # Public: Moves every local cursor left one column. - moveCursorLeft: -> - @moveCursors (cursor) -> cursor.moveLeft(moveToEndOfSelection: true) - - # Public: Moves every local cursor right one column. - moveCursorRight: -> - @moveCursors (cursor) -> cursor.moveRight(moveToEndOfSelection: true) - - # Public: Moves every local cursor to the top of the buffer. - moveCursorToTop: -> - @moveCursors (cursor) -> cursor.moveToTop() - - # Public: Moves every local cursor to the bottom of the buffer. - moveCursorToBottom: -> - @moveCursors (cursor) -> cursor.moveToBottom() - - # Public: Moves every local cursor to the beginning of the line. - moveCursorToBeginningOfLine: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfLine() - - # Public: Moves every local cursor to the first non-whitespace character of the line. - moveCursorToFirstCharacterOfLine: -> - @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine() - - # Public: Moves every local cursor to the end of the line. - moveCursorToEndOfLine: -> - @moveCursors (cursor) -> cursor.moveToEndOfLine() - - # Public: Moves every local cursor to the beginning of the current word. - moveCursorToBeginningOfWord: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfWord() - - # Public: Moves every local cursor to the end of the current word. - moveCursorToEndOfWord: -> - @moveCursors (cursor) -> cursor.moveToEndOfWord() - - # Public: Moves every local cursor to the beginning of the next word. - moveCursorToBeginningOfNextWord: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord() - - # Public: Moves every local cursor to the previous word boundary. - moveCursorToPreviousWordBoundary: -> - @moveCursors (cursor) -> cursor.moveToPreviousWordBoundary() - - # Public: Moves every local cursor to the next word boundary. - moveCursorToNextWordBoundary: -> - @moveCursors (cursor) -> cursor.moveToNextWordBoundary() - - # Internal: Executes given function on all local cursors. - moveCursors: (fn) -> - fn(cursor) for cursor in @getCursors() - @mergeCursors() - - # Public: Selects the text from the current cursor position to a given screen - # position. - # - # * position: - # An instance of {Point}, with a given `row` and `column`. - selectToScreenPosition: (position) -> - lastSelection = @getLastSelection() - lastSelection.selectToScreenPosition(position) - @mergeIntersectingSelections(isReversed: lastSelection.isReversed()) - - # Public: Selects the text one position right of all local cursors. - selectRight: -> - @expandSelectionsForward (selection) => selection.selectRight() - - # Public: Selects the text one position left of all local cursors. - selectLeft: -> - @expandSelectionsBackward (selection) => selection.selectLeft() - - # Public: Selects all the text one position above all local cursors. - selectUp: (rowCount) -> - @expandSelectionsBackward (selection) => selection.selectUp(rowCount) - - # Public: Selects all the text one position below all local cursors. - selectDown: (rowCount) -> - @expandSelectionsForward (selection) => selection.selectDown(rowCount) - - # Public: Selects all the text from all local cursors to the top of the - # buffer. - selectToTop: -> - @expandSelectionsBackward (selection) => selection.selectToTop() - - # Public: Selects all the text in the buffer. - selectAll: -> - @expandSelectionsForward (selection) => selection.selectAll() - - # Public: Selects all the text from all local cursors to the bottom of the - # buffer. - selectToBottom: -> - @expandSelectionsForward (selection) => selection.selectToBottom() - - # Public: Selects all the text from all local cursors to the beginning of each - # of their lines. - selectToBeginningOfLine: -> - @expandSelectionsBackward (selection) => selection.selectToBeginningOfLine() - - # Public: Selects to the first non-whitespace character of the line of all - # local cursors. - selectToFirstCharacterOfLine: -> - @expandSelectionsBackward (selection) => selection.selectToFirstCharacterOfLine() - - # Public: Selects all the text from each local cursor to the end of their - # lines. - selectToEndOfLine: -> - @expandSelectionsForward (selection) => selection.selectToEndOfLine() - - # Public: Selects all text from each local cursor to their previous word - # boundary. - selectToPreviousWordBoundary: -> - @expandSelectionsBackward (selection) => selection.selectToPreviousWordBoundary() - - # Public: Selects all text from each local cursor to their next word - # boundary. - selectToNextWordBoundary: -> - @expandSelectionsForward (selection) => selection.selectToNextWordBoundary() - - # Public: Selects the current line from each local cursor. - selectLine: -> - @expandSelectionsForward (selection) => selection.selectLine() - - # Public: Moves each local selection down one row. - addSelectionBelow: -> - @expandSelectionsForward (selection) => selection.addSelectionBelow() - - # Public: Moves each local selection up one row. - addSelectionAbove: -> - @expandSelectionsBackward (selection) => selection.addSelectionAbove() - - # Public: Transposes the current text selections. - # - # FIXME: I have no idea what this function does. - # - # This only works if there is more than one selection. Each selection is transferred - # to the position of the selection after it. The last selection is transferred to the - # position of the first. - transpose: -> - @mutateSelectedText (selection) => - if selection.isEmpty() - selection.selectRight() - text = selection.getText() - selection.delete() - selection.cursor.moveLeft() - selection.insertText text - else - selection.insertText selection.getText().split('').reverse().join('') - - # Public: Uppercases all locally selected text. - upperCase: -> - @replaceSelectedText selectWordIfEmpty:true, (text) => text.toUpperCase() - - # Public: Lowercases all locally selected text. - lowerCase: -> - @replaceSelectedText selectWordIfEmpty:true, (text) => text.toLowerCase() - - # Public: Joins the current line with the one below it. - # - # FIXME: Needs more clarity. - # - # Multiple cursors are considered equally. If there's a selection in the editor, - # all the lines are joined together. - joinLine: -> - @mutateSelectedText (selection) -> selection.joinLine() - - # {Delegates to: Selection.expandOverLine} - expandLastSelectionOverLine: -> - @getLastSelection().expandOverLine() - - # Public: Selects all the text from all local cursors to the beginning of - # their current words. - selectToBeginningOfWord: -> - @expandSelectionsBackward (selection) => selection.selectToBeginningOfWord() - - # Public: Selects all the text from all local cursors to the end of - # their current words. - selectToEndOfWord: -> - @expandSelectionsForward (selection) => selection.selectToEndOfWord() - - # Public: Selects all the text from all local cursors to the beginning of - # the next word. - selectToBeginningOfNextWord: -> - @expandSelectionsForward (selection) => selection.selectToBeginningOfNextWord() - - # Public: Selects the current word of each local cursor. - selectWord: -> - @expandSelectionsForward (selection) => selection.selectWord() - - # {Delegates to: Selection.expandOverWord} - expandLastSelectionOverWord: -> - @getLastSelection().expandOverWord() - - # Public: Selects the range associated with the given marker if it is valid. - # - # Returns the selected {Range} or a falsy value if the marker is invalid. - selectMarker: (marker) -> - if marker.isValid() - range = marker.getBufferRange() - @setSelectedBufferRange(range) - range - - # Public: - # - # FIXME: Not sure how to describe what this does. - mergeCursors: -> - positions = [] - for cursor in @getCursors() - position = cursor.getBufferPosition().toString() - if position in positions - cursor.destroy() - else - positions.push(position) - - # Public: - # - # FIXME: Not sure how to describe what this does. - expandSelectionsForward: (fn) -> - @mergeIntersectingSelections => - fn(selection) for selection in @getSelections() - - # Public: - # - # FIXME: Not sure how to describe what this does. - expandSelectionsBackward: (fn) -> - @mergeIntersectingSelections isReversed: true, => - fn(selection) for selection in @getSelections() - - # Public: - # - # FIXME: No idea what this does. - finalizeSelections: -> - selection.finalize() for selection in @getSelections() - - # Private: Merges intersecting selections. If passed a function, it executes - # the function with merging suppressed, then merges intersecting selections - # afterward. - mergeIntersectingSelections: (args...) -> - fn = args.pop() if _.isFunction(_.last(args)) - options = args.pop() ? {} - - return fn?() if @suppressSelectionMerging - - if fn? - @suppressSelectionMerging = true - result = fn() - @suppressSelectionMerging = false - - reducer = (disjointSelections, selection) -> - intersectingSelection = _.find(disjointSelections, (s) -> s.intersectsWith(selection)) - if intersectingSelection? - intersectingSelection.merge(selection, options) - disjointSelections - else - disjointSelections.concat([selection]) - - _.reduce(@getSelections(), reducer, []) - - # Private: - preserveCursorPositionOnBufferReload: -> - cursorPosition = null - @subscribe @buffer, "will-reload", => - cursorPosition = @getCursorBufferPosition() - @subscribe @buffer, "reloaded", => - @setCursorBufferPosition(cursorPosition) if cursorPosition - cursorPosition = null - - # {Delegates to: DisplayBuffer.getGrammar} - getGrammar: -> - @displayBuffer.getGrammar() - - # {Delegates to: DisplayBuffer.setGrammar} - setGrammar: (grammar) -> - @displayBuffer.setGrammar(grammar) - - # {Delegates to: DisplayBuffer.reloadGrammar} - reloadGrammar: -> - @displayBuffer.reloadGrammar() - - # Private: - shouldAutoIndent: -> - atom.config.get("editor.autoIndent") - - # Public: Performs all editor actions from the given function within a single - # undo step. - # - # Useful for implementing complex operations while still ensuring that the - # undo stack remains relevant. - transact: (fn) -> @buffer.transact(fn) - - # Private: - beginTransaction: -> @buffer.beginTransaction() - - # Private: - commitTransaction: -> @buffer.commitTransaction() - - # Private: - abortTransaction: -> @buffer.abortTransaction() - - # Private: - inspect: -> - JSON.stringify @state.toObject() - - # Private: - logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) - - # Private: - handleGrammarChange: -> - @unfoldAll() - @emit 'grammar-changed' - - # Private: - handleMarkerCreated: (marker) => - if marker.matchesAttributes(@getSelectionMarkerAttributes()) - @addSelection(marker) - - # Private: - getSelectionMarkerAttributes: -> - type: 'selection', editSessionId: @id, invalidate: 'never' - - # Private: - getDebugSnapshot: -> - [ - @displayBuffer.getDebugSnapshot() - @displayBuffer.tokenizedBuffer.getDebugSnapshot() - ].join('\n\n') diff --git a/src/editor-view.coffee b/src/editor-view.coffee new file mode 100644 index 000000000..6e60ebfbd --- /dev/null +++ b/src/editor-view.coffee @@ -0,0 +1,1854 @@ +{View, $, $$} = require './space-pen-extensions' +TextBuffer = require './text-buffer' +Gutter = require './gutter' +{Point, Range} = require 'telepath' +Editor = require './editor' +CursorView = require './cursor-view' +SelectionView = require './selection-view' +fs = require 'fs-plus' +_ = require 'underscore-plus' + +MeasureRange = document.createRange() +TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT } +NoScope = ['no-scope'] +LongLineLength = 1000 + +# Private: Represents the entire visual pane in Atom. +# +# The EditorView manages the {Editor}, which manages the file buffers. +module.exports = +class EditorView extends View + @characterWidthCache: {} + @configDefaults: + fontSize: 20 + showInvisibles: false + showIndentGuide: false + showLineNumbers: true + autoIndent: true + normalizeIndentOnPaste: true + nonWordCharacters: "./\\()\"':,.;<>~!@#$%^&*|+=[]{}`~?-" + preferredLineLength: 80 + tabLength: 2 + softWrap: false + softTabs: true + softWrapAtPreferredLineLength: false + + @nextEditorId: 1 + + ### Internal ### + + @content: (params) -> + attributes = { class: @classes(params), tabindex: -1 } + _.extend(attributes, params.attributes) if params.attributes + @div attributes, => + @subview 'gutter', new Gutter + @div class: 'scroll-view', outlet: 'scrollView', => + @div class: 'overlayer', outlet: 'overlayer' + @div class: 'lines', outlet: 'renderedLines' + @div class: 'underlayer', outlet: 'underlayer', => + @input class: 'hidden-input', outlet: 'hiddenInput' + @div class: 'vertical-scrollbar', outlet: 'verticalScrollbar', => + @div outlet: 'verticalScrollbarContent' + + @classes: ({mini} = {}) -> + classes = ['editor', 'editor-colors'] + classes.push 'mini' if mini + classes.join(' ') + + vScrollMargin: 2 + hScrollMargin: 10 + lineHeight: null + charWidth: null + charHeight: null + cursorViews: null + selectionViews: null + lineCache: null + isFocused: false + activeEditSession: null + attached: false + lineOverdraw: 10 + pendingChanges: null + newCursors: null + newSelections: null + redrawOnReattach: false + bottomPaddingInLines: 10 + + ### Public ### + + # The constructor for setting up an `EditorView` instance. + # + # editorOrOptions - Either an {Editor}, or an object with one property, `mini`. + # If `mini` is `true`, a "miniature" `Editor` is constructed. + # Typically, this is ideal for scenarios where you need an Atom editor, + # but without all the chrome, like scrollbars, gutter, _e.t.c._. + # + initialize: (editorOrOptions) -> + if editorOrOptions instanceof Editor + editor = editorOrOptions + else + {editor, editSession, @mini} = editorOrOptions ? {} + editor ?= editSession # TODO: Remove this line after packages have updated their api to use Editor and EditorView + + @id = EditorView.nextEditorId++ + @lineCache = [] + @configure() + @bindKeys() + @handleEvents() + @handleInputEvents() + @cursorViews = [] + @selectionViews = [] + @pendingChanges = [] + @newCursors = [] + @newSelections = [] + + if editor? + @edit(editor) + else if @mini + @edit(new Editor + buffer: TextBuffer.createAsRoot() + softWrap: false + tabLength: 2 + softTabs: true + ) + else + throw new Error("Must supply an Editor or mini: true") + + # Internal: Sets up the core Atom commands. + # + # Some commands are excluded from mini-editors. + bindKeys: -> + editorBindings = + 'core:move-left': @moveCursorLeft + 'core:move-right': @moveCursorRight + 'core:select-left': @selectLeft + 'core:select-right': @selectRight + 'core:select-all': @selectAll + 'core:backspace': @backspace + 'core:delete': @delete + 'core:undo': @undo + 'core:redo': @redo + 'core:cut': @cutSelection + 'core:copy': @copySelection + 'core:paste': @paste + 'editor:move-to-previous-word': @moveCursorToPreviousWord + 'editor:select-word': @selectWord + 'editor:consolidate-selections': @consolidateSelections + 'editor:backspace-to-beginning-of-word': @backspaceToBeginningOfWord + 'editor:backspace-to-beginning-of-line': @backspaceToBeginningOfLine + 'editor:delete-to-end-of-word': @deleteToEndOfWord + 'editor:delete-line': @deleteLine + 'editor:cut-to-end-of-line': @cutToEndOfLine + 'editor:move-to-beginning-of-line': @moveCursorToBeginningOfLine + 'editor:move-to-end-of-line': @moveCursorToEndOfLine + 'editor:move-to-first-character-of-line': @moveCursorToFirstCharacterOfLine + 'editor:move-to-beginning-of-word': @moveCursorToBeginningOfWord + 'editor:move-to-end-of-word': @moveCursorToEndOfWord + 'editor:move-to-beginning-of-next-word': @moveCursorToBeginningOfNextWord + 'editor:move-to-previous-word-boundary': @moveCursorToPreviousWordBoundary + 'editor:move-to-next-word-boundary': @moveCursorToNextWordBoundary + 'editor:select-to-end-of-line': @selectToEndOfLine + 'editor:select-to-beginning-of-line': @selectToBeginningOfLine + 'editor:select-to-end-of-word': @selectToEndOfWord + 'editor:select-to-beginning-of-word': @selectToBeginningOfWord + 'editor:select-to-beginning-of-next-word': @selectToBeginningOfNextWord + 'editor:select-to-next-word-boundary': @selectToNextWordBoundary + 'editor:select-to-previous-word-boundary': @selectToPreviousWordBoundary + 'editor:select-to-first-character-of-line': @selectToFirstCharacterOfLine + 'editor:select-line': @selectLine + 'editor:transpose': @transpose + 'editor:upper-case': @upperCase + 'editor:lower-case': @lowerCase + + unless @mini + _.extend editorBindings, + 'core:move-up': @moveCursorUp + 'core:move-down': @moveCursorDown + 'core:move-to-top': @moveCursorToTop + 'core:move-to-bottom': @moveCursorToBottom + 'core:page-down': @pageDown + 'core:page-up': @pageUp + 'core:select-up': @selectUp + 'core:select-down': @selectDown + 'core:select-to-top': @selectToTop + 'core:select-to-bottom': @selectToBottom + 'editor:indent': @indent + 'editor:auto-indent': @autoIndent + 'editor:indent-selected-rows': @indentSelectedRows + 'editor:outdent-selected-rows': @outdentSelectedRows + 'editor:newline': @insertNewline + 'editor:newline-below': @insertNewlineBelow + 'editor:newline-above': @insertNewlineAbove + 'editor:add-selection-below': @addSelectionBelow + 'editor:add-selection-above': @addSelectionAbove + 'editor:toggle-soft-tabs': @toggleSoftTabs + 'editor:toggle-soft-wrap': @toggleSoftWrap + 'editor:fold-all': @foldAll + 'editor:unfold-all': @unfoldAll + 'editor:fold-current-row': @foldCurrentRow + 'editor:unfold-current-row': @unfoldCurrentRow + 'editor:fold-selection': @foldSelection + 'editor:fold-at-indent-level-1': => @foldAllAtIndentLevel(0) + 'editor:fold-at-indent-level-2': => @foldAllAtIndentLevel(1) + 'editor:fold-at-indent-level-3': => @foldAllAtIndentLevel(2) + 'editor:fold-at-indent-level-4': => @foldAllAtIndentLevel(3) + 'editor:fold-at-indent-level-5': => @foldAllAtIndentLevel(4) + 'editor:fold-at-indent-level-6': => @foldAllAtIndentLevel(5) + 'editor:fold-at-indent-level-7': => @foldAllAtIndentLevel(6) + 'editor:fold-at-indent-level-8': => @foldAllAtIndentLevel(7) + 'editor:fold-at-indent-level-9': => @foldAllAtIndentLevel(8) + 'editor:toggle-line-comments': @toggleLineCommentsInSelection + 'editor:log-cursor-scope': @logCursorScope + 'editor:checkout-head-revision': @checkoutHead + 'editor:copy-path': @copyPathToPasteboard + 'editor:move-line-up': @moveLineUp + 'editor:move-line-down': @moveLineDown + 'editor:duplicate-line': @duplicateLine + 'editor:join-line': @joinLine + 'editor:toggle-indent-guide': => atom.config.toggle('editor.showIndentGuide') + 'editor:save-debug-snapshot': @saveDebugSnapshot + 'editor:toggle-line-numbers': => atom.config.toggle('editor.showLineNumbers') + 'editor:scroll-to-cursor': @scrollToCursorPosition + + documentation = {} + for name, method of editorBindings + do (name, method) => + @command name, (e) => method.call(this, e); false + + # {Delegates to: Editor.getCursor} + getCursor: -> @activeEditSession.getCursor() + + # {Delegates to: Editor.getCursors} + getCursors: -> @activeEditSession.getCursors() + + # {Delegates to: Editor.addCursorAtScreenPosition} + addCursorAtScreenPosition: (screenPosition) -> @activeEditSession.addCursorAtScreenPosition(screenPosition) + + # {Delegates to: Editor.addCursorAtBufferPosition} + addCursorAtBufferPosition: (bufferPosition) -> @activeEditSession.addCursorAtBufferPosition(bufferPosition) + + # {Delegates to: Editor.moveCursorUp} + moveCursorUp: -> @activeEditSession.moveCursorUp() + + # {Delegates to: Editor.moveCursorDown} + moveCursorDown: -> @activeEditSession.moveCursorDown() + + # {Delegates to: Editor.moveCursorLeft} + moveCursorLeft: -> @activeEditSession.moveCursorLeft() + + # {Delegates to: Editor.moveCursorRight} + moveCursorRight: -> @activeEditSession.moveCursorRight() + + # {Delegates to: Editor.moveCursorToBeginningOfWord} + moveCursorToBeginningOfWord: -> @activeEditSession.moveCursorToBeginningOfWord() + + # {Delegates to: Editor.moveCursorToEndOfWord} + moveCursorToEndOfWord: -> @activeEditSession.moveCursorToEndOfWord() + + # {Delegates to: Editor.moveCursorToBeginningOfNextWord} + moveCursorToBeginningOfNextWord: -> @activeEditSession.moveCursorToBeginningOfNextWord() + + # {Delegates to: Editor.moveCursorToTop} + moveCursorToTop: -> @activeEditSession.moveCursorToTop() + + # {Delegates to: Editor.moveCursorToBottom} + moveCursorToBottom: -> @activeEditSession.moveCursorToBottom() + + # {Delegates to: Editor.moveCursorToBeginningOfLine} + moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine() + + # {Delegates to: Editor.moveCursorToFirstCharacterOfLine} + moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine() + + # {Delegates to: Editor.moveCursorToPreviousWordBoundary} + moveCursorToPreviousWordBoundary: -> @activeEditSession.moveCursorToPreviousWordBoundary() + + # {Delegates to: Editor.moveCursorToNextWordBoundary} + moveCursorToNextWordBoundary: -> @activeEditSession.moveCursorToNextWordBoundary() + + # {Delegates to: Editor.moveCursorToEndOfLine} + moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine() + + # {Delegates to: Editor.moveLineUp} + moveLineUp: -> @activeEditSession.moveLineUp() + + # {Delegates to: Editor.moveLineDown} + moveLineDown: -> @activeEditSession.moveLineDown() + + # {Delegates to: Editor.setCursorScreenPosition} + setCursorScreenPosition: (position, options) -> @activeEditSession.setCursorScreenPosition(position, options) + + # {Delegates to: Editor.duplicateLine} + duplicateLine: -> @activeEditSession.duplicateLine() + + # {Delegates to: Editor.joinLine} + joinLine: -> @activeEditSession.joinLine() + + # {Delegates to: Editor.getCursorScreenPosition} + getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition() + + # {Delegates to: Editor.getCursorScreenRow} + getCursorScreenRow: -> @activeEditSession.getCursorScreenRow() + + # {Delegates to: Editor.setCursorBufferPosition} + setCursorBufferPosition: (position, options) -> @activeEditSession.setCursorBufferPosition(position, options) + + # {Delegates to: Editor.getCursorBufferPosition} + getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition() + + # {Delegates to: Editor.getCurrentParagraphBufferRange} + getCurrentParagraphBufferRange: -> @activeEditSession.getCurrentParagraphBufferRange() + + # {Delegates to: Editor.getWordUnderCursor} + getWordUnderCursor: (options) -> @activeEditSession.getWordUnderCursor(options) + + # {Delegates to: Editor.getSelection} + getSelection: (index) -> @activeEditSession.getSelection(index) + + # {Delegates to: Editor.getSelections} + getSelections: -> @activeEditSession.getSelections() + + # {Delegates to: Editor.getSelectionsOrderedByBufferPosition} + getSelectionsOrderedByBufferPosition: -> @activeEditSession.getSelectionsOrderedByBufferPosition() + + # {Delegates to: Editor.getLastSelectionInBuffer} + getLastSelectionInBuffer: -> @activeEditSession.getLastSelectionInBuffer() + + # {Delegates to: Editor.getSelectedText} + getSelectedText: -> @activeEditSession.getSelectedText() + + # {Delegates to: Editor.getSelectedBufferRanges} + getSelectedBufferRanges: -> @activeEditSession.getSelectedBufferRanges() + + # {Delegates to: Editor.getSelectedBufferRange} + getSelectedBufferRange: -> @activeEditSession.getSelectedBufferRange() + + # {Delegates to: Editor.setSelectedBufferRange} + setSelectedBufferRange: (bufferRange, options) -> @activeEditSession.setSelectedBufferRange(bufferRange, options) + + # {Delegates to: Editor.setSelectedBufferRanges} + setSelectedBufferRanges: (bufferRanges, options) -> @activeEditSession.setSelectedBufferRanges(bufferRanges, options) + + # {Delegates to: Editor.addSelectionForBufferRange} + addSelectionForBufferRange: (bufferRange, options) -> @activeEditSession.addSelectionForBufferRange(bufferRange, options) + + # {Delegates to: Editor.selectRight} + selectRight: -> @activeEditSession.selectRight() + + # {Delegates to: Editor.selectLeft} + selectLeft: -> @activeEditSession.selectLeft() + + # {Delegates to: Editor.selectUp} + selectUp: -> @activeEditSession.selectUp() + + # {Delegates to: Editor.selectDown} + selectDown: -> @activeEditSession.selectDown() + + # {Delegates to: Editor.selectToTop} + selectToTop: -> @activeEditSession.selectToTop() + + # {Delegates to: Editor.selectToBottom} + selectToBottom: -> @activeEditSession.selectToBottom() + + # {Delegates to: Editor.selectAll} + selectAll: -> @activeEditSession.selectAll() + + # {Delegates to: Editor.selectToBeginningOfLine} + selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine() + + # {Delegates to: Editor.selectToFirstCharacterOfLine} + selectToFirstCharacterOfLine: -> @activeEditSession.selectToFirstCharacterOfLine() + + # {Delegates to: Editor.selectToEndOfLine} + selectToEndOfLine: -> @activeEditSession.selectToEndOfLine() + + # {Delegates to: Editor.selectToPreviousWordBoundary} + selectToPreviousWordBoundary: -> @activeEditSession.selectToPreviousWordBoundary() + + # {Delegates to: Editor.selectToNextWordBoundary} + selectToNextWordBoundary: -> @activeEditSession.selectToNextWordBoundary() + + # {Delegates to: Editor.addSelectionBelow} + addSelectionBelow: -> @activeEditSession.addSelectionBelow() + + # {Delegates to: Editor.addSelectionAbove} + addSelectionAbove: -> @activeEditSession.addSelectionAbove() + + # {Delegates to: Editor.selectToBeginningOfWord} + selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord() + + # {Delegates to: Editor.selectToEndOfWord} + selectToEndOfWord: -> @activeEditSession.selectToEndOfWord() + + # {Delegates to: Editor.selectToBeginningOfNextWord} + selectToBeginningOfNextWord: -> @activeEditSession.selectToBeginningOfNextWord() + + # {Delegates to: Editor.selectWord} + selectWord: -> @activeEditSession.selectWord() + + # {Delegates to: Editor.selectLine} + selectLine: -> @activeEditSession.selectLine() + + # {Delegates to: Editor.selectToScreenPosition} + selectToScreenPosition: (position) -> @activeEditSession.selectToScreenPosition(position) + + # {Delegates to: Editor.transpose} + transpose: -> @activeEditSession.transpose() + + # {Delegates to: Editor.upperCase} + upperCase: -> @activeEditSession.upperCase() + + # {Delegates to: Editor.lowerCase} + lowerCase: -> @activeEditSession.lowerCase() + + # {Delegates to: Editor.clearSelections} + clearSelections: -> @activeEditSession.clearSelections() + + # {Delegates to: Editor.backspace} + backspace: -> @activeEditSession.backspace() + + # {Delegates to: Editor.backspaceToBeginningOfWord} + backspaceToBeginningOfWord: -> @activeEditSession.backspaceToBeginningOfWord() + + # {Delegates to: Editor.backspaceToBeginningOfLine} + backspaceToBeginningOfLine: -> @activeEditSession.backspaceToBeginningOfLine() + + # {Delegates to: Editor.delete} + delete: -> @activeEditSession.delete() + + # {Delegates to: Editor.deleteToEndOfWord} + deleteToEndOfWord: -> @activeEditSession.deleteToEndOfWord() + + # {Delegates to: Editor.deleteLine} + deleteLine: -> @activeEditSession.deleteLine() + + # {Delegates to: Editor.cutToEndOfLine} + cutToEndOfLine: -> @activeEditSession.cutToEndOfLine() + + # {Delegates to: Editor.insertText} + insertText: (text, options) -> @activeEditSession.insertText(text, options) + + # {Delegates to: Editor.insertNewline} + insertNewline: -> @activeEditSession.insertNewline() + + # {Delegates to: Editor.insertNewlineBelow} + insertNewlineBelow: -> @activeEditSession.insertNewlineBelow() + + # {Delegates to: Editor.insertNewlineAbove} + insertNewlineAbove: -> @activeEditSession.insertNewlineAbove() + + # {Delegates to: Editor.indent} + indent: (options) -> @activeEditSession.indent(options) + + # {Delegates to: Editor.autoIndentSelectedRows} + autoIndent: (options) -> @activeEditSession.autoIndentSelectedRows() + + # {Delegates to: Editor.indentSelectedRows} + indentSelectedRows: -> @activeEditSession.indentSelectedRows() + + # {Delegates to: Editor.outdentSelectedRows} + outdentSelectedRows: -> @activeEditSession.outdentSelectedRows() + + # {Delegates to: Editor.cutSelectedText} + cutSelection: -> @activeEditSession.cutSelectedText() + + # {Delegates to: Editor.copySelectedText} + copySelection: -> @activeEditSession.copySelectedText() + + # {Delegates to: Editor.pasteText} + paste: (options) -> @activeEditSession.pasteText(options) + + # {Delegates to: Editor.undo} + undo: -> @activeEditSession.undo() + + # {Delegates to: Editor.redo} + redo: -> @activeEditSession.redo() + + # {Delegates to: Editor.createFold} + createFold: (startRow, endRow) -> @activeEditSession.createFold(startRow, endRow) + + # {Delegates to: Editor.foldCurrentRow} + foldCurrentRow: -> @activeEditSession.foldCurrentRow() + + # {Delegates to: Editor.unfoldCurrentRow} + unfoldCurrentRow: -> @activeEditSession.unfoldCurrentRow() + + # {Delegates to: Editor.foldAll} + foldAll: -> @activeEditSession.foldAll() + + # {Delegates to: Editor.unfoldAll} + unfoldAll: -> @activeEditSession.unfoldAll() + + # {Delegates to: Editor.foldSelection} + foldSelection: -> @activeEditSession.foldSelection() + + # {Delegates to: Editor.destroyFoldsContainingBufferRow} + destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow) + + # {Delegates to: Editor.isFoldedAtScreenRow} + isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow) + + # {Delegates to: Editor.isFoldedAtBufferRow} + isFoldedAtBufferRow: (bufferRow) -> @activeEditSession.isFoldedAtBufferRow(bufferRow) + + # {Delegates to: Editor.isFoldedAtCursorRow} + isFoldedAtCursorRow: -> @activeEditSession.isFoldedAtCursorRow() + + foldAllAtIndentLevel: (indentLevel) -> @activeEditSession.foldAllAtIndentLevel(indentLevel) + + # {Delegates to: Editor.lineForScreenRow} + lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow) + + # {Delegates to: Editor.linesForScreenRows} + linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end) + + # {Delegates to: Editor.getScreenLineCount} + getScreenLineCount: -> @activeEditSession.getScreenLineCount() + + # Private: + setHeightInLines: (heightInLines)-> + heightInLines ?= @calculateHeightInLines() + @heightInLines = heightInLines if heightInLines + + # {Delegates to: Editor.setEditorWidthInChars} + setWidthInChars: (widthInChars) -> + widthInChars ?= @calculateWidthInChars() + @activeEditSession.setEditorWidthInChars(widthInChars) if widthInChars + + # {Delegates to: Editor.getMaxScreenLineLength} + getMaxScreenLineLength: -> @activeEditSession.getMaxScreenLineLength() + + # {Delegates to: Editor.getLastScreenRow} + getLastScreenRow: -> @activeEditSession.getLastScreenRow() + + # {Delegates to: Editor.clipScreenPosition} + clipScreenPosition: (screenPosition, options={}) -> @activeEditSession.clipScreenPosition(screenPosition, options) + + # {Delegates to: Editor.screenPositionForBufferPosition} + screenPositionForBufferPosition: (position, options) -> @activeEditSession.screenPositionForBufferPosition(position, options) + + # {Delegates to: Editor.bufferPositionForScreenPosition} + bufferPositionForScreenPosition: (position, options) -> @activeEditSession.bufferPositionForScreenPosition(position, options) + + # {Delegates to: Editor.screenRangeForBufferRange} + screenRangeForBufferRange: (range) -> @activeEditSession.screenRangeForBufferRange(range) + + # {Delegates to: Editor.bufferRangeForScreenRange} + bufferRangeForScreenRange: (range) -> @activeEditSession.bufferRangeForScreenRange(range) + + # {Delegates to: Editor.bufferRowsForScreenRows} + bufferRowsForScreenRows: (startRow, endRow) -> @activeEditSession.bufferRowsForScreenRows(startRow, endRow) + + # Public: Emulates the "page down" key, where the last row of a buffer scrolls to become the first. + pageDown: -> + newScrollTop = @scrollTop() + @scrollView[0].clientHeight + @activeEditSession.moveCursorDown(@getPageRows()) + @scrollTop(newScrollTop, adjustVerticalScrollbar: true) + + # Public: Emulates the "page up" key, where the frst row of a buffer scrolls to become the last. + pageUp: -> + newScrollTop = @scrollTop() - @scrollView[0].clientHeight + @activeEditSession.moveCursorUp(@getPageRows()) + @scrollTop(newScrollTop, adjustVerticalScrollbar: true) + + # Gets the number of actual page rows existing in an editor. + # + # Returns a {Number}. + getPageRows: -> + Math.max(1, Math.ceil(@scrollView[0].clientHeight / @lineHeight)) + + # Set whether invisible characters are shown. + # + # showInvisibles - A {Boolean} which, if `true`, show invisible characters + setShowInvisibles: (showInvisibles) -> + return if showInvisibles == @showInvisibles + @showInvisibles = showInvisibles + @resetDisplay() + + # Defines which characters are invisible. + # + # invisibles - A hash defining the invisible characters: The defaults are: + # eol: `\u00ac` + # space: `\u00b7` + # tab: `\u00bb` + # cr: `\u00a4` + setInvisibles: (@invisibles={}) -> + _.defaults @invisibles, + eol: '\u00ac' + space: '\u00b7' + tab: '\u00bb' + cr: '\u00a4' + @resetDisplay() + + # Sets whether you want to show the indentation guides. + # + # showIndentGuide - A {Boolean} you can set to `true` if you want to see the indentation guides. + setShowIndentGuide: (showIndentGuide) -> + return if showIndentGuide == @showIndentGuide + @showIndentGuide = showIndentGuide + @resetDisplay() + + # Checkout the HEAD revision of this editor's file. + checkoutHead: -> + if path = @getPath() + atom.project.getRepo()?.checkoutHead(path) + + # {Delegates to: Editor.setText} + setText: (text) -> @activeEditSession.setText(text) + + # {Delegates to: Editor.getText} + getText: -> @activeEditSession.getText() + + # {Delegates to: Editor.getPath} + getPath: -> @activeEditSession?.getPath() + + # {Delegates to: TextBuffer.getLineCount} + getLineCount: -> @getBuffer().getLineCount() + + # {Delegates to: TextBuffer.getLastRow} + getLastBufferRow: -> @getBuffer().getLastRow() + + # {Delegates to: TextBuffer.getTextInRange} + getTextInRange: (range) -> @getBuffer().getTextInRange(range) + + # {Delegates to: TextBuffer.getEofPosition} + getEofPosition: -> @getBuffer().getEofPosition() + + # {Delegates to: TextBuffer.lineForRow} + lineForBufferRow: (row) -> @getBuffer().lineForRow(row) + + # {Delegates to: TextBuffer.lineLengthForRow} + lineLengthForBufferRow: (row) -> @getBuffer().lineLengthForRow(row) + + # {Delegates to: TextBuffer.rangeForRow} + rangeForBufferRow: (row) -> @getBuffer().rangeForRow(row) + + # {Delegates to: TextBuffer.scanInRange} + scanInBufferRange: (args...) -> @getBuffer().scanInRange(args...) + + # {Delegates to: TextBuffer.backwardsScanInRange} + backwardsScanInBufferRange: (args...) -> @getBuffer().backwardsScanInRange(args...) + + ### Internal ### + + configure: -> + @observeConfig 'editor.showLineNumbers', (showLineNumbers) => @gutter.setShowLineNumbers(showLineNumbers) + @observeConfig 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles) + @observeConfig 'editor.showIndentGuide', (showIndentGuide) => @setShowIndentGuide(showIndentGuide) + @observeConfig 'editor.invisibles', (invisibles) => @setInvisibles(invisibles) + @observeConfig 'editor.fontSize', (fontSize) => @setFontSize(fontSize) + @observeConfig 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily) + + handleEvents: -> + @on 'focus', => + @hiddenInput.focus() + false + + @hiddenInput.on 'focus', => + @bringHiddenInputIntoView() + @isFocused = true + @addClass 'is-focused' + + @hiddenInput.on 'focusout', => + @bringHiddenInputIntoView() + @isFocused = false + @removeClass 'is-focused' + + @underlayer.on 'mousedown', (e) => + @renderedLines.trigger(e) + false if @isFocused + + @overlayer.on 'mousedown', (e) => + @overlayer.hide() + clickedElement = document.elementFromPoint(e.pageX, e.pageY) + @overlayer.show() + e.target = clickedElement + $(clickedElement).trigger(e) + false if @isFocused + + @renderedLines.on 'mousedown', '.fold.line', (e) => + id = $(e.currentTarget).attr('fold-id') + marker = @activeEditSession.displayBuffer.getMarker(id) + @activeEditSession.setCursorBufferPosition(marker.getBufferRange().start) + @activeEditSession.destroyFoldWithId(id) + false + + @renderedLines.on 'mousedown', (e) => + clickCount = e.originalEvent.detail + + screenPosition = @screenPositionFromMouseEvent(e) + if clickCount == 1 + if e.metaKey + @addCursorAtScreenPosition(screenPosition) + else if e.shiftKey + @selectToScreenPosition(screenPosition) + else + @setCursorScreenPosition(screenPosition) + else if clickCount == 2 + @activeEditSession.selectWord() unless e.shiftKey + else if clickCount == 3 + @activeEditSession.selectLine() unless e.shiftKey + + @selectOnMousemoveUntilMouseup() unless e.ctrlKey or e.originalEvent.which > 1 + + unless @mini + @scrollView.on 'mousewheel', (e) => + if delta = e.originalEvent.wheelDeltaY + @scrollTop(@scrollTop() - delta) + false + + @verticalScrollbar.on 'scroll', => + @scrollTop(@verticalScrollbar.scrollTop(), adjustVerticalScrollbar: false) + + @scrollView.on 'scroll', => + if @scrollLeft() == 0 + @gutter.removeClass('drop-shadow') + else + @gutter.addClass('drop-shadow') + + # Listen for overflow events to detect when the editor's width changes + # to update the soft wrap column. + updateWidthInChars = _.debounce((=> @setWidthInChars()), 100) + @scrollView.on 'overflowchanged', => + updateWidthInChars() if @[0].classList.contains('soft-wrap') + + handleInputEvents: -> + @on 'cursor:moved', => + return unless @isFocused + cursorView = @getCursorView() + + if cursorView.isVisible() + # This is an order of magnitude faster than checking .offset(). + style = cursorView[0].style + @hiddenInput[0].style.top = style.top + @hiddenInput[0].style.left = style.left + + selectedText = null + @hiddenInput.on 'compositionstart', => + selectedText = @getSelectedText() + @hiddenInput.css('width', '100%') + @hiddenInput.on 'compositionupdate', (e) => + @insertText(e.originalEvent.data, {select: true, undo: 'skip'}) + @hiddenInput.on 'compositionend', => + @insertText(selectedText, {select: true, undo: 'skip'}) + @hiddenInput.css('width', '1px') + + lastInput = '' + @on "textInput", (e) => + # Work around of the accented character suggestion feature in OS X. + selectedLength = @hiddenInput[0].selectionEnd - @hiddenInput[0].selectionStart + if selectedLength is 1 and lastInput is @hiddenInput.val() + @selectLeft() + + lastInput = e.originalEvent.data + @insertText(lastInput) + @hiddenInput.val(lastInput) + false + + bringHiddenInputIntoView: -> + @hiddenInput.css(top: @scrollTop(), left: @scrollLeft()) + + selectOnMousemoveUntilMouseup: -> + lastMoveEvent = null + moveHandler = (event = lastMoveEvent) => + if event + @selectToScreenPosition(@screenPositionFromMouseEvent(event)) + lastMoveEvent = event + + $(document).on "mousemove.editor-#{@id}", moveHandler + interval = setInterval(moveHandler, 20) + + $(document).one "mouseup.editor-#{@id}", => + clearInterval(interval) + $(document).off 'mousemove', moveHandler + @activeEditSession.mergeIntersectingSelections(isReversed: @activeEditSession.getLastSelection().isReversed()) + @activeEditSession.finalizeSelections() + @syncCursorAnimations() + + afterAttach: (onDom) -> + return unless onDom + @redraw() if @redrawOnReattach + return if @attached + @attached = true + @calculateDimensions() + @setWidthInChars() + @subscribe $(window), "resize.editor-#{@id}", => + @setHeightInLines() + @setWidthInChars() + @updateLayerDimensions() + @requestDisplayUpdate() + @focus() if @isFocused + + if pane = @getPane() + @active = @is(pane.activeView) + @subscribe pane, 'pane:active-item-changed', (event, item) => + wasActive = @active + @active = @is(pane.activeView) + @redraw() if @active and not wasActive + + @resetDisplay() + + @trigger 'editor:attached', [this] + + edit: (editor) -> + return if editor is @activeEditSession + + if @activeEditSession + @saveScrollPositionForActiveEditSession() + @activeEditSession.off(".editor") + + @activeEditSession = editor + + return unless @activeEditSession? + + @activeEditSession.setVisible(true) + + @activeEditSession.on "contents-conflicted.editor", => + @showBufferConflictAlert(@activeEditSession) + + @activeEditSession.on "path-changed.editor", => + @reloadGrammar() + @trigger 'editor:path-changed' + + @activeEditSession.on "grammar-changed.editor", => + @trigger 'editor:grammar-changed' + + @activeEditSession.on 'selection-added.editor', (selection) => + @newCursors.push(selection.cursor) + @newSelections.push(selection) + @requestDisplayUpdate() + + @activeEditSession.on 'screen-lines-changed.editor', (e) => + @handleScreenLinesChange(e) + + @activeEditSession.on 'scroll-top-changed.editor', (scrollTop) => + @scrollTop(scrollTop) + + @activeEditSession.on 'scroll-left-changed.editor', (scrollLeft) => + @scrollLeft(scrollLeft) + + @activeEditSession.on 'soft-wrap-changed.editor', (softWrap) => + @setSoftWrap(softWrap) + + @trigger 'editor:path-changed' + @resetDisplay() + + if @attached and @activeEditSession.buffer.isInConflict() + _.defer => @showBufferConflictAlert(@activeEditSession) # Display after editor has a chance to display + + getModel: -> + @activeEditSession + + setModel: (editor) -> + @edit(editor) + + showBufferConflictAlert: (editor) -> + atom.confirm( + editor.getPath(), + "Has changed on disk. Do you want to reload it?", + "Reload", (=> editor.buffer.reload()), + "Cancel" + ) + + scrollTop: (scrollTop, options={}) -> + return @cachedScrollTop or 0 unless scrollTop? + maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height() + scrollTop = Math.floor(Math.max(0, Math.min(maxScrollTop, scrollTop))) + return if scrollTop == @cachedScrollTop + @cachedScrollTop = scrollTop + + @updateDisplay() if @attached + + @renderedLines.css('top', -scrollTop) + @underlayer.css('top', -scrollTop) + @overlayer.css('top', -scrollTop) + @gutter.lineNumbers.css('top', -scrollTop) + + if options?.adjustVerticalScrollbar ? true + @verticalScrollbar.scrollTop(scrollTop) + @activeEditSession.setScrollTop(@scrollTop()) + + scrollBottom: (scrollBottom) -> + if scrollBottom? + @scrollTop(scrollBottom - @scrollView.height()) + else + @scrollTop() + @scrollView.height() + + scrollLeft: (scrollLeft) -> + if scrollLeft? + @scrollView.scrollLeft(scrollLeft) + @activeEditSession.setScrollLeft(@scrollLeft()) + else + @scrollView.scrollLeft() + + scrollRight: (scrollRight) -> + if scrollRight? + @scrollView.scrollRight(scrollRight) + @activeEditSession.setScrollLeft(@scrollLeft()) + else + @scrollView.scrollRight() + + ### Public ### + + # Retrieves the {Editor}'s buffer. + # + # Returns the current {TextBuffer}. + getBuffer: -> @activeEditSession.buffer + + # Scrolls the editor to the bottom. + scrollToBottom: -> + @scrollBottom(@getScreenLineCount() * @lineHeight) + + # Scrolls the editor to the position of the most recently added cursor. + # + # The editor is also centered. + scrollToCursorPosition: -> + @scrollToBufferPosition(@getCursorBufferPosition(), center: true) + + # Scrolls the editor to the given buffer position. + # + # bufferPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - A hash matching the options available to {.scrollToPixelPosition} + scrollToBufferPosition: (bufferPosition, options) -> + @scrollToPixelPosition(@pixelPositionForBufferPosition(bufferPosition), options) + + # Scrolls the editor to the given screen position. + # + # screenPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - A hash matching the options available to {.scrollToPixelPosition} + scrollToScreenPosition: (screenPosition, options) -> + @scrollToPixelPosition(@pixelPositionForScreenPosition(screenPosition), options) + + # Scrolls the editor to the given pixel position. + # + # pixelPosition - An object that represents a pixel position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - A hash with the following keys: + # center: if `true`, the position is scrolled such that it's in the center of the editor + scrollToPixelPosition: (pixelPosition, options) -> + return unless @attached + @scrollVertically(pixelPosition, options) + @scrollHorizontally(pixelPosition) + + # Highlight all the folds within the given buffer range. + # + # "Highlighting" essentially just adds the `fold-selected` class to the line's + # DOM element. + # + # bufferRange - The {Range} to check. + highlightFoldsContainingBufferRange: (bufferRange) -> + screenLines = @linesForScreenRows(@firstRenderedScreenRow, @lastRenderedScreenRow) + for screenLine, i in screenLines + if fold = screenLine.fold + screenRow = @firstRenderedScreenRow + i + element = @lineElementForScreenRow(screenRow) + + if bufferRange.intersectsWith(fold.getBufferRange()) + element.addClass('fold-selected') + else + element.removeClass('fold-selected') + + saveScrollPositionForActiveEditSession: -> + if @attached + @activeEditSession.setScrollTop(@scrollTop()) + @activeEditSession.setScrollLeft(@scrollLeft()) + + # Toggle soft tabs on the edit session. + toggleSoftTabs: -> + @activeEditSession.setSoftTabs(not @activeEditSession.getSoftTabs()) + + # Toggle soft wrap on the edit session. + toggleSoftWrap: -> + @setWidthInChars() + @activeEditSession.setSoftWrap(not @activeEditSession.getSoftWrap()) + + calculateWidthInChars: -> + Math.floor(@scrollView.width() / @charWidth) + + calculateHeightInLines: -> + Math.ceil($(window).height() / @lineHeight) + + # Enables/disables soft wrap on the editor. + # + # softWrap - A {Boolean} which, if `true`, enables soft wrap + setSoftWrap: (softWrap) -> + if softWrap + @addClass 'soft-wrap' + @scrollLeft(0) + else + @removeClass 'soft-wrap' + + # Sets the font size for the editor. + # + # fontSize - A {Number} indicating the font size in pixels. + setFontSize: (fontSize) -> + @css('font-size', "#{fontSize}px}") + + @clearCharacterWidthCache() + + if @isOnDom() + @redraw() + else + @redrawOnReattach = @attached + + # Retrieves the font size for the editor. + # + # Returns a {Number} indicating the font size in pixels. + getFontSize: -> + parseInt(@css("font-size")) + + # Sets the font family for the editor. + # + # fontFamily - A {String} identifying the CSS `font-family`, + setFontFamily: (fontFamily='') -> + @css('font-family', fontFamily) + + @clearCharacterWidthCache() + + @redraw() + + # Gets the font family for the editor. + # + # Returns a {String} identifying the CSS `font-family`, + getFontFamily: -> @css("font-family") + + # Redraw the editor + redraw: -> + return unless @hasParent() + return unless @attached + @redrawOnReattach = false + @calculateDimensions() + @updatePaddingOfRenderedLines() + @updateLayerDimensions() + @requestDisplayUpdate() + + splitLeft: -> + pane = @getPane() + pane?.splitLeft(pane?.copyActiveItem()).activeView + + splitRight: -> + pane = @getPane() + pane?.splitRight(pane?.copyActiveItem()).activeView + + splitUp: -> + pane = @getPane() + pane?.splitUp(pane?.copyActiveItem()).activeView + + splitDown: -> + pane = @getPane() + pane?.splitDown(pane?.copyActiveItem()).activeView + + # Retrieve's the `EditorView`'s pane. + # + # Returns a {Pane}. + getPane: -> + @parent('.item-views').parent('.pane').view() + + remove: (selector, keepData) -> + return super if keepData or @removed + super + rootView?.focus() + + beforeRemove: -> + @trigger 'editor:will-be-removed' + @removed = true + @activeEditSession?.destroy() + $(window).off(".editor-#{@id}") + $(document).off(".editor-#{@id}") + + getCursorView: (index) -> + index ?= @cursorViews.length - 1 + @cursorViews[index] + + getCursorViews: -> + new Array(@cursorViews...) + + addCursorView: (cursor, options) -> + cursorView = new CursorView(cursor, this, options) + @cursorViews.push(cursorView) + @overlayer.append(cursorView) + cursorView + + removeCursorView: (cursorView) -> + _.remove(@cursorViews, cursorView) + + getSelectionView: (index) -> + index ?= @selectionViews.length - 1 + @selectionViews[index] + + getSelectionViews: -> + new Array(@selectionViews...) + + addSelectionView: (selection) -> + selectionView = new SelectionView({editorView: this, selection}) + @selectionViews.push(selectionView) + @underlayer.append(selectionView) + selectionView + + removeSelectionView: (selectionView) -> + _.remove(@selectionViews, selectionView) + + removeAllCursorAndSelectionViews: -> + cursorView.remove() for cursorView in @getCursorViews() + selectionView.remove() for selectionView in @getSelectionViews() + + appendToLinesView: (view) -> + @overlayer.append(view) + + ### Internal ### + + # Scrolls the editor vertically to a given position. + scrollVertically: (pixelPosition, {center}={}) -> + scrollViewHeight = @scrollView.height() + scrollTop = @scrollTop() + scrollBottom = scrollTop + scrollViewHeight + + if center + unless scrollTop < pixelPosition.top < scrollBottom + @scrollTop(pixelPosition.top - (scrollViewHeight / 2)) + else + linesInView = @scrollView.height() / @lineHeight + maxScrollMargin = Math.floor((linesInView - 1) / 2) + scrollMargin = Math.min(@vScrollMargin, maxScrollMargin) + margin = scrollMargin * @lineHeight + desiredTop = pixelPosition.top - margin + desiredBottom = pixelPosition.top + @lineHeight + margin + if desiredBottom > scrollBottom + @scrollTop(desiredBottom - scrollViewHeight) + else if desiredTop < scrollTop + @scrollTop(desiredTop) + + # Scrolls the editor horizontally to a given position. + scrollHorizontally: (pixelPosition) -> + return if @activeEditSession.getSoftWrap() + + charsInView = @scrollView.width() / @charWidth + maxScrollMargin = Math.floor((charsInView - 1) / 2) + scrollMargin = Math.min(@hScrollMargin, maxScrollMargin) + margin = scrollMargin * @charWidth + desiredRight = pixelPosition.left + @charWidth + margin + desiredLeft = pixelPosition.left - margin + + if desiredRight > @scrollRight() + @scrollRight(desiredRight) + else if desiredLeft < @scrollLeft() + @scrollLeft(desiredLeft) + @saveScrollPositionForActiveEditSession() + + calculateDimensions: -> + fragment = $('') + @renderedLines.append(fragment) + + lineRect = fragment[0].getBoundingClientRect() + charRect = fragment.find('span')[0].getBoundingClientRect() + @lineHeight = lineRect.height + @charWidth = charRect.width + @charHeight = charRect.height + fragment.remove() + @setHeightInLines() + + updateLayerDimensions: -> + height = @lineHeight * @getScreenLineCount() + unless @layerHeight == height + @layerHeight = height + @underlayer.height(@layerHeight) + @renderedLines.height(@layerHeight) + @overlayer.height(@layerHeight) + @verticalScrollbarContent.height(@layerHeight) + @scrollBottom(height) if @scrollBottom() > height + + minWidth = Math.max(@charWidth * @getMaxScreenLineLength() + 20, @scrollView.width()) + unless @layerMinWidth == minWidth + @renderedLines.css('min-width', minWidth) + @underlayer.css('min-width', minWidth) + @overlayer.css('min-width', minWidth) + @layerMinWidth = minWidth + @trigger 'editor:min-width-changed' + + # Override for speed. The base function checks computedStyle, unnecessary here. + isHidden: -> + style = this[0].style + if style.display == 'none' or not @isOnDom() + true + else + false + + clearRenderedLines: -> + @renderedLines.empty() + @firstRenderedScreenRow = null + @lastRenderedScreenRow = null + + resetDisplay: -> + return unless @attached + + @clearRenderedLines() + @removeAllCursorAndSelectionViews() + editorScrollTop = @activeEditSession.getScrollTop() ? 0 + editorScrollLeft = @activeEditSession.getScrollLeft() ? 0 + @updateLayerDimensions() + @scrollTop(editorScrollTop) + @scrollLeft(editorScrollLeft) + @setSoftWrap(@activeEditSession.getSoftWrap()) + @newCursors = @activeEditSession.getAllCursors() + @newSelections = @activeEditSession.getAllSelections() + @updateDisplay(suppressAutoScroll: true) + + requestDisplayUpdate: -> + return if @pendingDisplayUpdate + return unless @isVisible() + @pendingDisplayUpdate = true + setImmediate => + @updateDisplay() + @pendingDisplayUpdate = false + + updateDisplay: (options={}) -> + return unless @attached and @activeEditSession + return if @activeEditSession.destroyed + unless @isOnDom() and @isVisible() + @redrawOnReattach = true + return + + @updateRenderedLines() + @highlightCursorLine() + @updateCursorViews() + @updateSelectionViews() + @autoscroll(options) + @trigger 'editor:display-updated' + + updateCursorViews: -> + if @newCursors.length > 0 + @addCursorView(cursor) for cursor in @newCursors when not cursor.destroyed + @syncCursorAnimations() + @newCursors = [] + + for cursorView in @getCursorViews() + if cursorView.needsRemoval + cursorView.remove() + else if @shouldUpdateCursor(cursorView) + cursorView.updateDisplay() + + shouldUpdateCursor: (cursorView) -> + return false unless cursorView.needsUpdate + + pos = cursorView.getScreenPosition() + pos.row >= @firstRenderedScreenRow and pos.row <= @lastRenderedScreenRow + + updateSelectionViews: -> + if @newSelections.length > 0 + @addSelectionView(selection) for selection in @newSelections when not selection.destroyed + @newSelections = [] + + for selectionView in @getSelectionViews() + if selectionView.needsRemoval + selectionView.remove() + else if @shouldUpdateSelection(selectionView) + selectionView.updateDisplay() + + shouldUpdateSelection: (selectionView) -> + screenRange = selectionView.getScreenRange() + startRow = screenRange.start.row + endRow = screenRange.end.row + (startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or # startRow in range + (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) or # endRow in range + (startRow <= @firstRenderedScreenRow and endRow >= @lastRenderedScreenRow) # selection surrounds the rendered items + + syncCursorAnimations: -> + for cursorView in @getCursorViews() + do (cursorView) -> cursorView.resetBlinking() + + autoscroll: (options={}) -> + for cursorView in @getCursorViews() + if !options.suppressAutoScroll and cursorView.needsAutoscroll() + @scrollToPixelPosition(cursorView.getPixelPosition()) + cursorView.clearAutoscroll() + + for selectionView in @getSelectionViews() + if !options.suppressAutoScroll and selectionView.needsAutoscroll() + @scrollToPixelPosition(selectionView.getCenterPixelPosition(), center: true) + selectionView.highlight() + selectionView.clearAutoscroll() + + updateRenderedLines: -> + firstVisibleScreenRow = @getFirstVisibleScreenRow() + lastScreenRowToRender = firstVisibleScreenRow + @heightInLines - 1 + lastScreenRow = @getLastScreenRow() + + if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastScreenRowToRender <= @lastRenderedScreenRow + renderFrom = Math.min(lastScreenRow, @firstRenderedScreenRow) + renderTo = Math.min(lastScreenRow, @lastRenderedScreenRow) + else + renderFrom = Math.min(lastScreenRow, Math.max(0, firstVisibleScreenRow - @lineOverdraw)) + renderTo = Math.min(lastScreenRow, lastScreenRowToRender + @lineOverdraw) + + if @pendingChanges.length == 0 and @firstRenderedScreenRow and @firstRenderedScreenRow <= renderFrom and renderTo <= @lastRenderedScreenRow + return + + changes = @pendingChanges + intactRanges = @computeIntactRanges(renderFrom, renderTo) + + @gutter.updateLineNumbers(changes, renderFrom, renderTo) + + @clearDirtyRanges(intactRanges) + @fillDirtyRanges(intactRanges, renderFrom, renderTo) + @firstRenderedScreenRow = renderFrom + @lastRenderedScreenRow = renderTo + @updateLayerDimensions() + @updatePaddingOfRenderedLines() + + computeSurroundingEmptyLineChanges: (change) -> + emptyLineChanges = [] + + if change.bufferDelta? + afterStart = change.end + change.bufferDelta + 1 + if @lineForBufferRow(afterStart) is '' + afterEnd = afterStart + afterEnd++ while @lineForBufferRow(afterEnd + 1) is '' + emptyLineChanges.push({start: afterStart, end: afterEnd, screenDelta: 0}) + + beforeEnd = change.start - 1 + if @lineForBufferRow(beforeEnd) is '' + beforeStart = beforeEnd + beforeStart-- while @lineForBufferRow(beforeStart - 1) is '' + emptyLineChanges.push({start: beforeStart, end: beforeEnd, screenDelta: 0}) + + emptyLineChanges + + computeIntactRanges: (renderFrom, renderTo) -> + return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow? + + intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}] + + if not @mini and @showIndentGuide + emptyLineChanges = [] + for change in @pendingChanges + emptyLineChanges.push(@computeSurroundingEmptyLineChanges(change)...) + @pendingChanges.push(emptyLineChanges...) + + for change in @pendingChanges + newIntactRanges = [] + for range in intactRanges + if change.end < range.start and change.screenDelta != 0 + newIntactRanges.push( + start: range.start + change.screenDelta + end: range.end + change.screenDelta + domStart: range.domStart + ) + else if change.end < range.start or change.start > range.end + newIntactRanges.push(range) + else + if change.start > range.start + newIntactRanges.push( + start: range.start + end: change.start - 1 + domStart: range.domStart) + if change.end < range.end + newIntactRanges.push( + start: change.end + change.screenDelta + 1 + end: range.end + change.screenDelta + domStart: range.domStart + change.end + 1 - range.start + ) + intactRanges = newIntactRanges + + @truncateIntactRanges(intactRanges, renderFrom, renderTo) + + @pendingChanges = [] + + intactRanges + + truncateIntactRanges: (intactRanges, renderFrom, renderTo) -> + i = 0 + while i < intactRanges.length + range = intactRanges[i] + if range.start < renderFrom + range.domStart += renderFrom - range.start + range.start = renderFrom + if range.end > renderTo + range.end = renderTo + if range.start >= range.end + intactRanges.splice(i--, 1) + i++ + intactRanges.sort (a, b) -> a.domStart - b.domStart + + clearDirtyRanges: (intactRanges) -> + if intactRanges.length == 0 + @renderedLines[0].innerHTML = '' + else if currentLine = @renderedLines[0].firstChild + domPosition = 0 + for intactRange in intactRanges + while intactRange.domStart > domPosition + currentLine = @clearLine(currentLine) + domPosition++ + for i in [intactRange.start..intactRange.end] + currentLine = currentLine.nextSibling + domPosition++ + while currentLine + currentLine = @clearLine(currentLine) + + clearLine: (lineElement) -> + next = lineElement.nextSibling + @renderedLines[0].removeChild(lineElement) + next + + fillDirtyRanges: (intactRanges, renderFrom, renderTo) -> + i = 0 + nextIntact = intactRanges[i] + currentLine = @renderedLines[0].firstChild + + row = renderFrom + while row <= renderTo + if row == nextIntact?.end + 1 + nextIntact = intactRanges[++i] + + if !nextIntact or row < nextIntact.start + if nextIntact + dirtyRangeEnd = nextIntact.start - 1 + else + dirtyRangeEnd = renderTo + + for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd) + @renderedLines[0].insertBefore(lineElement, currentLine) + row++ + else + currentLine = currentLine.nextSibling + row++ + + updatePaddingOfRenderedLines: -> + paddingTop = @firstRenderedScreenRow * @lineHeight + @renderedLines.css('padding-top', paddingTop) + @gutter.lineNumbers.css('padding-top', paddingTop) + + paddingBottom = (@getLastScreenRow() - @lastRenderedScreenRow) * @lineHeight + @renderedLines.css('padding-bottom', paddingBottom) + @gutter.lineNumbers.css('padding-bottom', paddingBottom) + + ### Public ### + + # Retrieves the number of the row that is visible and currently at the top of the editor. + # + # Returns a {Number}. + getFirstVisibleScreenRow: -> + screenRow = Math.floor(@scrollTop() / @lineHeight) + screenRow = 0 if isNaN(screenRow) + screenRow + + # Retrieves the number of the row that is visible and currently at the bottom of the editor. + # + # Returns a {Number}. + getLastVisibleScreenRow: -> + calculatedRow = Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1 + screenRow = Math.max(0, Math.min(@getScreenLineCount() - 1, calculatedRow)) + screenRow = 0 if isNaN(screenRow) + screenRow + + # Given a row number, identifies if it is currently visible. + # + # row - A row {Number} to check + # + # Returns a {Boolean}. + isScreenRowVisible: (row) -> + @getFirstVisibleScreenRow() <= row <= @getLastVisibleScreenRow() + + ### Internal ### + + handleScreenLinesChange: (change) -> + @pendingChanges.push(change) + @requestDisplayUpdate() + + buildLineElementForScreenRow: (screenRow) -> + @buildLineElementsForScreenRows(screenRow, screenRow)[0] + + buildLineElementsForScreenRows: (startRow, endRow) -> + div = document.createElement('div') + div.innerHTML = @htmlForScreenRows(startRow, endRow) + new Array(div.children...) + + htmlForScreenRows: (startRow, endRow) -> + htmlLines = '' + screenRow = startRow + for line in @activeEditSession.linesForScreenRows(startRow, endRow) + htmlLines += @htmlForScreenLine(line, screenRow++) + htmlLines + + htmlForScreenLine: (screenLine, screenRow) -> + { tokens, text, lineEnding, fold, isSoftWrapped } = screenLine + if fold + attributes = { class: 'fold line', 'fold-id': fold.id } + else + attributes = { class: 'line' } + + invisibles = @invisibles if @showInvisibles + eolInvisibles = @getEndOfLineInvisibles(screenLine) + htmlEolInvisibles = @buildHtmlEndOfLineInvisibles(screenLine) + + indentation = EditorView.buildIndentation(screenRow, @activeEditSession) + + EditorView.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, @showIndentGuide, indentation, @activeEditSession, @mini}) + + @buildIndentation: (screenRow, activeEditSession) -> + bufferRow = activeEditSession.bufferPositionForScreenPosition([screenRow]).row + bufferLine = activeEditSession.lineForBufferRow(bufferRow) + if bufferLine is '' + indentation = 0 + nextRow = screenRow + 1 + while nextRow < activeEditSession.getBuffer().getLineCount() + bufferRow = activeEditSession.bufferPositionForScreenPosition([nextRow]).row + bufferLine = activeEditSession.lineForBufferRow(bufferRow) + if bufferLine isnt '' + indentation = Math.ceil(activeEditSession.indentLevelForLine(bufferLine)) + break + nextRow++ + + previousRow = screenRow - 1 + while previousRow >= 0 + bufferRow = activeEditSession.bufferPositionForScreenPosition([previousRow]).row + bufferLine = activeEditSession.lineForBufferRow(bufferRow) + if bufferLine isnt '' + indentation = Math.max(indentation, Math.ceil(activeEditSession.indentLevelForLine(bufferLine))) + break + previousRow-- + + indentation + else + Math.ceil(activeEditSession.indentLevelForLine(bufferLine)) + + buildHtmlEndOfLineInvisibles: (screenLine) -> + invisibles = [] + for invisible in @getEndOfLineInvisibles(screenLine) + invisibles.push("#{invisible}") + invisibles.join('') + + getEndOfLineInvisibles: (screenLine) -> + return [] unless @showInvisibles and @invisibles + return [] if @mini or screenLine.isSoftWrapped() + + invisibles = [] + invisibles.push(@invisibles.cr) if @invisibles.cr and screenLine.lineEnding is '\r\n' + invisibles.push(@invisibles.eol) if @invisibles.eol + invisibles + + lineElementForScreenRow: (screenRow) -> + @renderedLines.children(":eq(#{screenRow - @firstRenderedScreenRow})") + + toggleLineCommentsInSelection: -> + @activeEditSession.toggleLineCommentsInSelection() + + ### Public ### + + # Converts a buffer position to a pixel position. + # + # position - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # + # Returns an object with two values: `top` and `left`, representing the pixel positions. + pixelPositionForBufferPosition: (position) -> + @pixelPositionForScreenPosition(@screenPositionForBufferPosition(position)) + + # Converts a screen position to a pixel position. + # + # position - An object that represents a screen position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # + # Returns an object with two values: `top` and `left`, representing the pixel positions. + pixelPositionForScreenPosition: (position) -> + return { top: 0, left: 0 } unless @isOnDom() and @isVisible() + {row, column} = Point.fromObject(position) + actualRow = Math.floor(row) + + lineElement = existingLineElement = @lineElementForScreenRow(actualRow)[0] + unless existingLineElement + lineElement = @buildLineElementForScreenRow(actualRow) + @renderedLines.append(lineElement) + left = @positionLeftForLineAndColumn(lineElement, actualRow, column) + unless existingLineElement + @renderedLines[0].removeChild(lineElement) + { top: row * @lineHeight, left } + + positionLeftForLineAndColumn: (lineElement, screenRow, column) -> + return 0 if column == 0 + + bufferRow = @bufferRowsForScreenRows(screenRow, screenRow)[0] ? screenRow + tokenizedLine = @activeEditSession.displayBuffer.tokenizedBuffer.tokenizedLines[bufferRow] + + left = 0 + index = 0 + for token in tokenizedLine.tokens + for char in token.value + return left if index >= column + + val = @getCharacterWidthCache(token.scopes, char) + if val? + left += val + else + return @measureToColumn(lineElement, tokenizedLine, column) + + index++ + left + + scopesForColumn: (tokenizedLine, column) -> + index = 0 + for token in tokenizedLine.tokens + for char in token.value + return token.scopes if index == column + index++ + null + + measureToColumn: (lineElement, tokenizedLine, column) -> + left = oldLeft = index = 0 + iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFilter) + + returnLeft = null + + offsetLeft = @scrollView.offset().left + paddingLeft = parseInt(@scrollView.css('padding-left')) + + while textNode = iterator.nextNode() + content = textNode.textContent + + for char, i in content + # Don't continue caching long lines :racehorse: + break if index > LongLineLength and column < index + + # Dont return right away, finish caching the whole line + returnLeft = left if index == column + oldLeft = left + + scopes = @scopesForColumn(tokenizedLine, index) + cachedCharWidth = @getCharacterWidthCache(scopes, char) + + if cachedCharWidth? + left = oldLeft + cachedCharWidth + else + # i + 1 to measure to the end of the current character + MeasureRange.setEnd(textNode, i + 1) + MeasureRange.collapse() + rects = MeasureRange.getClientRects() + return 0 if rects.length == 0 + left = rects[0].left - Math.floor(offsetLeft) + Math.floor(@scrollLeft()) - paddingLeft + + if scopes? + cachedCharWidth = left - oldLeft + @setCharacterWidthCache(scopes, char, cachedCharWidth) + + # Assume all the characters are the same width when dealing with long + # lines :racehorse: + return column * cachedCharWidth if index > LongLineLength + + index++ + + returnLeft ? left + + getCharacterWidthCache: (scopes, char) -> + scopes ?= NoScope + obj = EditorView.characterWidthCache + for scope in scopes + obj = obj[scope] + return null unless obj? + obj[char] + + setCharacterWidthCache: (scopes, char, val) -> + scopes ?= NoScope + obj = EditorView.characterWidthCache + for scope in scopes + obj[scope] ?= {} + obj = obj[scope] + obj[char] = val + + clearCharacterWidthCache: -> + EditorView.characterWidthCache = {} + + pixelOffsetForScreenPosition: (position) -> + {top, left} = @pixelPositionForScreenPosition(position) + offset = @renderedLines.offset() + {top: top + offset.top, left: left + offset.left} + + screenPositionFromMouseEvent: (e) -> + { pageX, pageY } = e + offset = @scrollView.offset() + + editorRelativeTop = pageY - offset.top + @scrollTop() + row = Math.floor(editorRelativeTop / @lineHeight) + column = 0 + + if pageX > offset.left and lineElement = @lineElementForScreenRow(row)[0] + range = document.createRange() + iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT) + while node = iterator.nextNode() + range.selectNodeContents(node) + column += node.textContent.length + {left, right} = range.getClientRects()[0] + break if left <= pageX <= right + + if node + for characterPosition in [node.textContent.length...0] + range.setStart(node, characterPosition - 1) + range.setEnd(node, characterPosition) + {left, right, width} = range.getClientRects()[0] + break if left <= pageX - width / 2 <= right + column-- + + range.detach() + + new Point(row, column) + + # Highlights the current line the cursor is on. + highlightCursorLine: -> + return if @mini + + @highlightedLine?.removeClass('cursor-line') + if @getSelection().isEmpty() + @highlightedLine = @lineElementForScreenRow(@getCursorScreenRow()) + @highlightedLine.addClass('cursor-line') + else + @highlightedLine = null + + # {Delegates to: Editor.getGrammar} + getGrammar: -> + @activeEditSession.getGrammar() + + # {Delegates to: Editor.setGrammar} + setGrammar: (grammar) -> + throw new Error("Only mini-editors can explicity set their grammar") unless @mini + @activeEditSession.setGrammar(grammar) + + # {Delegates to: Editor.reloadGrammar} + reloadGrammar: -> + @activeEditSession.reloadGrammar() + + # {Delegates to: Editor.scopesForBufferPosition} + scopesForBufferPosition: (bufferPosition) -> + @activeEditSession.scopesForBufferPosition(bufferPosition) + + # Copies the current file path to the native clipboard. + copyPathToPasteboard: -> + path = @getPath() + atom.pasteboard.write(path) if path? + + ### Internal ### + + @buildLineHtml: ({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, showIndentGuide, indentation, activeEditSession, mini}) -> + scopeStack = [] + line = [] + + attributePairs = '' + attributePairs += " #{attributeName}=\"#{value}\"" for attributeName, value of attributes + line.push("
") + + if text == '' + html = EditorView.buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) + line.push(html) if html + else + firstNonWhitespacePosition = text.search(/\S/) + firstTrailingWhitespacePosition = text.search(/\s*$/) + lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 + position = 0 + for token in tokens + @updateScopeStack(line, scopeStack, token.scopes) + hasLeadingWhitespace = position < firstNonWhitespacePosition + hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition + hasIndentGuide = not mini and showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly) + line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})) + position += token.value.length + + @popScope(line, scopeStack) while scopeStack.length > 0 + line.push(htmlEolInvisibles) unless text == '' + line.push("") if fold + + line.push('
') + line.join('') + + @updateScopeStack: (line, scopeStack, desiredScopes) -> + excessScopes = scopeStack.length - desiredScopes.length + if excessScopes > 0 + @popScope(line, scopeStack) while excessScopes-- + + # pop until common prefix + for i in [scopeStack.length..0] + break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) + @popScope(line, scopeStack) + + # push on top of common prefix until scopeStack == desiredScopes + for j in [i...desiredScopes.length] + @pushScope(line, scopeStack, desiredScopes[j]) + + null + + @pushScope: (line, scopeStack, scope) -> + scopeStack.push(scope) + line.push("") + + @popScope: (line, scopeStack) -> + scopeStack.pop() + line.push("") + + @buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) -> + indentCharIndex = 0 + if not mini and showIndentGuide + if indentation > 0 + tabLength = activeEditSession.getTabLength() + indentGuideHtml = '' + for level in [0...indentation] + indentLevelHtml = "" + for characterPosition in [0...tabLength] + if invisible = eolInvisibles[indentCharIndex++] + indentLevelHtml += "#{invisible}" + else + indentLevelHtml += ' ' + indentLevelHtml += "" + indentGuideHtml += indentLevelHtml + + while indentCharIndex < eolInvisibles.length + indentGuideHtml += "#{eolInvisibles[indentCharIndex++]}" + + return indentGuideHtml + + if htmlEolInvisibles.length > 0 + htmlEolInvisibles + else + ' ' + + replaceSelectedText: (replaceFn) -> + selection = @getSelection() + return false if selection.isEmpty() + + text = replaceFn(@getTextInRange(selection.getBufferRange())) + return false if text is null or text is undefined + + @insertText(text, select: true) + true + + consolidateSelections: (e) -> e.abortKeyBinding() unless @activeEditSession.consolidateSelections() + + logCursorScope: -> + console.log @activeEditSession.getCursorScopes() + + transact: (fn) -> @activeEditSession.transact(fn) + beginTransaction: -> @activeEditSession.beginTransaction() + commitTransaction: -> @activeEditSession.commitTransaction() + abortTransaction: -> @activeEditSession.abortTransaction() + + saveDebugSnapshot: -> + atom.showSaveDialog (path) => + fs.writeFileSync(path, @getDebugSnapshot()) if path + + getDebugSnapshot: -> + [ + "Debug Snapshot: #{@getPath()}" + @getRenderedLinesDebugSnapshot() + @activeEditSession.getDebugSnapshot() + @getBuffer().getDebugSnapshot() + ].join('\n\n') + + getRenderedLinesDebugSnapshot: -> + lines = ['Rendered Lines:'] + firstRenderedScreenRow = @firstRenderedScreenRow + @renderedLines.find('.line').each (n) -> + lines.push "#{firstRenderedScreenRow + n}: #{$(this).text()}" + lines.join('\n') + + logScreenLines: (start, end) -> + @activeEditSession.logScreenLines(start, end) + + logRenderedLines: -> + @renderedLines.find('.line').each (n) -> + console.log n, $(this).text() diff --git a/src/editor.coffee b/src/editor.coffee index ed14e6e9a..f000ea225 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1,1853 +1,1445 @@ -{View, $, $$} = require './space-pen-extensions' -TextBuffer = require './text-buffer' -Gutter = require './gutter' -{Point, Range} = require 'telepath' -EditSession = require './edit-session' -CursorView = require './cursor-view' -SelectionView = require './selection-view' -fs = require 'fs-plus' _ = require 'underscore-plus' +path = require 'path' +telepath = require 'telepath' +guid = require 'guid' +{Point, Range} = telepath +LanguageMode = require './language-mode' +DisplayBuffer = require './display-buffer' +Cursor = require './cursor' +Selection = require './selection' +{Emitter, Subscriber} = require 'emissary' +TextMateScopeSelector = require('first-mate').ScopeSelector -MeasureRange = document.createRange() -TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT } -NoScope = ['no-scope'] -LongLineLength = 1000 - -# Private: Represents the entire visual pane in Atom. +# Public: The core model of Atom. # -# The Editor manages the {EditSession}, which manages the file buffers. +# An {Editor} represents a unique view of each document, with it's own +# {Cursor}s and scroll position. +# +# For instance if a user creates a split, Atom creates a second {Editor} +# but both {Editor}s interact with the same buffer underlying buffer. So +# if you type in either buffer it immediately appears in both but if you scroll +# in one it doesn't scroll the other. +# +# Almost all extension will interact primiarily with this class as it provides +# access to objects you'll most commonly interact with. To access it you'll +# want to register a callback on {RootView} which will be fired once for every +# existing {Editor} as well as any future {Editor}s. +# +# ## Example +# ```coffeescript +# global.rootView.eachEditSession (editor) -> +# editor.insertText('Hello World') +# ``` +# +# ## Collaboration builtin +# +# FIXME: Describe how there are both local and remote cursors and selections and +# why that is. module.exports = -class Editor extends View - @characterWidthCache: {} - @configDefaults: - fontSize: 20 - showInvisibles: false - showIndentGuide: false - showLineNumbers: true - autoIndent: true - normalizeIndentOnPaste: true - nonWordCharacters: "./\\()\"':,.;<>~!@#$%^&*|+=[]{}`~?-" - preferredLineLength: 80 - tabLength: 2 - softWrap: false - softTabs: true - softWrapAtPreferredLineLength: false +class Editor + Emitter.includeInto(this) + Subscriber.includeInto(this) - @nextEditorId: 1 + @acceptsDocuments: true - ### Internal ### + registerDeserializer(this) - @content: (params) -> - attributes = { class: @classes(params), tabindex: -1 } - _.extend(attributes, params.attributes) if params.attributes - @div attributes, => - @subview 'gutter', new Gutter - @div class: 'scroll-view', outlet: 'scrollView', => - @div class: 'overlayer', outlet: 'overlayer' - @div class: 'lines', outlet: 'renderedLines' - @div class: 'underlayer', outlet: 'underlayer', => - @input class: 'hidden-input', outlet: 'hiddenInput' - @div class: 'vertical-scrollbar', outlet: 'verticalScrollbar', => - @div outlet: 'verticalScrollbarContent' + @version: 5 - @classes: ({mini} = {}) -> - classes = ['editor', 'editor-colors'] - classes.push 'mini' if mini - classes.join(' ') + @deserialize: (state) -> + new Editor(state) - vScrollMargin: 2 - hScrollMargin: 10 - lineHeight: null - charWidth: null - charHeight: null - cursorViews: null - selectionViews: null - lineCache: null - isFocused: false - activeEditSession: null - attached: false - lineOverdraw: 10 - pendingChanges: null - newCursors: null - newSelections: null - redrawOnReattach: false - bottomPaddingInLines: 10 - - ### Public ### - - # The constructor for setting up an `Editor` instance. - # - # editSessionOrOptions - Either an {EditSession}, or an object with one property, `mini`. - # If `mini` is `true`, a "miniature" `EditSession` is constructed. - # Typically, this is ideal for scenarios where you need an Atom editor, - # but without all the chrome, like scrollbars, gutter, _e.t.c._. - # - initialize: (editSessionOrOptions) -> - if editSessionOrOptions instanceof EditSession - editSession = editSessionOrOptions - else - {editSession, @mini} = editSessionOrOptions ? {} - - @id = Editor.nextEditorId++ - @lineCache = [] - @configure() - @bindKeys() - @handleEvents() - @handleInputEvents() - @cursorViews = [] - @selectionViews = [] - @pendingChanges = [] - @newCursors = [] - @newSelections = [] - - if editSession? - @edit(editSession) - else if @mini - @edit(new EditSession - buffer: TextBuffer.createAsRoot() - softWrap: false - tabLength: 2 - softTabs: true - ) - else - throw new Error("Must supply an EditSession or mini: true") - - # Internal: Sets up the core Atom commands. - # - # Some commands are excluded from mini-editors. - bindKeys: -> - editorBindings = - 'core:move-left': @moveCursorLeft - 'core:move-right': @moveCursorRight - 'core:select-left': @selectLeft - 'core:select-right': @selectRight - 'core:select-all': @selectAll - 'core:backspace': @backspace - 'core:delete': @delete - 'core:undo': @undo - 'core:redo': @redo - 'core:cut': @cutSelection - 'core:copy': @copySelection - 'core:paste': @paste - 'editor:move-to-previous-word': @moveCursorToPreviousWord - 'editor:select-word': @selectWord - 'editor:consolidate-selections': @consolidateSelections - 'editor:backspace-to-beginning-of-word': @backspaceToBeginningOfWord - 'editor:backspace-to-beginning-of-line': @backspaceToBeginningOfLine - 'editor:delete-to-end-of-word': @deleteToEndOfWord - 'editor:delete-line': @deleteLine - 'editor:cut-to-end-of-line': @cutToEndOfLine - 'editor:move-to-beginning-of-line': @moveCursorToBeginningOfLine - 'editor:move-to-end-of-line': @moveCursorToEndOfLine - 'editor:move-to-first-character-of-line': @moveCursorToFirstCharacterOfLine - 'editor:move-to-beginning-of-word': @moveCursorToBeginningOfWord - 'editor:move-to-end-of-word': @moveCursorToEndOfWord - 'editor:move-to-beginning-of-next-word': @moveCursorToBeginningOfNextWord - 'editor:move-to-previous-word-boundary': @moveCursorToPreviousWordBoundary - 'editor:move-to-next-word-boundary': @moveCursorToNextWordBoundary - 'editor:select-to-end-of-line': @selectToEndOfLine - 'editor:select-to-beginning-of-line': @selectToBeginningOfLine - 'editor:select-to-end-of-word': @selectToEndOfWord - 'editor:select-to-beginning-of-word': @selectToBeginningOfWord - 'editor:select-to-beginning-of-next-word': @selectToBeginningOfNextWord - 'editor:select-to-next-word-boundary': @selectToNextWordBoundary - 'editor:select-to-previous-word-boundary': @selectToPreviousWordBoundary - 'editor:select-to-first-character-of-line': @selectToFirstCharacterOfLine - 'editor:select-line': @selectLine - 'editor:transpose': @transpose - 'editor:upper-case': @upperCase - 'editor:lower-case': @lowerCase - - unless @mini - _.extend editorBindings, - 'core:move-up': @moveCursorUp - 'core:move-down': @moveCursorDown - 'core:move-to-top': @moveCursorToTop - 'core:move-to-bottom': @moveCursorToBottom - 'core:page-down': @pageDown - 'core:page-up': @pageUp - 'core:select-up': @selectUp - 'core:select-down': @selectDown - 'core:select-to-top': @selectToTop - 'core:select-to-bottom': @selectToBottom - 'editor:indent': @indent - 'editor:auto-indent': @autoIndent - 'editor:indent-selected-rows': @indentSelectedRows - 'editor:outdent-selected-rows': @outdentSelectedRows - 'editor:newline': @insertNewline - 'editor:newline-below': @insertNewlineBelow - 'editor:newline-above': @insertNewlineAbove - 'editor:add-selection-below': @addSelectionBelow - 'editor:add-selection-above': @addSelectionAbove - 'editor:toggle-soft-tabs': @toggleSoftTabs - 'editor:toggle-soft-wrap': @toggleSoftWrap - 'editor:fold-all': @foldAll - 'editor:unfold-all': @unfoldAll - 'editor:fold-current-row': @foldCurrentRow - 'editor:unfold-current-row': @unfoldCurrentRow - 'editor:fold-selection': @foldSelection - 'editor:fold-at-indent-level-1': => @foldAllAtIndentLevel(0) - 'editor:fold-at-indent-level-2': => @foldAllAtIndentLevel(1) - 'editor:fold-at-indent-level-3': => @foldAllAtIndentLevel(2) - 'editor:fold-at-indent-level-4': => @foldAllAtIndentLevel(3) - 'editor:fold-at-indent-level-5': => @foldAllAtIndentLevel(4) - 'editor:fold-at-indent-level-6': => @foldAllAtIndentLevel(5) - 'editor:fold-at-indent-level-7': => @foldAllAtIndentLevel(6) - 'editor:fold-at-indent-level-8': => @foldAllAtIndentLevel(7) - 'editor:fold-at-indent-level-9': => @foldAllAtIndentLevel(8) - 'editor:toggle-line-comments': @toggleLineCommentsInSelection - 'editor:log-cursor-scope': @logCursorScope - 'editor:checkout-head-revision': @checkoutHead - 'editor:copy-path': @copyPathToPasteboard - 'editor:move-line-up': @moveLineUp - 'editor:move-line-down': @moveLineDown - 'editor:duplicate-line': @duplicateLine - 'editor:join-line': @joinLine - 'editor:toggle-indent-guide': => atom.config.toggle('editor.showIndentGuide') - 'editor:save-debug-snapshot': @saveDebugSnapshot - 'editor:toggle-line-numbers': => atom.config.toggle('editor.showLineNumbers') - 'editor:scroll-to-cursor': @scrollToCursorPosition - - documentation = {} - for name, method of editorBindings - do (name, method) => - @command name, (e) => method.call(this, e); false - - # {Delegates to: EditSession.getCursor} - getCursor: -> @activeEditSession.getCursor() - - # {Delegates to: EditSession.getCursors} - getCursors: -> @activeEditSession.getCursors() - - # {Delegates to: EditSession.addCursorAtScreenPosition} - addCursorAtScreenPosition: (screenPosition) -> @activeEditSession.addCursorAtScreenPosition(screenPosition) - - # {Delegates to: EditSession.addCursorAtBufferPosition} - addCursorAtBufferPosition: (bufferPosition) -> @activeEditSession.addCursorAtBufferPosition(bufferPosition) - - # {Delegates to: EditSession.moveCursorUp} - moveCursorUp: -> @activeEditSession.moveCursorUp() - - # {Delegates to: EditSession.moveCursorDown} - moveCursorDown: -> @activeEditSession.moveCursorDown() - - # {Delegates to: EditSession.moveCursorLeft} - moveCursorLeft: -> @activeEditSession.moveCursorLeft() - - # {Delegates to: EditSession.moveCursorRight} - moveCursorRight: -> @activeEditSession.moveCursorRight() - - # {Delegates to: EditSession.moveCursorToBeginningOfWord} - moveCursorToBeginningOfWord: -> @activeEditSession.moveCursorToBeginningOfWord() - - # {Delegates to: EditSession.moveCursorToEndOfWord} - moveCursorToEndOfWord: -> @activeEditSession.moveCursorToEndOfWord() - - # {Delegates to: EditSession.moveCursorToBeginningOfNextWord} - moveCursorToBeginningOfNextWord: -> @activeEditSession.moveCursorToBeginningOfNextWord() - - # {Delegates to: EditSession.moveCursorToTop} - moveCursorToTop: -> @activeEditSession.moveCursorToTop() - - # {Delegates to: EditSession.moveCursorToBottom} - moveCursorToBottom: -> @activeEditSession.moveCursorToBottom() - - # {Delegates to: EditSession.moveCursorToBeginningOfLine} - moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine() - - # {Delegates to: EditSession.moveCursorToFirstCharacterOfLine} - moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine() - - # {Delegates to: EditSession.moveCursorToPreviousWordBoundary} - moveCursorToPreviousWordBoundary: -> @activeEditSession.moveCursorToPreviousWordBoundary() - - # {Delegates to: EditSession.moveCursorToNextWordBoundary} - moveCursorToNextWordBoundary: -> @activeEditSession.moveCursorToNextWordBoundary() - - # {Delegates to: EditSession.moveCursorToEndOfLine} - moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine() - - # {Delegates to: EditSession.moveLineUp} - moveLineUp: -> @activeEditSession.moveLineUp() - - # {Delegates to: EditSession.moveLineDown} - moveLineDown: -> @activeEditSession.moveLineDown() - - # {Delegates to: EditSession.setCursorScreenPosition} - setCursorScreenPosition: (position, options) -> @activeEditSession.setCursorScreenPosition(position, options) - - # {Delegates to: EditSession.duplicateLine} - duplicateLine: -> @activeEditSession.duplicateLine() - - # {Delegates to: EditSession.joinLine} - joinLine: -> @activeEditSession.joinLine() - - # {Delegates to: EditSession.getCursorScreenPosition} - getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition() - - # {Delegates to: EditSession.getCursorScreenRow} - getCursorScreenRow: -> @activeEditSession.getCursorScreenRow() - - # {Delegates to: EditSession.setCursorBufferPosition} - setCursorBufferPosition: (position, options) -> @activeEditSession.setCursorBufferPosition(position, options) - - # {Delegates to: EditSession.getCursorBufferPosition} - getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition() - - # {Delegates to: EditSession.getCurrentParagraphBufferRange} - getCurrentParagraphBufferRange: -> @activeEditSession.getCurrentParagraphBufferRange() - - # {Delegates to: EditSession.getWordUnderCursor} - getWordUnderCursor: (options) -> @activeEditSession.getWordUnderCursor(options) - - # {Delegates to: EditSession.getSelection} - getSelection: (index) -> @activeEditSession.getSelection(index) - - # {Delegates to: EditSession.getSelections} - getSelections: -> @activeEditSession.getSelections() - - # {Delegates to: EditSession.getSelectionsOrderedByBufferPosition} - getSelectionsOrderedByBufferPosition: -> @activeEditSession.getSelectionsOrderedByBufferPosition() - - # {Delegates to: EditSession.getLastSelectionInBuffer} - getLastSelectionInBuffer: -> @activeEditSession.getLastSelectionInBuffer() - - # {Delegates to: EditSession.getSelectedText} - getSelectedText: -> @activeEditSession.getSelectedText() - - # {Delegates to: EditSession.getSelectedBufferRanges} - getSelectedBufferRanges: -> @activeEditSession.getSelectedBufferRanges() - - # {Delegates to: EditSession.getSelectedBufferRange} - getSelectedBufferRange: -> @activeEditSession.getSelectedBufferRange() - - # {Delegates to: EditSession.setSelectedBufferRange} - setSelectedBufferRange: (bufferRange, options) -> @activeEditSession.setSelectedBufferRange(bufferRange, options) - - # {Delegates to: EditSession.setSelectedBufferRanges} - setSelectedBufferRanges: (bufferRanges, options) -> @activeEditSession.setSelectedBufferRanges(bufferRanges, options) - - # {Delegates to: EditSession.addSelectionForBufferRange} - addSelectionForBufferRange: (bufferRange, options) -> @activeEditSession.addSelectionForBufferRange(bufferRange, options) - - # {Delegates to: EditSession.selectRight} - selectRight: -> @activeEditSession.selectRight() - - # {Delegates to: EditSession.selectLeft} - selectLeft: -> @activeEditSession.selectLeft() - - # {Delegates to: EditSession.selectUp} - selectUp: -> @activeEditSession.selectUp() - - # {Delegates to: EditSession.selectDown} - selectDown: -> @activeEditSession.selectDown() - - # {Delegates to: EditSession.selectToTop} - selectToTop: -> @activeEditSession.selectToTop() - - # {Delegates to: EditSession.selectToBottom} - selectToBottom: -> @activeEditSession.selectToBottom() - - # {Delegates to: EditSession.selectAll} - selectAll: -> @activeEditSession.selectAll() - - # {Delegates to: EditSession.selectToBeginningOfLine} - selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine() - - # {Delegates to: EditSession.selectToFirstCharacterOfLine} - selectToFirstCharacterOfLine: -> @activeEditSession.selectToFirstCharacterOfLine() - - # {Delegates to: EditSession.selectToEndOfLine} - selectToEndOfLine: -> @activeEditSession.selectToEndOfLine() - - # {Delegates to: EditSession.selectToPreviousWordBoundary} - selectToPreviousWordBoundary: -> @activeEditSession.selectToPreviousWordBoundary() - - # {Delegates to: EditSession.selectToNextWordBoundary} - selectToNextWordBoundary: -> @activeEditSession.selectToNextWordBoundary() - - # {Delegates to: EditSession.addSelectionBelow} - addSelectionBelow: -> @activeEditSession.addSelectionBelow() - - # {Delegates to: EditSession.addSelectionAbove} - addSelectionAbove: -> @activeEditSession.addSelectionAbove() - - # {Delegates to: EditSession.selectToBeginningOfWord} - selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord() - - # {Delegates to: EditSession.selectToEndOfWord} - selectToEndOfWord: -> @activeEditSession.selectToEndOfWord() - - # {Delegates to: EditSession.selectToBeginningOfNextWord} - selectToBeginningOfNextWord: -> @activeEditSession.selectToBeginningOfNextWord() - - # {Delegates to: EditSession.selectWord} - selectWord: -> @activeEditSession.selectWord() - - # {Delegates to: EditSession.selectLine} - selectLine: -> @activeEditSession.selectLine() - - # {Delegates to: EditSession.selectToScreenPosition} - selectToScreenPosition: (position) -> @activeEditSession.selectToScreenPosition(position) - - # {Delegates to: EditSession.transpose} - transpose: -> @activeEditSession.transpose() - - # {Delegates to: EditSession.upperCase} - upperCase: -> @activeEditSession.upperCase() - - # {Delegates to: EditSession.lowerCase} - lowerCase: -> @activeEditSession.lowerCase() - - # {Delegates to: EditSession.clearSelections} - clearSelections: -> @activeEditSession.clearSelections() - - # {Delegates to: EditSession.backspace} - backspace: -> @activeEditSession.backspace() - - # {Delegates to: EditSession.backspaceToBeginningOfWord} - backspaceToBeginningOfWord: -> @activeEditSession.backspaceToBeginningOfWord() - - # {Delegates to: EditSession.backspaceToBeginningOfLine} - backspaceToBeginningOfLine: -> @activeEditSession.backspaceToBeginningOfLine() - - # {Delegates to: EditSession.delete} - delete: -> @activeEditSession.delete() - - # {Delegates to: EditSession.deleteToEndOfWord} - deleteToEndOfWord: -> @activeEditSession.deleteToEndOfWord() - - # {Delegates to: EditSession.deleteLine} - deleteLine: -> @activeEditSession.deleteLine() - - # {Delegates to: EditSession.cutToEndOfLine} - cutToEndOfLine: -> @activeEditSession.cutToEndOfLine() - - # {Delegates to: EditSession.insertText} - insertText: (text, options) -> @activeEditSession.insertText(text, options) - - # {Delegates to: EditSession.insertNewline} - insertNewline: -> @activeEditSession.insertNewline() - - # {Delegates to: EditSession.insertNewlineBelow} - insertNewlineBelow: -> @activeEditSession.insertNewlineBelow() - - # {Delegates to: EditSession.insertNewlineAbove} - insertNewlineAbove: -> @activeEditSession.insertNewlineAbove() - - # {Delegates to: EditSession.indent} - indent: (options) -> @activeEditSession.indent(options) - - # {Delegates to: EditSession.autoIndentSelectedRows} - autoIndent: (options) -> @activeEditSession.autoIndentSelectedRows() - - # {Delegates to: EditSession.indentSelectedRows} - indentSelectedRows: -> @activeEditSession.indentSelectedRows() - - # {Delegates to: EditSession.outdentSelectedRows} - outdentSelectedRows: -> @activeEditSession.outdentSelectedRows() - - # {Delegates to: EditSession.cutSelectedText} - cutSelection: -> @activeEditSession.cutSelectedText() - - # {Delegates to: EditSession.copySelectedText} - copySelection: -> @activeEditSession.copySelectedText() - - # {Delegates to: EditSession.pasteText} - paste: (options) -> @activeEditSession.pasteText(options) - - # {Delegates to: EditSession.undo} - undo: -> @activeEditSession.undo() - - # {Delegates to: EditSession.redo} - redo: -> @activeEditSession.redo() - - # {Delegates to: EditSession.createFold} - createFold: (startRow, endRow) -> @activeEditSession.createFold(startRow, endRow) - - # {Delegates to: EditSession.foldCurrentRow} - foldCurrentRow: -> @activeEditSession.foldCurrentRow() - - # {Delegates to: EditSession.unfoldCurrentRow} - unfoldCurrentRow: -> @activeEditSession.unfoldCurrentRow() - - # {Delegates to: EditSession.foldAll} - foldAll: -> @activeEditSession.foldAll() - - # {Delegates to: EditSession.unfoldAll} - unfoldAll: -> @activeEditSession.unfoldAll() - - # {Delegates to: EditSession.foldSelection} - foldSelection: -> @activeEditSession.foldSelection() - - # {Delegates to: EditSession.destroyFoldsContainingBufferRow} - destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow) - - # {Delegates to: EditSession.isFoldedAtScreenRow} - isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow) - - # {Delegates to: EditSession.isFoldedAtBufferRow} - isFoldedAtBufferRow: (bufferRow) -> @activeEditSession.isFoldedAtBufferRow(bufferRow) - - # {Delegates to: EditSession.isFoldedAtCursorRow} - isFoldedAtCursorRow: -> @activeEditSession.isFoldedAtCursorRow() - - foldAllAtIndentLevel: (indentLevel) -> @activeEditSession.foldAllAtIndentLevel(indentLevel) - - # {Delegates to: EditSession.lineForScreenRow} - lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow) - - # {Delegates to: EditSession.linesForScreenRows} - linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end) - - # {Delegates to: EditSession.getScreenLineCount} - getScreenLineCount: -> @activeEditSession.getScreenLineCount() + id: null + languageMode: null + displayBuffer: null + cursors: null + remoteCursors: null + selections: null + remoteSelections: null + suppressSelectionMerging: false # Private: - setHeightInLines: (heightInLines)-> - heightInLines ?= @calculateHeightInLines() - @heightInLines = heightInLines if heightInLines + constructor: (optionsOrState) -> + @cursors = [] + @remoteCursors = [] + @selections = [] + @remoteSelections = [] + if optionsOrState instanceof telepath.Document + @state = optionsOrState + @id = @state.get('id') + displayBuffer = deserialize(@state.get('displayBuffer')) + @setBuffer(displayBuffer.buffer) + @setDisplayBuffer(displayBuffer) + for marker in @findMarkers(@getSelectionMarkerAttributes()) + marker.setAttributes(preserveFolds: true) + @addSelection(marker) + @setScrollTop(@state.get('scrollTop')) + @setScrollLeft(@state.get('scrollLeft')) + registerEditSession = true + else + {buffer, displayBuffer, tabLength, softTabs, softWrap, suppressCursorCreation, initialLine} = optionsOrState + @id = guid.create().toString() + displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrap}) + @state = atom.site.createDocument + deserializer: @constructor.name + version: @constructor.version + id: @id + displayBuffer: displayBuffer.getState() + softTabs: buffer.usesSoftTabs() ? softTabs ? atom.config.get('editor.softTabs') ? true + scrollTop: 0 + scrollLeft: 0 + @setBuffer(buffer) + @setDisplayBuffer(displayBuffer) - # {Delegates to: EditSession.setEditorWidthInChars} - setWidthInChars: (widthInChars) -> - widthInChars ?= @calculateWidthInChars() - @activeEditSession.setEditorWidthInChars(widthInChars) if widthInChars - - # {Delegates to: EditSession.getMaxScreenLineLength} - getMaxScreenLineLength: -> @activeEditSession.getMaxScreenLineLength() - - # {Delegates to: EditSession.getLastScreenRow} - getLastScreenRow: -> @activeEditSession.getLastScreenRow() - - # {Delegates to: EditSession.clipScreenPosition} - clipScreenPosition: (screenPosition, options={}) -> @activeEditSession.clipScreenPosition(screenPosition, options) - - # {Delegates to: EditSession.screenPositionForBufferPosition} - screenPositionForBufferPosition: (position, options) -> @activeEditSession.screenPositionForBufferPosition(position, options) - - # {Delegates to: EditSession.bufferPositionForScreenPosition} - bufferPositionForScreenPosition: (position, options) -> @activeEditSession.bufferPositionForScreenPosition(position, options) - - # {Delegates to: EditSession.screenRangeForBufferRange} - screenRangeForBufferRange: (range) -> @activeEditSession.screenRangeForBufferRange(range) - - # {Delegates to: EditSession.bufferRangeForScreenRange} - bufferRangeForScreenRange: (range) -> @activeEditSession.bufferRangeForScreenRange(range) - - # {Delegates to: EditSession.bufferRowsForScreenRows} - bufferRowsForScreenRows: (startRow, endRow) -> @activeEditSession.bufferRowsForScreenRows(startRow, endRow) - - # Public: Emulates the "page down" key, where the last row of a buffer scrolls to become the first. - pageDown: -> - newScrollTop = @scrollTop() + @scrollView[0].clientHeight - @activeEditSession.moveCursorDown(@getPageRows()) - @scrollTop(newScrollTop, adjustVerticalScrollbar: true) - - # Public: Emulates the "page up" key, where the frst row of a buffer scrolls to become the last. - pageUp: -> - newScrollTop = @scrollTop() - @scrollView[0].clientHeight - @activeEditSession.moveCursorUp(@getPageRows()) - @scrollTop(newScrollTop, adjustVerticalScrollbar: true) - - # Gets the number of actual page rows existing in an editor. - # - # Returns a {Number}. - getPageRows: -> - Math.max(1, Math.ceil(@scrollView[0].clientHeight / @lineHeight)) - - # Set whether invisible characters are shown. - # - # showInvisibles - A {Boolean} which, if `true`, show invisible characters - setShowInvisibles: (showInvisibles) -> - return if showInvisibles == @showInvisibles - @showInvisibles = showInvisibles - @resetDisplay() - - # Defines which characters are invisible. - # - # invisibles - A hash defining the invisible characters: The defaults are: - # eol: `\u00ac` - # space: `\u00b7` - # tab: `\u00bb` - # cr: `\u00a4` - setInvisibles: (@invisibles={}) -> - _.defaults @invisibles, - eol: '\u00ac' - space: '\u00b7' - tab: '\u00bb' - cr: '\u00a4' - @resetDisplay() - - # Sets whether you want to show the indentation guides. - # - # showIndentGuide - A {Boolean} you can set to `true` if you want to see the indentation guides. - setShowIndentGuide: (showIndentGuide) -> - return if showIndentGuide == @showIndentGuide - @showIndentGuide = showIndentGuide - @resetDisplay() - - # Checkout the HEAD revision of this editor's file. - checkoutHead: -> - if path = @getPath() - atom.project.getRepo()?.checkoutHead(path) - - # {Delegates to: EditSession.setText} - setText: (text) -> @activeEditSession.setText(text) - - # {Delegates to: EditSession.getText} - getText: -> @activeEditSession.getText() - - # {Delegates to: EditSession.getPath} - getPath: -> @activeEditSession?.getPath() - - # {Delegates to: TextBuffer.getLineCount} - getLineCount: -> @getBuffer().getLineCount() - - # {Delegates to: TextBuffer.getLastRow} - getLastBufferRow: -> @getBuffer().getLastRow() - - # {Delegates to: TextBuffer.getTextInRange} - getTextInRange: (range) -> @getBuffer().getTextInRange(range) - - # {Delegates to: TextBuffer.getEofPosition} - getEofPosition: -> @getBuffer().getEofPosition() - - # {Delegates to: TextBuffer.lineForRow} - lineForBufferRow: (row) -> @getBuffer().lineForRow(row) - - # {Delegates to: TextBuffer.lineLengthForRow} - lineLengthForBufferRow: (row) -> @getBuffer().lineLengthForRow(row) - - # {Delegates to: TextBuffer.rangeForRow} - rangeForBufferRow: (row) -> @getBuffer().rangeForRow(row) - - # {Delegates to: TextBuffer.scanInRange} - scanInBufferRange: (args...) -> @getBuffer().scanInRange(args...) - - # {Delegates to: TextBuffer.backwardsScanInRange} - backwardsScanInBufferRange: (args...) -> @getBuffer().backwardsScanInRange(args...) - - ### Internal ### - - configure: -> - @observeConfig 'editor.showLineNumbers', (showLineNumbers) => @gutter.setShowLineNumbers(showLineNumbers) - @observeConfig 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles) - @observeConfig 'editor.showIndentGuide', (showIndentGuide) => @setShowIndentGuide(showIndentGuide) - @observeConfig 'editor.invisibles', (invisibles) => @setInvisibles(invisibles) - @observeConfig 'editor.fontSize', (fontSize) => @setFontSize(fontSize) - @observeConfig 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily) - - handleEvents: -> - @on 'focus', => - @hiddenInput.focus() - false - - @hiddenInput.on 'focus', => - @bringHiddenInputIntoView() - @isFocused = true - @addClass 'is-focused' - - @hiddenInput.on 'focusout', => - @bringHiddenInputIntoView() - @isFocused = false - @removeClass 'is-focused' - - @underlayer.on 'mousedown', (e) => - @renderedLines.trigger(e) - false if @isFocused - - @overlayer.on 'mousedown', (e) => - @overlayer.hide() - clickedElement = document.elementFromPoint(e.pageX, e.pageY) - @overlayer.show() - e.target = clickedElement - $(clickedElement).trigger(e) - false if @isFocused - - @renderedLines.on 'mousedown', '.fold.line', (e) => - id = $(e.currentTarget).attr('fold-id') - marker = @activeEditSession.displayBuffer.getMarker(id) - @activeEditSession.setCursorBufferPosition(marker.getBufferRange().start) - @activeEditSession.destroyFoldWithId(id) - false - - @renderedLines.on 'mousedown', (e) => - clickCount = e.originalEvent.detail - - screenPosition = @screenPositionFromMouseEvent(e) - if clickCount == 1 - if e.metaKey - @addCursorAtScreenPosition(screenPosition) - else if e.shiftKey - @selectToScreenPosition(screenPosition) - else - @setCursorScreenPosition(screenPosition) - else if clickCount == 2 - @activeEditSession.selectWord() unless e.shiftKey - else if clickCount == 3 - @activeEditSession.selectLine() unless e.shiftKey - - @selectOnMousemoveUntilMouseup() unless e.ctrlKey or e.originalEvent.which > 1 - - unless @mini - @scrollView.on 'mousewheel', (e) => - if delta = e.originalEvent.wheelDeltaY - @scrollTop(@scrollTop() - delta) - false - - @verticalScrollbar.on 'scroll', => - @scrollTop(@verticalScrollbar.scrollTop(), adjustVerticalScrollbar: false) - - @scrollView.on 'scroll', => - if @scrollLeft() == 0 - @gutter.removeClass('drop-shadow') + if @getCursors().length is 0 and not suppressCursorCreation + if initialLine + position = [initialLine, 0] else - @gutter.addClass('drop-shadow') + position = _.last(@getRemoteCursors())?.getBufferPosition() ? [0, 0] + @addCursorAtBufferPosition(position) - # Listen for overflow events to detect when the editor's width changes - # to update the soft wrap column. - updateWidthInChars = _.debounce((=> @setWidthInChars()), 100) - @scrollView.on 'overflowchanged', => - updateWidthInChars() if @[0].classList.contains('soft-wrap') + @languageMode = new LanguageMode(this, @buffer.getExtension()) + @subscribe @state, 'changed', ({newValues}) => + for key, newValue of newValues + switch key + when 'scrollTop' + @emit 'scroll-top-changed', newValue + when 'scrollLeft' + @emit 'scroll-left-changed', newValue - handleInputEvents: -> - @on 'cursor:moved', => - return unless @isFocused - cursorView = @getCursorView() + project.addEditSession(this) if registerEditSession - if cursorView.isVisible() - # This is an order of magnitude faster than checking .offset(). - style = cursorView[0].style - @hiddenInput[0].style.top = style.top - @hiddenInput[0].style.left = style.left + # Private: + setBuffer: (@buffer) -> + @buffer.retain() + @subscribe @buffer, "path-changed", => + project.setPath(path.dirname(@getPath())) unless project.getPath()? + @emit "title-changed" + @emit "path-changed" + @subscribe @buffer, "contents-modified", => @emit "contents-modified" + @subscribe @buffer, "contents-conflicted", => @emit "contents-conflicted" + @subscribe @buffer, "modified-status-changed", => @emit "modified-status-changed" + @preserveCursorPositionOnBufferReload() - selectedText = null - @hiddenInput.on 'compositionstart', => - selectedText = @getSelectedText() - @hiddenInput.css('width', '100%') - @hiddenInput.on 'compositionupdate', (e) => - @insertText(e.originalEvent.data, {select: true, undo: 'skip'}) - @hiddenInput.on 'compositionend', => - @insertText(selectedText, {select: true, undo: 'skip'}) - @hiddenInput.css('width', '1px') + # Private: + setDisplayBuffer: (@displayBuffer) -> + @subscribe @displayBuffer, 'marker-created', @handleMarkerCreated + @subscribe @displayBuffer, "changed", (e) => @emit 'screen-lines-changed', e + @subscribe @displayBuffer, "markers-updated", => @mergeIntersectingSelections() + @subscribe @displayBuffer, 'grammar-changed', => @handleGrammarChange() + @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... - lastInput = '' - @on "textInput", (e) => - # Work around of the accented character suggestion feature in OS X. - selectedLength = @hiddenInput[0].selectionEnd - @hiddenInput[0].selectionStart - if selectedLength is 1 and lastInput is @hiddenInput.val() - @selectLeft() + # Private: + getViewClass: -> + require './editor-view' - lastInput = e.originalEvent.data - @insertText(lastInput) - @hiddenInput.val(lastInput) - false + # Private: + destroy: -> + return if @destroyed + @destroyed = true + @unsubscribe() + selection.destroy() for selection in @getSelections() + @buffer.release() + @displayBuffer.destroy() + @languageMode.destroy() + project?.removeEditSession(this) + @emit 'destroyed' + @off() - bringHiddenInputIntoView: -> - @hiddenInput.css(top: @scrollTop(), left: @scrollLeft()) + # Private: + serialize: -> @state.clone() - selectOnMousemoveUntilMouseup: -> - lastMoveEvent = null - moveHandler = (event = lastMoveEvent) => - if event - @selectToScreenPosition(@screenPositionFromMouseEvent(event)) - lastMoveEvent = event + # Private: + getState: -> @state - $(document).on "mousemove.editor-#{@id}", moveHandler - interval = setInterval(moveHandler, 20) + # Private: Creates an {Editor} with the same initial state + copy: -> + tabLength = @getTabLength() + displayBuffer = @displayBuffer.copy() + softTabs = @getSoftTabs() + newEditSession = new Editor({@buffer, displayBuffer, tabLength, softTabs, suppressCursorCreation: true}) + newEditSession.setScrollTop(@getScrollTop()) + newEditSession.setScrollLeft(@getScrollLeft()) + for marker in @findMarkers(editorId: @id) + marker.copy(editorId: newEditSession.id, preserveFolds: true) + project.addEditSession(newEditSession) + newEditSession - $(document).one "mouseup.editor-#{@id}", => - clearInterval(interval) - $(document).off 'mousemove', moveHandler - @activeEditSession.mergeIntersectingSelections(isReversed: @activeEditSession.getLastSelection().isReversed()) - @activeEditSession.finalizeSelections() - @syncCursorAnimations() - - afterAttach: (onDom) -> - return unless onDom - @redraw() if @redrawOnReattach - return if @attached - @attached = true - @calculateDimensions() - @setWidthInChars() - @subscribe $(window), "resize.editor-#{@id}", => - @setHeightInLines() - @setWidthInChars() - @updateLayerDimensions() - @requestDisplayUpdate() - @focus() if @isFocused - - if pane = @getPane() - @active = @is(pane.activeView) - @subscribe pane, 'pane:active-item-changed', (event, item) => - wasActive = @active - @active = @is(pane.activeView) - @redraw() if @active and not wasActive - - @resetDisplay() - - @trigger 'editor:attached', [this] - - edit: (editSession) -> - return if editSession is @activeEditSession - - if @activeEditSession - @saveScrollPositionForActiveEditSession() - @activeEditSession.off(".editor") - - @activeEditSession = editSession - - return unless @activeEditSession? - - @activeEditSession.setVisible(true) - - @activeEditSession.on "contents-conflicted.editor", => - @showBufferConflictAlert(@activeEditSession) - - @activeEditSession.on "path-changed.editor", => - @reloadGrammar() - @trigger 'editor:path-changed' - - @activeEditSession.on "grammar-changed.editor", => - @trigger 'editor:grammar-changed' - - @activeEditSession.on 'selection-added.editor', (selection) => - @newCursors.push(selection.cursor) - @newSelections.push(selection) - @requestDisplayUpdate() - - @activeEditSession.on 'screen-lines-changed.editor', (e) => - @handleScreenLinesChange(e) - - @activeEditSession.on 'scroll-top-changed.editor', (scrollTop) => - @scrollTop(scrollTop) - - @activeEditSession.on 'scroll-left-changed.editor', (scrollLeft) => - @scrollLeft(scrollLeft) - - @activeEditSession.on 'soft-wrap-changed.editor', (softWrap) => - @setSoftWrap(softWrap) - - @trigger 'editor:path-changed' - @resetDisplay() - - if @attached and @activeEditSession.buffer.isInConflict() - _.defer => @showBufferConflictAlert(@activeEditSession) # Display after editSession has a chance to display - - getModel: -> - @activeEditSession - - setModel: (editSession) -> - @edit(editSession) - - showBufferConflictAlert: (editSession) -> - atom.confirm( - editSession.getPath(), - "Has changed on disk. Do you want to reload it?", - "Reload", (=> editSession.buffer.reload()), - "Cancel" - ) - - scrollTop: (scrollTop, options={}) -> - return @cachedScrollTop or 0 unless scrollTop? - maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height() - scrollTop = Math.floor(Math.max(0, Math.min(maxScrollTop, scrollTop))) - return if scrollTop == @cachedScrollTop - @cachedScrollTop = scrollTop - - @updateDisplay() if @attached - - @renderedLines.css('top', -scrollTop) - @underlayer.css('top', -scrollTop) - @overlayer.css('top', -scrollTop) - @gutter.lineNumbers.css('top', -scrollTop) - - if options?.adjustVerticalScrollbar ? true - @verticalScrollbar.scrollTop(scrollTop) - @activeEditSession.setScrollTop(@scrollTop()) - - scrollBottom: (scrollBottom) -> - if scrollBottom? - @scrollTop(scrollBottom - @scrollView.height()) + # Public: Retrieves the filename of the open file. + # + # This is `'untitled'` if the file is new and not saved to the disk. + # + # Returns a {String}. + getTitle: -> + if sessionPath = @getPath() + path.basename(sessionPath) else - @scrollTop() + @scrollView.height() + 'untitled' - scrollLeft: (scrollLeft) -> - if scrollLeft? - @scrollView.scrollLeft(scrollLeft) - @activeEditSession.setScrollLeft(@scrollLeft()) + # Public: Retrieves the filename and path of the open file. + # + # It has the follows the following format, ` - `. If the + # file is brand new, the title is `untitled`. + # + # Returns a {String}. + getLongTitle: -> + if sessionPath = @getPath() + fileName = path.basename(sessionPath) + directory = path.basename(path.dirname(sessionPath)) + "#{fileName} - #{directory}" else - @scrollView.scrollLeft() + 'untitled' - scrollRight: (scrollRight) -> - if scrollRight? - @scrollView.scrollRight(scrollRight) - @activeEditSession.setScrollLeft(@scrollLeft()) + # Public: Compares two `Editor`s to determine equality. + # + # Equality is based on the condition that: + # + # * the two {TextBuffer}s are the same + # * the two `scrollTop` and `scrollLeft` property are the same + # * the two {Cursor} screen positions are the same + # + # Returns a {Boolean}. + isEqual: (other) -> + return false unless other instanceof Editor + @buffer == other.buffer and + @getScrollTop() == other.getScrollTop() and + @getScrollLeft() == other.getScrollLeft() and + @getCursorScreenPosition().isEqual(other.getCursorScreenPosition()) + + # Public: Controls visiblity based on the given Boolean. + setVisible: (visible) -> @displayBuffer.setVisible(visible) + + # Public: FIXME: I don't understand this. + setScrollTop: (scrollTop) -> @state.set('scrollTop', scrollTop) + + # Public: Returns the current `scrollTop` value + getScrollTop: -> @state.get('scrollTop') ? 0 + + # Public: FIXME: I don't understand this. + setScrollLeft: (scrollLeft) -> @state.set('scrollLeft', scrollLeft) + + # Public: Returns the current `scrollLeft` value + getScrollLeft: -> @state.get('scrollLeft') + + # Set the number of characters that can be displayed horizontally in the + # editor that contains this edit session. + # + # editorWidthInChars - A {Number} of characters + setEditorWidthInChars: (editorWidthInChars) -> + @displayBuffer.setEditorWidthInChars(editorWidthInChars) + + # Public: Sets the column at which columsn will soft wrap + getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() + + # Public: Returns whether soft tabs are enabled or not. + getSoftTabs: -> @state.get('softTabs') + + # Public: Controls whether soft tabs are enabled or not. + setSoftTabs: (softTabs) -> + @state.set('softTabs', softTabs) + + # Public: Returns whether soft wrap is enabled or not. + getSoftWrap: -> @displayBuffer.getSoftWrap() + + # Public: Controls whether soft tabs are enabled or not. + setSoftWrap: (softWrap) -> @displayBuffer.setSoftWrap(softWrap) + + # Public: Returns that String used to indicate a tab. + # + # If soft tabs are enabled, this is a space (`" "`) times the {.getTabLength} value. + # Otherwise, it's a tab (`\t`). + getTabText: -> @buildIndentString(1) + + # Public: Returns the current tab length. + getTabLength: -> @displayBuffer.getTabLength() + + # Public: Sets the current tab length. + setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) + + # Public: Given a position, this clips it to a real position. + # + # For example, if `position`'s row exceeds the row count of the buffer, + # or if its column goes beyond a line's length, this "sanitizes" the value + # to a real position. + # + # * position: + # The {Point} to clip + # + # Returns the new, clipped {Point}. Note that this could be the same as + # `position` if no clipping was performed. + clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) + + # Public: Given a range, this clips it to a real range. + # + # For example, if `range`'s row exceeds the row count of the buffer, + # or if its column goes beyond a line's length, this "sanitizes" the value + # to a real range. + # + # * range: + # The {Range} to clip + # + # Returns the new, clipped {Range}. Note that this could be the same as + # `range` if no clipping was performed. + clipBufferRange: (range) -> @buffer.clipRange(range) + + # Public: Returns the indentation level of the given a buffer row + # + # * bufferRow: + # A Number indicating the buffer row. + indentationForBufferRow: (bufferRow) -> + @indentLevelForLine(@lineForBufferRow(bufferRow)) + + # Public: Sets the indentation level for the given buffer row. + # + # * bufferRow: + # A {Number} indicating the buffer row. + # * newLevel: + # A {Number} indicating the new indentation level. + setIndentationForBufferRow: (bufferRow, newLevel) -> + currentLevel = @indentationForBufferRow(bufferRow) + currentIndentString = @buildIndentString(currentLevel) + newIndentString = @buildIndentString(newLevel) + @buffer.change([[bufferRow, 0], [bufferRow, currentIndentString.length]], newIndentString) + + # Public: Returns the indentation level of the given line of text. + # + # * line: + # A {String} in the current buffer. + # + # Returns a {Number} or 0 if the text isn't found within the buffer. + indentLevelForLine: (line) -> + if match = line.match(/^[\t ]+/) + leadingWhitespace = match[0] + tabCount = leadingWhitespace.match(/\t/g)?.length ? 0 + spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0 + tabCount + (spaceCount / @getTabLength()) else - @scrollView.scrollRight() + 0 - ### Public ### + # Private: Constructs the string used for tabs. + buildIndentString: (number) -> + if @getSoftTabs() + _.multiplyString(" ", number * @getTabLength()) + else + _.multiplyString("\t", Math.floor(number)) - # Retrieves the {EditSession}'s buffer. + # {Delegates to: TextBuffer.save} + save: -> @buffer.save() + + # {Delegates to: TextBuffer.saveAs} + saveAs: (path) -> @buffer.saveAs(path) + + # {Delegates to: TextBuffer.getExtension} + getFileExtension: -> @buffer.getExtension() + + # {Delegates to: TextBuffer.getPath} + getPath: -> @buffer.getPath() + + # {Delegates to: TextBuffer.getText} + getText: -> @buffer.getText() + + # {Delegates to: TextBuffer.setText} + setText: (text) -> @buffer.setText(text) + + # Private: Retrieves the current {TextBuffer}. + getBuffer: -> @buffer + + # Public: Retrieves the current buffer's URI. + getUri: -> @buffer.getUri() + + # {Delegates to: TextBuffer.isRowBlank} + isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) + + # Public: Determine if the given row is entirely a comment + isBufferRowCommented: (bufferRow) -> + if match = @lineForBufferRow(bufferRow).match(/\S/) + scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes + new TextMateScopeSelector('comment.*').matches(scopes) + + # {Delegates to: TextBuffer.nextNonBlankRow} + nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) + + # {Delegates to: TextBuffer.getEofPosition} + getEofBufferPosition: -> @buffer.getEofPosition() + + # {Delegates to: TextBuffer.getLastRow} + getLastBufferRow: -> @buffer.getLastRow() + + # {Delegates to: TextBuffer.rangeForRow} + bufferRangeForBufferRow: (row, options) -> @buffer.rangeForRow(row, options) + + # {Delegates to: TextBuffer.lineForRow} + lineForBufferRow: (row) -> @buffer.lineForRow(row) + + # {Delegates to: TextBuffer.lineLengthForRow} + lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row) + + # {Delegates to: TextBuffer.scan} + scan: (args...) -> @buffer.scan(args...) + + # {Delegates to: TextBuffer.scanInRange} + scanInBufferRange: (args...) -> @buffer.scanInRange(args...) + + # {Delegates to: TextBuffer.backwardsScanInRange} + backwardsScanInBufferRange: (args...) -> @buffer.backwardsScanInRange(args...) + + # {Delegates to: TextBuffer.isModified} + isModified: -> @buffer.isModified() + + # Public: Determines if the user should be prompted to save before closing. + shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() + + # {Delegates to: DisplayBuffer.screenPositionForBufferPosition} + screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options) + + # {Delegates to: DisplayBuffer.bufferPositionForScreenPosition} + bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options) + + # {Delegates to: DisplayBuffer.screenRangeForBufferRange} + screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange) + + # {Delegates to: DisplayBuffer.bufferRangeForScreenRange} + bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange) + + # {Delegates to: DisplayBuffer.clipScreenPosition} + clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options) + + # {Delegates to: DisplayBuffer.lineForRow} + lineForScreenRow: (row) -> @displayBuffer.lineForRow(row) + + # {Delegates to: DisplayBuffer.linesForRows} + linesForScreenRows: (start, end) -> @displayBuffer.linesForRows(start, end) + + # {Delegates to: DisplayBuffer.getLineCount} + getScreenLineCount: -> @displayBuffer.getLineCount() + + # {Delegates to: DisplayBuffer.getMaxLineLength} + getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength() + + # {Delegates to: DisplayBuffer.getLastRow} + getLastScreenRow: -> @displayBuffer.getLastRow() + + # {Delegates to: DisplayBuffer.bufferRowsForScreenRows} + bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow) + + # {Delegates to: DisplayBuffer.scopesForBufferPosition} + scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) + + # Public: ? + bufferRangeForScopeAtCursor: (selector) -> + @displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition()) + + # {Delegates to: DisplayBuffer.tokenForBufferPosition} + tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) + + # Public: Retrieves the grammar's token scopes for the line with the most + # recently added cursor. # - # Returns the current {TextBuffer}. - getBuffer: -> @activeEditSession.buffer + # Returns an {Array} of {String}s. + getCursorScopes: -> @getCursor().getScopes() - # Scrolls the editor to the bottom. - scrollToBottom: -> - @scrollBottom(@getScreenLineCount() * @lineHeight) - - # Scrolls the editor to the position of the most recently added cursor. + # Public: Inserts text at the current cursor positions # - # The editor is also centered. - scrollToCursorPosition: -> - @scrollToBufferPosition(@getCursorBufferPosition(), center: true) + # * text: + # A String representing the text to insert. + # * options: + # + A set of options equivalent to {Selection.insertText} + insertText: (text, options={}) -> + options.autoIndentNewline ?= @shouldAutoIndent() + options.autoDecreaseIndent ?= @shouldAutoIndent() + @mutateSelectedText (selection) -> selection.insertText(text, options) - # Scrolls the editor to the given buffer position. - # - # bufferPosition - An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # options - A hash matching the options available to {.scrollToPixelPosition} - scrollToBufferPosition: (bufferPosition, options) -> - @scrollToPixelPosition(@pixelPositionForBufferPosition(bufferPosition), options) + # Public: Inserts a new line at the current cursor positions. + insertNewline: -> + @insertText('\n') - # Scrolls the editor to the given screen position. - # - # screenPosition - An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # options - A hash matching the options available to {.scrollToPixelPosition} - scrollToScreenPosition: (screenPosition, options) -> - @scrollToPixelPosition(@pixelPositionForScreenPosition(screenPosition), options) + # Public: Inserts a new line below the current cursor positions. + insertNewlineBelow: -> + @transact => + @moveCursorToEndOfLine() + @insertNewline() - # Scrolls the editor to the given pixel position. - # - # pixelPosition - An object that represents a pixel position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # options - A hash with the following keys: - # center: if `true`, the position is scrolled such that it's in the center of the editor - scrollToPixelPosition: (pixelPosition, options) -> - return unless @attached - @scrollVertically(pixelPosition, options) - @scrollHorizontally(pixelPosition) + # Public: Inserts a new line above the current cursor positions. + insertNewlineAbove: -> + @transact => + onFirstLine = @getCursorBufferPosition().row is 0 + @moveCursorToBeginningOfLine() + @moveCursorLeft() + @insertNewline() + @moveCursorUp() if onFirstLine - # Highlight all the folds within the given buffer range. + # Public: Indents the current line. # - # "Highlighting" essentially just adds the `fold-selected` class to the line's - # DOM element. - # - # bufferRange - The {Range} to check. - highlightFoldsContainingBufferRange: (bufferRange) -> - screenLines = @linesForScreenRows(@firstRenderedScreenRow, @lastRenderedScreenRow) - for screenLine, i in screenLines - if fold = screenLine.fold - screenRow = @firstRenderedScreenRow + i - element = @lineElementForScreenRow(screenRow) + # * options + # + A set of options equivalent to {Selection.indent}. + indent: (options={})-> + options.autoIndent ?= @shouldAutoIndent() + @mutateSelectedText (selection) -> selection.indent(options) - if bufferRange.intersectsWith(fold.getBufferRange()) - element.addClass('fold-selected') + # Public: Removes the character found behind the current cursor position. + # + # FIXME: Does this remove content from all cursors or the last one? + backspace: -> + @mutateSelectedText (selection) -> selection.backspace() + + # Public: Removes all characters from the current cursor position until the + # beginging of the current word. + backspaceToBeginningOfWord: -> + @mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord() + + # Public: Removes all characters from the current cursor position to the start + # of the line. + backspaceToBeginningOfLine: -> + @mutateSelectedText (selection) -> selection.backspaceToBeginningOfLine() + + # Public: Removes the current selection or the next character after the + # cursor. + delete: -> + @mutateSelectedText (selection) -> selection.delete() + + # Public: Removes all characters from the cursor until the end of the current + # word. + deleteToEndOfWord: -> + @mutateSelectedText (selection) -> selection.deleteToEndOfWord() + + # Public: Deletes the entire line. + deleteLine: -> + @mutateSelectedText (selection) -> selection.deleteLine() + + # Public: Indents the currently selected rows. + # + # FIXME: what does this do if no selection? + indentSelectedRows: -> + @mutateSelectedText (selection) -> selection.indentSelectedRows() + + # Public: Outdents the selected rows. + # + # FIXME: what does this do if no selection? + outdentSelectedRows: -> + @mutateSelectedText (selection) -> selection.outdentSelectedRows() + + # Public: Wraps the lines within a selection in comments. + # + # If the language doesn't have comments, nothing happens. + # + # Returns an {Array} of the commented {Ranges}. + toggleLineCommentsInSelection: -> + @mutateSelectedText (selection) -> selection.toggleLineComments() + + # Public: Indents selected lines based on grammar's suggested indent levels. + autoIndentSelectedRows: -> + @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() + + # Public: Converts all indents to the current {.getTabText} given a {Range}. + normalizeTabsInBufferRange: (bufferRange) -> + return unless @getSoftTabs() + @scanInBufferRange /\t/, bufferRange, ({replace}) => replace(@getTabText()) + + # Public: Copies and removes all characters from cursor to the end of the + # line. + cutToEndOfLine: -> + maintainPasteboard = false + @mutateSelectedText (selection) -> + selection.cutToEndOfLine(maintainPasteboard) + maintainPasteboard = true + + # Public: Cuts the selected text. + cutSelectedText: -> + maintainPasteboard = false + @mutateSelectedText (selection) -> + selection.cut(maintainPasteboard) + maintainPasteboard = true + + # Public: Copies the selected text. + copySelectedText: -> + maintainPasteboard = false + for selection in @getSelections() + selection.copy(maintainPasteboard) + maintainPasteboard = true + + # Public: Pastes the text in the clipboard. + # + # * options: + # + A set of options equivalent to {Selection.insertText}. + pasteText: (options={}) -> + [text, metadata] = atom.pasteboard.read() + + containsNewlines = text.indexOf('\n') isnt -1 + + if atom.config.get('editor.normalizeIndentOnPaste') and metadata + if !@getCursor().hasPrecedingCharactersOnLine() or containsNewlines + options.indentBasis ?= metadata.indentBasis + + @insertText(text, options) + + # Public: Undoes the last change. + undo: -> + @getCursor().needsAutoscroll = true + @buffer.undo(this) + + # Pulic: Redoes the last change. + redo: -> + @getCursor().needsAutoscroll = true + @buffer.redo(this) + + # Public: Folds all the rows. + foldAll: -> + @languageMode.foldAll() + + # Public: Unfolds all the rows. + unfoldAll: -> + @languageMode.unfoldAll() + + # Public: Creates a fold for each section at the given indent level. + foldAllAtIndentLevel: (indentLevel) -> + @languageMode.foldAllAtIndentLevel(indentLevel) + + # Public: Folds the current row. + foldCurrentRow: -> + bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row + @foldBufferRow(bufferRow) + + # Public: Folds a give buffer row. + foldBufferRow: (bufferRow) -> + @languageMode.foldBufferRow(bufferRow) + + # Public: Unfolds the current row. + unfoldCurrentRow: -> + bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row + @unfoldBufferRow(bufferRow) + + # Public: Unfolds a given a buffer row. + unfoldBufferRow: (bufferRow) -> + @languageMode.unfoldBufferRow(bufferRow) + + # Public: Folds all selections. + foldSelection: -> + selection.fold() for selection in @getSelections() + + # {Delegates to: DisplayBuffer.createFold} + createFold: (startRow, endRow) -> + @displayBuffer.createFold(startRow, endRow) + + # {Delegates to: DisplayBuffer.destroyFoldWithId} + destroyFoldWithId: (id) -> + @displayBuffer.destroyFoldWithId(id) + + # {Delegates to: DisplayBuffer.destroyFoldsContainingBufferRow} + destroyFoldsContainingBufferRow: (bufferRow) -> + @displayBuffer.destroyFoldsContainingBufferRow(bufferRow) + + # Public: Removes any {Fold}s found that intersect the given buffer row. + destroyFoldsIntersectingBufferRange: (bufferRange) -> + for row in [bufferRange.start.row..bufferRange.end.row] + @destroyFoldsContainingBufferRow(row) + + # Public: Returns whether the current row is folded. + isFoldedAtCursorRow: -> + @isFoldedAtScreenRow(@getCursorScreenRow()) + + # Public: Returns whether a given buffer row if folded + isFoldedAtBufferRow: (bufferRow) -> + @displayBuffer.isFoldedAtBufferRow(bufferRow) + + # Public: Returns whether a given screen row if folded + isFoldedAtScreenRow: (screenRow) -> + @displayBuffer.isFoldedAtScreenRow(screenRow) + + # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow} + largestFoldContainingBufferRow: (bufferRow) -> + @displayBuffer.largestFoldContainingBufferRow(bufferRow) + + # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow} + largestFoldStartingAtScreenRow: (screenRow) -> + @displayBuffer.largestFoldStartingAtScreenRow(screenRow) + + # Public: Suggests the indent for the given buffer row. + suggestedIndentForBufferRow: (bufferRow) -> + @languageMode.suggestedIndentForBufferRow(bufferRow) + + # Public: Indents all the rows between two buffer rows. + # + # * startRow: The row {Number} to start at (inclusive) + # * endRow: The row {Number} to end at (inclusive) + autoIndentBufferRows: (startRow, endRow) -> + @languageMode.autoIndentBufferRows(startRow, endRow) + + # Public: Indents the given buffer row to it's suggested level. + autoIndentBufferRow: (bufferRow) -> + @languageMode.autoIndentBufferRow(bufferRow) + + # Public: + # + # FIXME: What does this do? + autoDecreaseIndentForBufferRow: (bufferRow) -> + @languageMode.autoDecreaseIndentForBufferRow(bufferRow) + + # Public: Wraps the lines between two rows in comments. + # + # If the language doesn't have comments, nothing happens. + # + # startRow - The row {Number} to start at (inclusive) + # endRow - The row {Number} to end at (inclusive) + # + # Returns an {Array} of the commented {Ranges}. + toggleLineCommentsForBufferRows: (start, end) -> + @languageMode.toggleLineCommentsForBufferRows(start, end) + + # Public: Moves the selected line up one row. + moveLineUp: -> + selection = @getSelectedBufferRange() + return if selection.start.row is 0 + lastRow = @buffer.getLastRow() + return if selection.isEmpty() and selection.start.row is lastRow and @buffer.getLastLine() is '' + + @transact => + foldedRows = [] + rows = [selection.start.row..selection.end.row] + if selection.start.row isnt selection.end.row and selection.end.column is 0 + rows.pop() unless @isFoldedAtBufferRow(selection.end.row) + for row in rows + screenRow = @screenPositionForBufferPosition([row]).row + if @isFoldedAtScreenRow(screenRow) + bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) + startRow = bufferRange.start.row + endRow = bufferRange.end.row - 1 + foldedRows.push(endRow - 1) else - element.removeClass('fold-selected') + startRow = row + endRow = row - saveScrollPositionForActiveEditSession: -> - if @attached - @activeEditSession.setScrollTop(@scrollTop()) - @activeEditSession.setScrollLeft(@scrollLeft()) + endPosition = Point.min([endRow + 1], @buffer.getEofPosition()) + lines = @buffer.getTextInRange([[startRow], endPosition]) + if endPosition.row is lastRow and endPosition.column > 0 and not @buffer.lineEndingForRow(endPosition.row) + lines = "#{lines}\n" + @buffer.deleteRows(startRow, endRow) + @buffer.insert([startRow - 1], lines) - # Toggle soft tabs on the edit session. - toggleSoftTabs: -> - @activeEditSession.setSoftTabs(not @activeEditSession.getSoftTabs()) + @foldBufferRow(foldedRow) for foldedRow in foldedRows - # Toggle soft wrap on the edit session. - toggleSoftWrap: -> - @setWidthInChars() - @activeEditSession.setSoftWrap(not @activeEditSession.getSoftWrap()) + @setSelectedBufferRange(selection.translate([-1]), preserveFolds: true) - calculateWidthInChars: -> - Math.floor(@scrollView.width() / @charWidth) + # Public: Moves the selected line down one row. + moveLineDown: -> + selection = @getSelectedBufferRange() + lastRow = @buffer.getLastRow() + return if selection.end.row is lastRow + return if selection.end.row is lastRow - 1 and @buffer.getLastLine() is '' - calculateHeightInLines: -> - Math.ceil($(window).height() / @lineHeight) + @transact => + foldedRows = [] + rows = [selection.end.row..selection.start.row] + if selection.start.row isnt selection.end.row and selection.end.column is 0 + rows.shift() unless @isFoldedAtBufferRow(selection.end.row) + for row in rows + screenRow = @screenPositionForBufferPosition([row]).row + if @isFoldedAtScreenRow(screenRow) + bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) + startRow = bufferRange.start.row + endRow = bufferRange.end.row - 1 + foldedRows.push(endRow + 1) + else + startRow = row + endRow = row - # Enables/disables soft wrap on the editor. + if endRow + 1 is lastRow + endPosition = [endRow, @buffer.lineLengthForRow(endRow)] + else + endPosition = [endRow + 1] + lines = @buffer.getTextInRange([[startRow], endPosition]) + @buffer.deleteRows(startRow, endRow) + insertPosition = Point.min([startRow + 1], @buffer.getEofPosition()) + if insertPosition.row is @buffer.getLastRow() and insertPosition.column > 0 + lines = "\n#{lines}" + @buffer.insert(insertPosition, lines) + + @foldBufferRow(foldedRow) for foldedRow in foldedRows + + @setSelectedBufferRange(selection.translate([1]), preserveFolds: true) + + # Public: Duplicates the current line. # - # softWrap - A {Boolean} which, if `true`, enables soft wrap - setSoftWrap: (softWrap) -> - if softWrap - @addClass 'soft-wrap' - @scrollLeft(0) + # If more than one cursor is present, only the most recently added one is + # duplicated. + duplicateLine: -> + return unless @getSelection().isEmpty() + + @transact => + cursorPosition = @getCursorBufferPosition() + cursorRowFolded = @isFoldedAtCursorRow() + if cursorRowFolded + screenRow = @screenPositionForBufferPosition(cursorPosition).row + bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) + else + bufferRange = new Range([cursorPosition.row], [cursorPosition.row + 1]) + + insertPosition = new Point(bufferRange.end.row) + if insertPosition.row > @buffer.getLastRow() + @unfoldCurrentRow() if cursorRowFolded + @buffer.append("\n#{@getTextInBufferRange(bufferRange)}") + @foldCurrentRow() if cursorRowFolded + else + @buffer.insert(insertPosition, @getTextInBufferRange(bufferRange)) + + @setCursorScreenPosition(@getCursorScreenPosition().translate([1])) + @foldCurrentRow() if cursorRowFolded + + # Private: + mutateSelectedText: (fn) -> + @transact => fn(selection) for selection in @getSelections() + + # Private: + replaceSelectedText: (options={}, fn) -> + {selectWordIfEmpty} = options + @mutateSelectedText (selection) -> + range = selection.getBufferRange() + if selectWordIfEmpty and selection.isEmpty() + selection.selectWord() + text = selection.getText() + selection.deleteSelectedText() + selection.insertText(fn(text)) + selection.setBufferRange(range) + + # Public: Returns a valid {DisplayBufferMarker} object for the given id. + getMarker: (id) -> + @displayBuffer.getMarker(id) + + # Public: Returns all {DisplayBufferMarker}s. + getMarkers: -> + @displayBuffer.getMarkers() + + # Public: Returns all {DisplayBufferMarker}s that match all given attributes. + findMarkers: (attributes) -> + @displayBuffer.findMarkers(attributes) + + # {Delegates to: DisplayBuffer.markScreenRange} + markScreenRange: (args...) -> + @displayBuffer.markScreenRange(args...) + + # {Delegates to: DisplayBuffer.markBufferRange} + markBufferRange: (args...) -> + @displayBuffer.markBufferRange(args...) + + # {Delegates to: DisplayBuffer.markScreenPosition} + markScreenPosition: (args...) -> + @displayBuffer.markScreenPosition(args...) + + # {Delegates to: DisplayBuffer.markBufferPosition} + markBufferPosition: (args...) -> + @displayBuffer.markBufferPosition(args...) + + # {Delegates to: DisplayBuffer.destroyMarker} + destroyMarker: (args...) -> + @displayBuffer.destroyMarker(args...) + + # {Delegates to: DisplayBuffer.getMarkerCount} + getMarkerCount: -> + @buffer.getMarkerCount() + + # Public: Determines if there are multiple cursors. + hasMultipleCursors: -> + @getCursors().length > 1 + + # Public: Returns an Array of all {Cursor}s, including cursors representing + # remote users. + getAllCursors: -> + @getCursors().concat(@getRemoteCursors()) + + # Public: Returns an Array of all local {Cursor}s. + getCursors: -> new Array(@cursors...) + + # Public: Returns the most recently added {Cursor}. + getCursor: -> + _.last(@cursors) + + # Public: Returns an Array of all remove {Cursor}s. + getRemoteCursors: -> new Array(@remoteCursors...) + + # Public: Adds and returns a cursor at the given screen position. + addCursorAtScreenPosition: (screenPosition) -> + @markScreenPosition(screenPosition, @getSelectionMarkerAttributes()) + @getLastSelection().cursor + + # Public: Adds and returns a cursor at the given buffer position. + addCursorAtBufferPosition: (bufferPosition) -> + @markBufferPosition(bufferPosition, @getSelectionMarkerAttributes()) + @getLastSelection().cursor + + # Public: Adds and returns a cursor at the given {DisplayBufferMarker} + # position. + addCursor: (marker) -> + cursor = new Cursor(editor: this, marker: marker) + if marker.isLocal() + @cursors.push(cursor) else - @removeClass 'soft-wrap' + @remoteCursors.push(cursor) + @emit 'cursor-added', cursor + cursor - # Sets the font size for the editor. + # Public: Removes and returns a cursor from the `Editor`. + removeCursor: (cursor) -> + _.remove(@cursors, cursor) + + # Public: Creates a new selection at the given marker. # - # fontSize - A {Number} indicating the font size in pixels. - setFontSize: (fontSize) -> - @css('font-size', "#{fontSize}px}") + # * marker: + # The {DisplayBufferMarker} to highlight + # * options: + # + A hash of options that pertain to the {Selection} constructor. + # + # Returns the new {Selection}. + addSelection: (marker, options={}) -> + unless marker.getAttributes().preserveFolds + @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) + cursor = @addCursor(marker) + selection = new Selection(_.extend({editor: this, marker, cursor}, options)) - @clearCharacterWidthCache() - - if @isOnDom() - @redraw() + if marker.isLocal() + @selections.push(selection) else - @redrawOnReattach = @attached + @remoteSelections.push(selection) - # Retrieves the font size for the editor. - # - # Returns a {Number} indicating the font size in pixels. - getFontSize: -> - parseInt(@css("font-size")) - - # Sets the font family for the editor. - # - # fontFamily - A {String} identifying the CSS `font-family`, - setFontFamily: (fontFamily='') -> - @css('font-family', fontFamily) - - @clearCharacterWidthCache() - - @redraw() - - # Gets the font family for the editor. - # - # Returns a {String} identifying the CSS `font-family`, - getFontFamily: -> @css("font-family") - - # Redraw the editor - redraw: -> - return unless @hasParent() - return unless @attached - @redrawOnReattach = false - @calculateDimensions() - @updatePaddingOfRenderedLines() - @updateLayerDimensions() - @requestDisplayUpdate() - - splitLeft: -> - pane = @getPane() - pane?.splitLeft(pane?.copyActiveItem()).activeView - - splitRight: -> - pane = @getPane() - pane?.splitRight(pane?.copyActiveItem()).activeView - - splitUp: -> - pane = @getPane() - pane?.splitUp(pane?.copyActiveItem()).activeView - - splitDown: -> - pane = @getPane() - pane?.splitDown(pane?.copyActiveItem()).activeView - - # Retrieve's the `Editor`'s pane. - # - # Returns a {Pane}. - getPane: -> - @parent('.item-views').parent('.pane').view() - - remove: (selector, keepData) -> - return super if keepData or @removed - super - rootView?.focus() - - beforeRemove: -> - @trigger 'editor:will-be-removed' - @removed = true - @activeEditSession?.destroy() - $(window).off(".editor-#{@id}") - $(document).off(".editor-#{@id}") - - getCursorView: (index) -> - index ?= @cursorViews.length - 1 - @cursorViews[index] - - getCursorViews: -> - new Array(@cursorViews...) - - addCursorView: (cursor, options) -> - cursorView = new CursorView(cursor, this, options) - @cursorViews.push(cursorView) - @overlayer.append(cursorView) - cursorView - - removeCursorView: (cursorView) -> - _.remove(@cursorViews, cursorView) - - getSelectionView: (index) -> - index ?= @selectionViews.length - 1 - @selectionViews[index] - - getSelectionViews: -> - new Array(@selectionViews...) - - addSelectionView: (selection) -> - selectionView = new SelectionView({editor: this, selection}) - @selectionViews.push(selectionView) - @underlayer.append(selectionView) - selectionView - - removeSelectionView: (selectionView) -> - _.remove(@selectionViews, selectionView) - - removeAllCursorAndSelectionViews: -> - cursorView.remove() for cursorView in @getCursorViews() - selectionView.remove() for selectionView in @getSelectionViews() - - appendToLinesView: (view) -> - @overlayer.append(view) - - ### Internal ### - - # Scrolls the editor vertically to a given position. - scrollVertically: (pixelPosition, {center}={}) -> - scrollViewHeight = @scrollView.height() - scrollTop = @scrollTop() - scrollBottom = scrollTop + scrollViewHeight - - if center - unless scrollTop < pixelPosition.top < scrollBottom - @scrollTop(pixelPosition.top - (scrollViewHeight / 2)) + selectionBufferRange = selection.getBufferRange() + @mergeIntersectingSelections() + if selection.destroyed + for selection in @getSelections() + if selection.intersectsBufferRange(selectionBufferRange) + return selection else - linesInView = @scrollView.height() / @lineHeight - maxScrollMargin = Math.floor((linesInView - 1) / 2) - scrollMargin = Math.min(@vScrollMargin, maxScrollMargin) - margin = scrollMargin * @lineHeight - desiredTop = pixelPosition.top - margin - desiredBottom = pixelPosition.top + @lineHeight + margin - if desiredBottom > scrollBottom - @scrollTop(desiredBottom - scrollViewHeight) - else if desiredTop < scrollTop - @scrollTop(desiredTop) + @emit 'selection-added', selection + selection - # Scrolls the editor horizontally to a given position. - scrollHorizontally: (pixelPosition) -> - return if @activeEditSession.getSoftWrap() + # Public: Given a buffer range, this adds a new selection for it. + # + # * bufferRange: + # A {Range} in the buffer + # * options: + # + A hash of options for {.markBufferRange} + # + # Returns the new {Selection}. + addSelectionForBufferRange: (bufferRange, options={}) -> + @markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options)) + @getLastSelection() - charsInView = @scrollView.width() / @charWidth - maxScrollMargin = Math.floor((charsInView - 1) / 2) - scrollMargin = Math.min(@hScrollMargin, maxScrollMargin) - margin = scrollMargin * @charWidth - desiredRight = pixelPosition.left + @charWidth + margin - desiredLeft = pixelPosition.left - margin + # Public: Given a buffer range, this removes all previous selections and + # creates a new selection for it. + # + # * bufferRange: + # A {Range} in the buffer + # * options: + # + A hash of options for {.setSelectedBufferRanges} + setSelectedBufferRange: (bufferRange, options) -> + @setSelectedBufferRanges([bufferRange], options) - if desiredRight > @scrollRight() - @scrollRight(desiredRight) - else if desiredLeft < @scrollLeft() - @scrollLeft(desiredLeft) - @saveScrollPositionForActiveEditSession() + # Public: Given an array of buffer ranges, this removes all previous + # selections and creates new selections for them. + # + # * bufferRange: + # A {Range} in the buffer + # * options: + # + A hash of options for {.setSelectedBufferRanges} + setSelectedBufferRanges: (bufferRanges, options={}) -> + throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length - calculateDimensions: -> - fragment = $('') - @renderedLines.append(fragment) + selections = @getSelections() + selection.destroy() for selection in selections[bufferRanges.length...] - lineRect = fragment[0].getBoundingClientRect() - charRect = fragment.find('span')[0].getBoundingClientRect() - @lineHeight = lineRect.height - @charWidth = charRect.width - @charHeight = charRect.height - fragment.remove() - @setHeightInLines() + @mergeIntersectingSelections options, => + for bufferRange, i in bufferRanges + bufferRange = Range.fromObject(bufferRange) + if selections[i] + selections[i].setBufferRange(bufferRange, options) + else + @addSelectionForBufferRange(bufferRange, options) - updateLayerDimensions: -> - height = @lineHeight * @getScreenLineCount() - unless @layerHeight == height - @layerHeight = height - @underlayer.height(@layerHeight) - @renderedLines.height(@layerHeight) - @overlayer.height(@layerHeight) - @verticalScrollbarContent.height(@layerHeight) - @scrollBottom(height) if @scrollBottom() > height + # Public: Unselects a given selection. + # + # * selection - The {Selection} to remove. + removeSelection: (selection) -> + if selection.isLocal() + _.remove(@selections, selection) + else + _.remove(@remoteSelections, selection) - minWidth = Math.max(@charWidth * @getMaxScreenLineLength() + 20, @scrollView.width()) - unless @layerMinWidth == minWidth - @renderedLines.css('min-width', minWidth) - @underlayer.css('min-width', minWidth) - @overlayer.css('min-width', minWidth) - @layerMinWidth = minWidth - @trigger 'editor:min-width-changed' + # Public: Clears every selection. + # + # TODO: Is this still to be done? + clearSelections: -> + @consolidateSelections() + @getSelection().clear() - # Override for speed. The base function checks computedStyle, unnecessary here. - isHidden: -> - style = this[0].style - if style.display == 'none' or not @isOnDom() + # Public: + # + # FIXME: What does this do? + consolidateSelections: -> + selections = @getSelections() + if selections.length > 1 + selection.destroy() for selection in selections[0...-1] true else false - clearRenderedLines: -> - @renderedLines.empty() - @firstRenderedScreenRow = null - @lastRenderedScreenRow = null + # Public: Returns all selections, including remote selections. + getAllSelections: -> + @getSelections().concat(@getRemoteSelections()) - resetDisplay: -> - return unless @attached - - @clearRenderedLines() - @removeAllCursorAndSelectionViews() - editSessionScrollTop = @activeEditSession.getScrollTop() ? 0 - editSessionScrollLeft = @activeEditSession.getScrollLeft() ? 0 - @updateLayerDimensions() - @scrollTop(editSessionScrollTop) - @scrollLeft(editSessionScrollLeft) - @setSoftWrap(@activeEditSession.getSoftWrap()) - @newCursors = @activeEditSession.getAllCursors() - @newSelections = @activeEditSession.getAllSelections() - @updateDisplay(suppressAutoScroll: true) - - requestDisplayUpdate: -> - return if @pendingDisplayUpdate - return unless @isVisible() - @pendingDisplayUpdate = true - setImmediate => - @updateDisplay() - @pendingDisplayUpdate = false - - updateDisplay: (options={}) -> - return unless @attached and @activeEditSession - return if @activeEditSession.destroyed - unless @isOnDom() and @isVisible() - @redrawOnReattach = true - return - - @updateRenderedLines() - @highlightCursorLine() - @updateCursorViews() - @updateSelectionViews() - @autoscroll(options) - @trigger 'editor:display-updated' - - updateCursorViews: -> - if @newCursors.length > 0 - @addCursorView(cursor) for cursor in @newCursors when not cursor.destroyed - @syncCursorAnimations() - @newCursors = [] - - for cursorView in @getCursorViews() - if cursorView.needsRemoval - cursorView.remove() - else if @shouldUpdateCursor(cursorView) - cursorView.updateDisplay() - - shouldUpdateCursor: (cursorView) -> - return false unless cursorView.needsUpdate - - pos = cursorView.getScreenPosition() - pos.row >= @firstRenderedScreenRow and pos.row <= @lastRenderedScreenRow - - updateSelectionViews: -> - if @newSelections.length > 0 - @addSelectionView(selection) for selection in @newSelections when not selection.destroyed - @newSelections = [] - - for selectionView in @getSelectionViews() - if selectionView.needsRemoval - selectionView.remove() - else if @shouldUpdateSelection(selectionView) - selectionView.updateDisplay() - - shouldUpdateSelection: (selectionView) -> - screenRange = selectionView.getScreenRange() - startRow = screenRange.start.row - endRow = screenRange.end.row - (startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or # startRow in range - (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) or # endRow in range - (startRow <= @firstRenderedScreenRow and endRow >= @lastRenderedScreenRow) # selection surrounds the rendered items - - syncCursorAnimations: -> - for cursorView in @getCursorViews() - do (cursorView) -> cursorView.resetBlinking() - - autoscroll: (options={}) -> - for cursorView in @getCursorViews() - if !options.suppressAutoScroll and cursorView.needsAutoscroll() - @scrollToPixelPosition(cursorView.getPixelPosition()) - cursorView.clearAutoscroll() - - for selectionView in @getSelectionViews() - if !options.suppressAutoScroll and selectionView.needsAutoscroll() - @scrollToPixelPosition(selectionView.getCenterPixelPosition(), center: true) - selectionView.highlight() - selectionView.clearAutoscroll() - - updateRenderedLines: -> - firstVisibleScreenRow = @getFirstVisibleScreenRow() - lastScreenRowToRender = firstVisibleScreenRow + @heightInLines - 1 - lastScreenRow = @getLastScreenRow() - - if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastScreenRowToRender <= @lastRenderedScreenRow - renderFrom = Math.min(lastScreenRow, @firstRenderedScreenRow) - renderTo = Math.min(lastScreenRow, @lastRenderedScreenRow) - else - renderFrom = Math.min(lastScreenRow, Math.max(0, firstVisibleScreenRow - @lineOverdraw)) - renderTo = Math.min(lastScreenRow, lastScreenRowToRender + @lineOverdraw) - - if @pendingChanges.length == 0 and @firstRenderedScreenRow and @firstRenderedScreenRow <= renderFrom and renderTo <= @lastRenderedScreenRow - return - - changes = @pendingChanges - intactRanges = @computeIntactRanges(renderFrom, renderTo) - - @gutter.updateLineNumbers(changes, renderFrom, renderTo) - - @clearDirtyRanges(intactRanges) - @fillDirtyRanges(intactRanges, renderFrom, renderTo) - @firstRenderedScreenRow = renderFrom - @lastRenderedScreenRow = renderTo - @updateLayerDimensions() - @updatePaddingOfRenderedLines() - - computeSurroundingEmptyLineChanges: (change) -> - emptyLineChanges = [] - - if change.bufferDelta? - afterStart = change.end + change.bufferDelta + 1 - if @lineForBufferRow(afterStart) is '' - afterEnd = afterStart - afterEnd++ while @lineForBufferRow(afterEnd + 1) is '' - emptyLineChanges.push({start: afterStart, end: afterEnd, screenDelta: 0}) - - beforeEnd = change.start - 1 - if @lineForBufferRow(beforeEnd) is '' - beforeStart = beforeEnd - beforeStart-- while @lineForBufferRow(beforeStart - 1) is '' - emptyLineChanges.push({start: beforeStart, end: beforeEnd, screenDelta: 0}) - - emptyLineChanges - - computeIntactRanges: (renderFrom, renderTo) -> - return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow? - - intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}] - - if not @mini and @showIndentGuide - emptyLineChanges = [] - for change in @pendingChanges - emptyLineChanges.push(@computeSurroundingEmptyLineChanges(change)...) - @pendingChanges.push(emptyLineChanges...) - - for change in @pendingChanges - newIntactRanges = [] - for range in intactRanges - if change.end < range.start and change.screenDelta != 0 - newIntactRanges.push( - start: range.start + change.screenDelta - end: range.end + change.screenDelta - domStart: range.domStart - ) - else if change.end < range.start or change.start > range.end - newIntactRanges.push(range) - else - if change.start > range.start - newIntactRanges.push( - start: range.start - end: change.start - 1 - domStart: range.domStart) - if change.end < range.end - newIntactRanges.push( - start: change.end + change.screenDelta + 1 - end: range.end + change.screenDelta - domStart: range.domStart + change.end + 1 - range.start - ) - intactRanges = newIntactRanges - - @truncateIntactRanges(intactRanges, renderFrom, renderTo) - - @pendingChanges = [] - - intactRanges - - truncateIntactRanges: (intactRanges, renderFrom, renderTo) -> - i = 0 - while i < intactRanges.length - range = intactRanges[i] - if range.start < renderFrom - range.domStart += renderFrom - range.start - range.start = renderFrom - if range.end > renderTo - range.end = renderTo - if range.start >= range.end - intactRanges.splice(i--, 1) - i++ - intactRanges.sort (a, b) -> a.domStart - b.domStart - - clearDirtyRanges: (intactRanges) -> - if intactRanges.length == 0 - @renderedLines[0].innerHTML = '' - else if currentLine = @renderedLines[0].firstChild - domPosition = 0 - for intactRange in intactRanges - while intactRange.domStart > domPosition - currentLine = @clearLine(currentLine) - domPosition++ - for i in [intactRange.start..intactRange.end] - currentLine = currentLine.nextSibling - domPosition++ - while currentLine - currentLine = @clearLine(currentLine) - - clearLine: (lineElement) -> - next = lineElement.nextSibling - @renderedLines[0].removeChild(lineElement) - next - - fillDirtyRanges: (intactRanges, renderFrom, renderTo) -> - i = 0 - nextIntact = intactRanges[i] - currentLine = @renderedLines[0].firstChild - - row = renderFrom - while row <= renderTo - if row == nextIntact?.end + 1 - nextIntact = intactRanges[++i] - - if !nextIntact or row < nextIntact.start - if nextIntact - dirtyRangeEnd = nextIntact.start - 1 - else - dirtyRangeEnd = renderTo - - for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd) - @renderedLines[0].insertBefore(lineElement, currentLine) - row++ - else - currentLine = currentLine.nextSibling - row++ - - updatePaddingOfRenderedLines: -> - paddingTop = @firstRenderedScreenRow * @lineHeight - @renderedLines.css('padding-top', paddingTop) - @gutter.lineNumbers.css('padding-top', paddingTop) - - paddingBottom = (@getLastScreenRow() - @lastRenderedScreenRow) * @lineHeight - @renderedLines.css('padding-bottom', paddingBottom) - @gutter.lineNumbers.css('padding-bottom', paddingBottom) - - ### Public ### - - # Retrieves the number of the row that is visible and currently at the top of the editor. + # Public: Gets all local selections. # - # Returns a {Number}. - getFirstVisibleScreenRow: -> - screenRow = Math.floor(@scrollTop() / @lineHeight) - screenRow = 0 if isNaN(screenRow) - screenRow + # Returns an {Array} of {Selection}s. + getSelections: -> new Array(@selections...) - # Retrieves the number of the row that is visible and currently at the bottom of the editor. - # - # Returns a {Number}. - getLastVisibleScreenRow: -> - calculatedRow = Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1 - screenRow = Math.max(0, Math.min(@getScreenLineCount() - 1, calculatedRow)) - screenRow = 0 if isNaN(screenRow) - screenRow + # Public: Returns the selection at the specified index. + getSelection: (index) -> + index ?= @selections.length - 1 + @selections[index] - # Given a row number, identifies if it is currently visible. + # Public: Returns the most recently added {Selection} + getLastSelection: -> + _.last(@selections) + + # Public: Returns all remote selections. + getRemoteSelections: -> new Array(@remoteSelections...) + + # Public: Gets all local selections, ordered by their position in the buffer. # - # row - A row {Number} to check + # Returns an {Array} of {Selection}s. + getSelectionsOrderedByBufferPosition: -> + @getSelections().sort (a, b) -> a.compare(b) + + # Public: Gets all remote selections, ordered by their position in the buffer. + # + # Returns an {Array} of {Selection}s. + getRemoteSelectionsOrderedByBufferPosition: -> + @getRemoteSelections().sort (a, b) -> a.compare(b) + + # Public: Gets the very last local selection in the buffer. + # + # Returns a {Selection}. + getLastSelectionInBuffer: -> + _.last(@getSelectionsOrderedByBufferPosition()) + + # Public: Determines if a given buffer range is included in a {Selection}. + # + # * bufferRange: + # The {Range} you're checking against # # Returns a {Boolean}. - isScreenRowVisible: (row) -> - @getFirstVisibleScreenRow() <= row <= @getLastVisibleScreenRow() + selectionIntersectsBufferRange: (bufferRange) -> + _.any @getSelections(), (selection) -> + selection.intersectsBufferRange(bufferRange) - ### Internal ### - - handleScreenLinesChange: (change) -> - @pendingChanges.push(change) - @requestDisplayUpdate() - - buildLineElementForScreenRow: (screenRow) -> - @buildLineElementsForScreenRows(screenRow, screenRow)[0] - - buildLineElementsForScreenRows: (startRow, endRow) -> - div = document.createElement('div') - div.innerHTML = @htmlForScreenRows(startRow, endRow) - new Array(div.children...) - - htmlForScreenRows: (startRow, endRow) -> - htmlLines = '' - screenRow = startRow - for line in @activeEditSession.linesForScreenRows(startRow, endRow) - htmlLines += @htmlForScreenLine(line, screenRow++) - htmlLines - - htmlForScreenLine: (screenLine, screenRow) -> - { tokens, text, lineEnding, fold, isSoftWrapped } = screenLine - if fold - attributes = { class: 'fold line', 'fold-id': fold.id } - else - attributes = { class: 'line' } - - invisibles = @invisibles if @showInvisibles - eolInvisibles = @getEndOfLineInvisibles(screenLine) - htmlEolInvisibles = @buildHtmlEndOfLineInvisibles(screenLine) - - indentation = Editor.buildIndentation(screenRow, @activeEditSession) - - Editor.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, @showIndentGuide, indentation, @activeEditSession, @mini}) - - @buildIndentation: (screenRow, activeEditSession) -> - bufferRow = activeEditSession.bufferPositionForScreenPosition([screenRow]).row - bufferLine = activeEditSession.lineForBufferRow(bufferRow) - if bufferLine is '' - indentation = 0 - nextRow = screenRow + 1 - while nextRow < activeEditSession.getBuffer().getLineCount() - bufferRow = activeEditSession.bufferPositionForScreenPosition([nextRow]).row - bufferLine = activeEditSession.lineForBufferRow(bufferRow) - if bufferLine isnt '' - indentation = Math.ceil(activeEditSession.indentLevelForLine(bufferLine)) - break - nextRow++ - - previousRow = screenRow - 1 - while previousRow >= 0 - bufferRow = activeEditSession.bufferPositionForScreenPosition([previousRow]).row - bufferLine = activeEditSession.lineForBufferRow(bufferRow) - if bufferLine isnt '' - indentation = Math.max(indentation, Math.ceil(activeEditSession.indentLevelForLine(bufferLine))) - break - previousRow-- - - indentation - else - Math.ceil(activeEditSession.indentLevelForLine(bufferLine)) - - buildHtmlEndOfLineInvisibles: (screenLine) -> - invisibles = [] - for invisible in @getEndOfLineInvisibles(screenLine) - invisibles.push("#{invisible}") - invisibles.join('') - - getEndOfLineInvisibles: (screenLine) -> - return [] unless @showInvisibles and @invisibles - return [] if @mini or screenLine.isSoftWrapped() - - invisibles = [] - invisibles.push(@invisibles.cr) if @invisibles.cr and screenLine.lineEnding is '\r\n' - invisibles.push(@invisibles.eol) if @invisibles.eol - invisibles - - lineElementForScreenRow: (screenRow) -> - @renderedLines.children(":eq(#{screenRow - @firstRenderedScreenRow})") - - toggleLineCommentsInSelection: -> - @activeEditSession.toggleLineCommentsInSelection() - - ### Public ### - - # Converts a buffer position to a pixel position. + # Public: Moves every local cursor to a given screen position. # - # position - An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # * position: + # An {Array} of two numbers: the screen row, and the screen column. + # * options: + # An object with properties based on {Cursor.setScreenPosition} + setCursorScreenPosition: (position, options) -> + @moveCursors (cursor) -> cursor.setScreenPosition(position, options) + + # Public: Gets the current screen position of the most recently added + # local {Cursor}. # - # Returns an object with two values: `top` and `left`, representing the pixel positions. - pixelPositionForBufferPosition: (position) -> - @pixelPositionForScreenPosition(@screenPositionForBufferPosition(position)) + # Returns an {Array} of two numbers: the screen row, and the screen column. + getCursorScreenPosition: -> + @getCursor().getScreenPosition() - # Converts a screen position to a pixel position. + # Public: Gets the screen row of the most recently added local {Cursor}. # - # position - An object that represents a screen position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # Returns the screen row {Number}. + getCursorScreenRow: -> + @getCursor().getScreenRow() + + # Public: Moves every cursor to a given buffer position. # - # Returns an object with two values: `top` and `left`, representing the pixel positions. - pixelPositionForScreenPosition: (position) -> - return { top: 0, left: 0 } unless @isOnDom() and @isVisible() - {row, column} = Point.fromObject(position) - actualRow = Math.floor(row) + # * position: + # An {Array} of two numbers: the buffer row, and the buffer column. + # * options: + # + An object with properties based on {Cursor.setBufferPosition} + setCursorBufferPosition: (position, options) -> + @moveCursors (cursor) -> cursor.setBufferPosition(position, options) - lineElement = existingLineElement = @lineElementForScreenRow(actualRow)[0] - unless existingLineElement - lineElement = @buildLineElementForScreenRow(actualRow) - @renderedLines.append(lineElement) - left = @positionLeftForLineAndColumn(lineElement, actualRow, column) - unless existingLineElement - @renderedLines[0].removeChild(lineElement) - { top: row * @lineHeight, left } + # Public: Gets the current buffer position of the most recently added {Cursor}. + # + # Returns an {Array} of two numbers: the buffer row, and the buffer column. + getCursorBufferPosition: -> + @getCursor().getBufferPosition() - positionLeftForLineAndColumn: (lineElement, screenRow, column) -> - return 0 if column == 0 + # Public: Returns the screen {Range} of the most recently added local + # {Selection}. + getSelectedScreenRange: -> + @getLastSelection().getScreenRange() - bufferRow = @bufferRowsForScreenRows(screenRow, screenRow)[0] ? screenRow - tokenizedLine = @activeEditSession.displayBuffer.tokenizedBuffer.tokenizedLines[bufferRow] + # Public: Returns the buffer {Range} of the most recently added local + # {Selection}. + getSelectedBufferRange: -> + @getLastSelection().getBufferRange() - left = 0 - index = 0 - for token in tokenizedLine.tokens - for char in token.value - return left if index >= column + # Public: Gets an Array of buffer {Range}s of all the local {Selection}s. + # + # Sorted by their position in the file itself. + getSelectedBufferRanges: -> + selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition() - val = @getCharacterWidthCache(token.scopes, char) - if val? - left += val - else - return @measureToColumn(lineElement, tokenizedLine, column) + # Public: Gets an Array of buffer {Range}s of all the remote {Selection}s. + # + # Sorted by their position in the file itself. + getRemoteSelectedBufferRanges: -> + selection.getBufferRange() for selection in @getRemoteSelectionsOrderedByBufferPosition() - index++ - left + # Public: Returns the selected text of the most recently added local {Selection}. + getSelectedText: -> + @getLastSelection().getText() - scopesForColumn: (tokenizedLine, column) -> - index = 0 - for token in tokenizedLine.tokens - for char in token.value - return token.scopes if index == column - index++ - null + # Public: Returns the text within a given a buffer {Range} + getTextInBufferRange: (range) -> + @buffer.getTextInRange(range) - measureToColumn: (lineElement, tokenizedLine, column) -> - left = oldLeft = index = 0 - iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFilter) + setTextInBufferRange: (range, text) -> @getBuffer().change(range, text) - returnLeft = null + # Public: Returns the text of the most recent local cursor's surrounding + # paragraph. + getCurrentParagraphBufferRange: -> + @getCursor().getCurrentParagraphBufferRange() - offsetLeft = @scrollView.offset().left - paddingLeft = parseInt(@scrollView.css('padding-left')) + # Public: Returns the word under the most recently added local {Cursor}. + # + # * options: + # + An object with properties based on + # {Cursor.getBeginningOfCurrentWordBufferPosition}. + getWordUnderCursor: (options) -> + @getTextInBufferRange(@getCursor().getCurrentWordBufferRange(options)) - while textNode = iterator.nextNode() - content = textNode.textContent + # Public: Moves every local cursor up one row. + moveCursorUp: (lineCount) -> + @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true) - for char, i in content - # Don't continue caching long lines :racehorse: - break if index > LongLineLength and column < index + # Public: Moves every local cursor down one row. + moveCursorDown: (lineCount) -> + @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true) - # Dont return right away, finish caching the whole line - returnLeft = left if index == column - oldLeft = left + # Public: Moves every local cursor left one column. + moveCursorLeft: -> + @moveCursors (cursor) -> cursor.moveLeft(moveToEndOfSelection: true) - scopes = @scopesForColumn(tokenizedLine, index) - cachedCharWidth = @getCharacterWidthCache(scopes, char) + # Public: Moves every local cursor right one column. + moveCursorRight: -> + @moveCursors (cursor) -> cursor.moveRight(moveToEndOfSelection: true) - if cachedCharWidth? - left = oldLeft + cachedCharWidth - else - # i + 1 to measure to the end of the current character - MeasureRange.setEnd(textNode, i + 1) - MeasureRange.collapse() - rects = MeasureRange.getClientRects() - return 0 if rects.length == 0 - left = rects[0].left - Math.floor(offsetLeft) + Math.floor(@scrollLeft()) - paddingLeft + # Public: Moves every local cursor to the top of the buffer. + moveCursorToTop: -> + @moveCursors (cursor) -> cursor.moveToTop() - if scopes? - cachedCharWidth = left - oldLeft - @setCharacterWidthCache(scopes, char, cachedCharWidth) + # Public: Moves every local cursor to the bottom of the buffer. + moveCursorToBottom: -> + @moveCursors (cursor) -> cursor.moveToBottom() - # Assume all the characters are the same width when dealing with long - # lines :racehorse: - return column * cachedCharWidth if index > LongLineLength + # Public: Moves every local cursor to the beginning of the line. + moveCursorToBeginningOfLine: -> + @moveCursors (cursor) -> cursor.moveToBeginningOfLine() - index++ + # Public: Moves every local cursor to the first non-whitespace character of the line. + moveCursorToFirstCharacterOfLine: -> + @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine() - returnLeft ? left + # Public: Moves every local cursor to the end of the line. + moveCursorToEndOfLine: -> + @moveCursors (cursor) -> cursor.moveToEndOfLine() - getCharacterWidthCache: (scopes, char) -> - scopes ?= NoScope - obj = Editor.characterWidthCache - for scope in scopes - obj = obj[scope] - return null unless obj? - obj[char] + # Public: Moves every local cursor to the beginning of the current word. + moveCursorToBeginningOfWord: -> + @moveCursors (cursor) -> cursor.moveToBeginningOfWord() - setCharacterWidthCache: (scopes, char, val) -> - scopes ?= NoScope - obj = Editor.characterWidthCache - for scope in scopes - obj[scope] ?= {} - obj = obj[scope] - obj[char] = val + # Public: Moves every local cursor to the end of the current word. + moveCursorToEndOfWord: -> + @moveCursors (cursor) -> cursor.moveToEndOfWord() - clearCharacterWidthCache: -> - Editor.characterWidthCache = {} + # Public: Moves every local cursor to the beginning of the next word. + moveCursorToBeginningOfNextWord: -> + @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord() - pixelOffsetForScreenPosition: (position) -> - {top, left} = @pixelPositionForScreenPosition(position) - offset = @renderedLines.offset() - {top: top + offset.top, left: left + offset.left} + # Public: Moves every local cursor to the previous word boundary. + moveCursorToPreviousWordBoundary: -> + @moveCursors (cursor) -> cursor.moveToPreviousWordBoundary() - screenPositionFromMouseEvent: (e) -> - { pageX, pageY } = e - offset = @scrollView.offset() + # Public: Moves every local cursor to the next word boundary. + moveCursorToNextWordBoundary: -> + @moveCursors (cursor) -> cursor.moveToNextWordBoundary() - editorRelativeTop = pageY - offset.top + @scrollTop() - row = Math.floor(editorRelativeTop / @lineHeight) - column = 0 + # Internal: Executes given function on all local cursors. + moveCursors: (fn) -> + fn(cursor) for cursor in @getCursors() + @mergeCursors() - if pageX > offset.left and lineElement = @lineElementForScreenRow(row)[0] - range = document.createRange() - iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT) - while node = iterator.nextNode() - range.selectNodeContents(node) - column += node.textContent.length - {left, right} = range.getClientRects()[0] - break if left <= pageX <= right + # Public: Selects the text from the current cursor position to a given screen + # position. + # + # * position: + # An instance of {Point}, with a given `row` and `column`. + selectToScreenPosition: (position) -> + lastSelection = @getLastSelection() + lastSelection.selectToScreenPosition(position) + @mergeIntersectingSelections(isReversed: lastSelection.isReversed()) - if node - for characterPosition in [node.textContent.length...0] - range.setStart(node, characterPosition - 1) - range.setEnd(node, characterPosition) - {left, right, width} = range.getClientRects()[0] - break if left <= pageX - width / 2 <= right - column-- + # Public: Selects the text one position right of all local cursors. + selectRight: -> + @expandSelectionsForward (selection) => selection.selectRight() - range.detach() + # Public: Selects the text one position left of all local cursors. + selectLeft: -> + @expandSelectionsBackward (selection) => selection.selectLeft() - new Point(row, column) + # Public: Selects all the text one position above all local cursors. + selectUp: (rowCount) -> + @expandSelectionsBackward (selection) => selection.selectUp(rowCount) - # Highlights the current line the cursor is on. - highlightCursorLine: -> - return if @mini + # Public: Selects all the text one position below all local cursors. + selectDown: (rowCount) -> + @expandSelectionsForward (selection) => selection.selectDown(rowCount) - @highlightedLine?.removeClass('cursor-line') - if @getSelection().isEmpty() - @highlightedLine = @lineElementForScreenRow(@getCursorScreenRow()) - @highlightedLine.addClass('cursor-line') - else - @highlightedLine = null + # Public: Selects all the text from all local cursors to the top of the + # buffer. + selectToTop: -> + @expandSelectionsBackward (selection) => selection.selectToTop() - # {Delegates to: EditSession.getGrammar} + # Public: Selects all the text in the buffer. + selectAll: -> + @expandSelectionsForward (selection) => selection.selectAll() + + # Public: Selects all the text from all local cursors to the bottom of the + # buffer. + selectToBottom: -> + @expandSelectionsForward (selection) => selection.selectToBottom() + + # Public: Selects all the text from all local cursors to the beginning of each + # of their lines. + selectToBeginningOfLine: -> + @expandSelectionsBackward (selection) => selection.selectToBeginningOfLine() + + # Public: Selects to the first non-whitespace character of the line of all + # local cursors. + selectToFirstCharacterOfLine: -> + @expandSelectionsBackward (selection) => selection.selectToFirstCharacterOfLine() + + # Public: Selects all the text from each local cursor to the end of their + # lines. + selectToEndOfLine: -> + @expandSelectionsForward (selection) => selection.selectToEndOfLine() + + # Public: Selects all text from each local cursor to their previous word + # boundary. + selectToPreviousWordBoundary: -> + @expandSelectionsBackward (selection) => selection.selectToPreviousWordBoundary() + + # Public: Selects all text from each local cursor to their next word + # boundary. + selectToNextWordBoundary: -> + @expandSelectionsForward (selection) => selection.selectToNextWordBoundary() + + # Public: Selects the current line from each local cursor. + selectLine: -> + @expandSelectionsForward (selection) => selection.selectLine() + + # Public: Moves each local selection down one row. + addSelectionBelow: -> + @expandSelectionsForward (selection) => selection.addSelectionBelow() + + # Public: Moves each local selection up one row. + addSelectionAbove: -> + @expandSelectionsBackward (selection) => selection.addSelectionAbove() + + # Public: Transposes the current text selections. + # + # FIXME: I have no idea what this function does. + # + # This only works if there is more than one selection. Each selection is transferred + # to the position of the selection after it. The last selection is transferred to the + # position of the first. + transpose: -> + @mutateSelectedText (selection) => + if selection.isEmpty() + selection.selectRight() + text = selection.getText() + selection.delete() + selection.cursor.moveLeft() + selection.insertText text + else + selection.insertText selection.getText().split('').reverse().join('') + + # Public: Uppercases all locally selected text. + upperCase: -> + @replaceSelectedText selectWordIfEmpty:true, (text) => text.toUpperCase() + + # Public: Lowercases all locally selected text. + lowerCase: -> + @replaceSelectedText selectWordIfEmpty:true, (text) => text.toLowerCase() + + # Public: Joins the current line with the one below it. + # + # FIXME: Needs more clarity. + # + # Multiple cursors are considered equally. If there's a selection in the editor, + # all the lines are joined together. + joinLine: -> + @mutateSelectedText (selection) -> selection.joinLine() + + # {Delegates to: Selection.expandOverLine} + expandLastSelectionOverLine: -> + @getLastSelection().expandOverLine() + + # Public: Selects all the text from all local cursors to the beginning of + # their current words. + selectToBeginningOfWord: -> + @expandSelectionsBackward (selection) => selection.selectToBeginningOfWord() + + # Public: Selects all the text from all local cursors to the end of + # their current words. + selectToEndOfWord: -> + @expandSelectionsForward (selection) => selection.selectToEndOfWord() + + # Public: Selects all the text from all local cursors to the beginning of + # the next word. + selectToBeginningOfNextWord: -> + @expandSelectionsForward (selection) => selection.selectToBeginningOfNextWord() + + # Public: Selects the current word of each local cursor. + selectWord: -> + @expandSelectionsForward (selection) => selection.selectWord() + + # {Delegates to: Selection.expandOverWord} + expandLastSelectionOverWord: -> + @getLastSelection().expandOverWord() + + # Public: Selects the range associated with the given marker if it is valid. + # + # Returns the selected {Range} or a falsy value if the marker is invalid. + selectMarker: (marker) -> + if marker.isValid() + range = marker.getBufferRange() + @setSelectedBufferRange(range) + range + + # Public: + # + # FIXME: Not sure how to describe what this does. + mergeCursors: -> + positions = [] + for cursor in @getCursors() + position = cursor.getBufferPosition().toString() + if position in positions + cursor.destroy() + else + positions.push(position) + + # Public: + # + # FIXME: Not sure how to describe what this does. + expandSelectionsForward: (fn) -> + @mergeIntersectingSelections => + fn(selection) for selection in @getSelections() + + # Public: + # + # FIXME: Not sure how to describe what this does. + expandSelectionsBackward: (fn) -> + @mergeIntersectingSelections isReversed: true, => + fn(selection) for selection in @getSelections() + + # Public: + # + # FIXME: No idea what this does. + finalizeSelections: -> + selection.finalize() for selection in @getSelections() + + # Private: Merges intersecting selections. If passed a function, it executes + # the function with merging suppressed, then merges intersecting selections + # afterward. + mergeIntersectingSelections: (args...) -> + fn = args.pop() if _.isFunction(_.last(args)) + options = args.pop() ? {} + + return fn?() if @suppressSelectionMerging + + if fn? + @suppressSelectionMerging = true + result = fn() + @suppressSelectionMerging = false + + reducer = (disjointSelections, selection) -> + intersectingSelection = _.find(disjointSelections, (s) -> s.intersectsWith(selection)) + if intersectingSelection? + intersectingSelection.merge(selection, options) + disjointSelections + else + disjointSelections.concat([selection]) + + _.reduce(@getSelections(), reducer, []) + + # Private: + preserveCursorPositionOnBufferReload: -> + cursorPosition = null + @subscribe @buffer, "will-reload", => + cursorPosition = @getCursorBufferPosition() + @subscribe @buffer, "reloaded", => + @setCursorBufferPosition(cursorPosition) if cursorPosition + cursorPosition = null + + # {Delegates to: DisplayBuffer.getGrammar} getGrammar: -> - @activeEditSession.getGrammar() + @displayBuffer.getGrammar() - # {Delegates to: EditSession.setGrammar} + # {Delegates to: DisplayBuffer.setGrammar} setGrammar: (grammar) -> - throw new Error("Only mini-editors can explicity set their grammar") unless @mini - @activeEditSession.setGrammar(grammar) + @displayBuffer.setGrammar(grammar) - # {Delegates to: EditSession.reloadGrammar} + # {Delegates to: DisplayBuffer.reloadGrammar} reloadGrammar: -> - @activeEditSession.reloadGrammar() + @displayBuffer.reloadGrammar() - # {Delegates to: EditSession.scopesForBufferPosition} - scopesForBufferPosition: (bufferPosition) -> - @activeEditSession.scopesForBufferPosition(bufferPosition) + # Private: + shouldAutoIndent: -> + atom.config.get("editor.autoIndent") - # Copies the current file path to the native clipboard. - copyPathToPasteboard: -> - path = @getPath() - atom.pasteboard.write(path) if path? + # Public: Performs all editor actions from the given function within a single + # undo step. + # + # Useful for implementing complex operations while still ensuring that the + # undo stack remains relevant. + transact: (fn) -> @buffer.transact(fn) - ### Internal ### + # Private: + beginTransaction: -> @buffer.beginTransaction() - @buildLineHtml: ({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, showIndentGuide, indentation, activeEditSession, mini}) -> - scopeStack = [] - line = [] + # Private: + commitTransaction: -> @buffer.commitTransaction() - attributePairs = '' - attributePairs += " #{attributeName}=\"#{value}\"" for attributeName, value of attributes - line.push("
") + # Private: + abortTransaction: -> @buffer.abortTransaction() - if text == '' - html = Editor.buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) - line.push(html) if html - else - firstNonWhitespacePosition = text.search(/\S/) - firstTrailingWhitespacePosition = text.search(/\s*$/) - lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 - position = 0 - for token in tokens - @updateScopeStack(line, scopeStack, token.scopes) - hasLeadingWhitespace = position < firstNonWhitespacePosition - hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition - hasIndentGuide = not mini and showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly) - line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})) - position += token.value.length + # Private: + inspect: -> + JSON.stringify @state.toObject() - @popScope(line, scopeStack) while scopeStack.length > 0 - line.push(htmlEolInvisibles) unless text == '' - line.push("") if fold + # Private: + logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) - line.push('
') - line.join('') + # Private: + handleGrammarChange: -> + @unfoldAll() + @emit 'grammar-changed' - @updateScopeStack: (line, scopeStack, desiredScopes) -> - excessScopes = scopeStack.length - desiredScopes.length - if excessScopes > 0 - @popScope(line, scopeStack) while excessScopes-- + # Private: + handleMarkerCreated: (marker) => + if marker.matchesAttributes(@getSelectionMarkerAttributes()) + @addSelection(marker) - # pop until common prefix - for i in [scopeStack.length..0] - break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) - @popScope(line, scopeStack) - - # push on top of common prefix until scopeStack == desiredScopes - for j in [i...desiredScopes.length] - @pushScope(line, scopeStack, desiredScopes[j]) - - null - - @pushScope: (line, scopeStack, scope) -> - scopeStack.push(scope) - line.push("") - - @popScope: (line, scopeStack) -> - scopeStack.pop() - line.push("") - - @buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) -> - indentCharIndex = 0 - if not mini and showIndentGuide - if indentation > 0 - tabLength = activeEditSession.getTabLength() - indentGuideHtml = '' - for level in [0...indentation] - indentLevelHtml = "" - for characterPosition in [0...tabLength] - if invisible = eolInvisibles[indentCharIndex++] - indentLevelHtml += "#{invisible}" - else - indentLevelHtml += ' ' - indentLevelHtml += "" - indentGuideHtml += indentLevelHtml - - while indentCharIndex < eolInvisibles.length - indentGuideHtml += "#{eolInvisibles[indentCharIndex++]}" - - return indentGuideHtml - - if htmlEolInvisibles.length > 0 - htmlEolInvisibles - else - ' ' - - replaceSelectedText: (replaceFn) -> - selection = @getSelection() - return false if selection.isEmpty() - - text = replaceFn(@getTextInRange(selection.getBufferRange())) - return false if text is null or text is undefined - - @insertText(text, select: true) - true - - consolidateSelections: (e) -> e.abortKeyBinding() unless @activeEditSession.consolidateSelections() - - logCursorScope: -> - console.log @activeEditSession.getCursorScopes() - - transact: (fn) -> @activeEditSession.transact(fn) - beginTransaction: -> @activeEditSession.beginTransaction() - commitTransaction: -> @activeEditSession.commitTransaction() - abortTransaction: -> @activeEditSession.abortTransaction() - - saveDebugSnapshot: -> - atom.showSaveDialog (path) => - fs.writeFileSync(path, @getDebugSnapshot()) if path + # Private: + getSelectionMarkerAttributes: -> + type: 'selection', editorId: @id, invalidate: 'never' + # Private: getDebugSnapshot: -> [ - "Debug Snapshot: #{@getPath()}" - @getRenderedLinesDebugSnapshot() - @activeEditSession.getDebugSnapshot() - @getBuffer().getDebugSnapshot() + @displayBuffer.getDebugSnapshot() + @displayBuffer.tokenizedBuffer.getDebugSnapshot() ].join('\n\n') - - getRenderedLinesDebugSnapshot: -> - lines = ['Rendered Lines:'] - firstRenderedScreenRow = @firstRenderedScreenRow - @renderedLines.find('.line').each (n) -> - lines.push "#{firstRenderedScreenRow + n}: #{$(this).text()}" - lines.join('\n') - - logScreenLines: (start, end) -> - @activeEditSession.logScreenLines(start, end) - - logRenderedLines: -> - @renderedLines.find('.line').each (n) -> - console.log n, $(this).text() diff --git a/src/gutter.coffee b/src/gutter.coffee index a776bf447..710200473 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -2,7 +2,7 @@ {Range} = require 'telepath' _ = require 'underscore-plus' -# Private: Represents the portion of the {Editor} containing row numbers. +# Private: Represents the portion of the {EditorView} containing row numbers. # # The gutter also indicates if rows are folded. module.exports = @@ -25,37 +25,37 @@ class Gutter extends View @attached = true highlightLines = => @highlightLines() - @getEditor().on 'cursor:moved', highlightLines - @getEditor().on 'selection:changed', highlightLines + @getEditorView().on 'cursor:moved', highlightLines + @getEditorView().on 'selection:changed', highlightLines @on 'mousedown', (e) => @handleMouseEvents(e) beforeRemove: -> - $(document).off(".gutter-#{@getEditor().id}") + $(document).off(".gutter-#{@getEditorView().id}") handleMouseEvents: (e) -> - editor = @getEditor() - startRow = editor.screenPositionFromMouseEvent(e).row + editorView = @getEditorView() + startRow = editorView.screenPositionFromMouseEvent(e).row if e.shiftKey - editor.selectToScreenPosition([startRow + 1, 0]) + editorView.selectToScreenPosition([startRow + 1, 0]) return else - editor.getSelection().setScreenRange([[startRow, 0], [startRow, 0]]) + editorView.getSelection().setScreenRange([[startRow, 0], [startRow, 0]]) moveHandler = (e) => start = startRow - end = editor.screenPositionFromMouseEvent(e).row + end = editorView.screenPositionFromMouseEvent(e).row if end > start then end++ else start++ - editor.getSelection().setScreenRange([[start, 0], [end, 0]]) + editorView.getSelection().setScreenRange([[start, 0], [end, 0]]) - $(document).on "mousemove.gutter-#{@getEditor().id}", moveHandler - $(document).one "mouseup.gutter-#{@getEditor().id}", => $(document).off 'mousemove', moveHandler + $(document).on "mousemove.gutter-#{@getEditorView().id}", moveHandler + $(document).one "mouseup.gutter-#{@getEditorView().id}", => $(document).off 'mousemove', moveHandler ### Public ### - # Retrieves the containing {Editor}. + # Retrieves the containing {EditorView}. # - # Returns an {Editor}. - getEditor: -> + # Returns an {EditorView}. + getEditorView: -> @parentView # Defines whether to show the gutter or not. @@ -192,9 +192,9 @@ class Gutter extends View @elementBuilder.children buildLineElementsHtml: (startScreenRow, endScreenRow) => - editor = @getEditor() - maxDigits = editor.getLineCount().toString().length - rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow) + editorView = @getEditorView() + maxDigits = editorView.getLineCount().toString().length + rows = editorView.bufferRowsForScreenRows(startScreenRow, endScreenRow) html = '' for row in rows @@ -204,7 +204,7 @@ class Gutter extends View rowValue = (row + 1).toString() classes = "line-number line-number-#{row}" - classes += ' fold' if editor.isFoldedAtBufferRow(row) + classes += ' fold' if editorView.isFoldedAtBufferRow(row) rowValuePadding = _.multiplyString(' ', maxDigits - rowValue.length) @@ -230,8 +230,8 @@ class Gutter extends View @highlightedLineNumbers.push(highlightedLineNumber) highlightLines: -> - if @getEditor().getSelection().isEmpty() - row = @getEditor().getCursorScreenPosition().row + if @getEditorView().getSelection().isEmpty() + row = @getEditorView().getCursorScreenPosition().row rowRange = new Range([row, 0], [row, 0]) return if @selectionEmpty and @highlightedRows?.isEqual(rowRange) @@ -240,7 +240,7 @@ class Gutter extends View @highlightedRows = rowRange @selectionEmpty = true else - selectedRows = @getEditor().getSelection().getScreenRange() + selectedRows = @getEditorView().getSelection().getScreenRange() endRow = selectedRows.end.row endRow-- if selectedRows.end.column is 0 selectedRows = new Range([selectedRows.start.row, 0], [endRow, 0]) diff --git a/src/language-mode.coffee b/src/language-mode.coffee index 1947de408..28ee1dc70 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -12,7 +12,7 @@ class LanguageMode buffer: null grammar: null - editSession: null + editor: null currentGrammarScore: null ### Internal ### @@ -22,11 +22,11 @@ class LanguageMode ### Public ### - # Sets up a `LanguageMode` for the given {EditSession}. + # Sets up a `LanguageMode` for the given {Editor}. # - # editSession - The {EditSession} to associate with - constructor: (@editSession) -> - @buffer = @editSession.buffer + # editor - The {Editor} to associate with + constructor: (@editor) -> + @buffer = @editor.buffer # Wraps the lines between two rows in comments. # @@ -37,7 +37,7 @@ class LanguageMode # # Returns an {Array} of the commented {Ranges}. toggleLineCommentsForBufferRows: (start, end) -> - scopes = @editSession.scopesForBufferPosition([start, 0]) + scopes = @editor.scopesForBufferPosition([start, 0]) properties = atom.syntax.propertiesForScope(scopes, "editor.commentStart")[0] return unless properties @@ -46,7 +46,7 @@ class LanguageMode return unless commentStartString - buffer = @editSession.buffer + buffer = @editor.buffer commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '($1)?') commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") shouldUncomment = commentStartRegex.test(buffer.lineForRow(start)) @@ -83,8 +83,8 @@ class LanguageMode buffer.change([[row, columnStart], [row, columnEnd]], "") else indent = @minIndentLevelForRowRange(start, end) - indentString = @editSession.buildIndentString(indent) - tabLength = @editSession.getTabLength() + indentString = @editor.buildIndentString(indent) + tabLength = @editor.getTabLength() indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}") for row in [start..end] line = buffer.lineForRow(row) @@ -98,12 +98,12 @@ class LanguageMode for currentRow in [0..@buffer.getLastRow()] [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? - @editSession.createFold(startRow, endRow) + @editor.createFold(startRow, endRow) # Unfolds all the foldable lines in the buffer. unfoldAll: -> for row in [@buffer.getLastRow()..0] - fold.destroy() for fold in @editSession.displayBuffer.foldsStartingAtBufferRow(row) + fold.destroy() for fold in @editor.displayBuffer.foldsStartingAtBufferRow(row) # Fold all comment and code blocks at a given indentLevel # @@ -114,8 +114,8 @@ class LanguageMode continue unless startRow? # assumption: startRow will always be the min indent level for the entire range - if @editSession.indentationForBufferRow(startRow) == indentLevel - @editSession.createFold(startRow, endRow) + if @editor.indentationForBufferRow(startRow) == indentLevel + @editor.createFold(startRow, endRow) # Given a buffer row, creates a fold at it. # @@ -126,14 +126,14 @@ class LanguageMode for currentRow in [bufferRow..0] [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? and startRow <= bufferRow <= endRow - fold = @editSession.displayBuffer.largestFoldStartingAtBufferRow(startRow) - return @editSession.createFold(startRow, endRow) unless fold + fold = @editor.displayBuffer.largestFoldStartingAtBufferRow(startRow) + return @editor.createFold(startRow, endRow) unless fold # Given a buffer row, this unfolds it. # # bufferRow - A {Number} indicating the buffer row unfoldBufferRow: (bufferRow) -> - @editSession.displayBuffer.largestFoldContainingBufferRow(bufferRow)?.destroy() + @editor.displayBuffer.largestFoldContainingBufferRow(bufferRow)?.destroy() # Find the row range for a fold at a given bufferRow. Will handle comments # and code. @@ -147,30 +147,30 @@ class LanguageMode rowRange rowRangeForCommentAtBufferRow: (bufferRow) -> - return unless @editSession.displayBuffer.tokenizedBuffer.lineForScreenRow(bufferRow).isComment() + return unless @editor.displayBuffer.tokenizedBuffer.lineForScreenRow(bufferRow).isComment() startRow = bufferRow for currentRow in [bufferRow-1..0] break if @buffer.isRowBlank(currentRow) - break unless @editSession.displayBuffer.tokenizedBuffer.lineForScreenRow(currentRow).isComment() + break unless @editor.displayBuffer.tokenizedBuffer.lineForScreenRow(currentRow).isComment() startRow = currentRow endRow = bufferRow for currentRow in [bufferRow+1..@buffer.getLastRow()] break if @buffer.isRowBlank(currentRow) - break unless @editSession.displayBuffer.tokenizedBuffer.lineForScreenRow(currentRow).isComment() + break unless @editor.displayBuffer.tokenizedBuffer.lineForScreenRow(currentRow).isComment() endRow = currentRow return [startRow, endRow] if startRow isnt endRow rowRangeForCodeFoldAtBufferRow: (bufferRow) -> return null unless @doesBufferRowStartFold(bufferRow) - startIndentLevel = @editSession.indentationForBufferRow(bufferRow) - scopes = @editSession.scopesForBufferPosition([bufferRow, 0]) - for row in [(bufferRow + 1)..@editSession.getLastBufferRow()] - continue if @editSession.isBufferRowBlank(row) - indentation = @editSession.indentationForBufferRow(row) + startIndentLevel = @editor.indentationForBufferRow(bufferRow) + scopes = @editor.scopesForBufferPosition([bufferRow, 0]) + for row in [(bufferRow + 1)..@editor.getLastBufferRow()] + continue if @editor.isBufferRowBlank(row) + indentation = @editor.indentationForBufferRow(row) if indentation <= startIndentLevel - includeRowInFold = indentation == startIndentLevel and @foldEndRegexForScopes(scopes)?.search(@editSession.lineForBufferRow(row)) + includeRowInFold = indentation == startIndentLevel and @foldEndRegexForScopes(scopes)?.search(@editor.lineForBufferRow(row)) foldEndRow = row if includeRowInFold break @@ -179,19 +179,19 @@ class LanguageMode [bufferRow, foldEndRow] doesBufferRowStartFold: (bufferRow) -> - return false if @editSession.isBufferRowBlank(bufferRow) - nextNonEmptyRow = @editSession.nextNonBlankBufferRow(bufferRow) + return false if @editor.isBufferRowBlank(bufferRow) + nextNonEmptyRow = @editor.nextNonBlankBufferRow(bufferRow) return false unless nextNonEmptyRow? - @editSession.indentationForBufferRow(nextNonEmptyRow) > @editSession.indentationForBufferRow(bufferRow) + @editor.indentationForBufferRow(nextNonEmptyRow) > @editor.indentationForBufferRow(bufferRow) # Find a row range for a 'paragraph' around specified bufferRow. # Right now, a paragraph is a block of text bounded by and empty line or a # block of text that is not the same type (comments next to source code). rowRangeForParagraphAtBufferRow: (bufferRow) -> - return unless /\w/.test(@editSession.lineForBufferRow(bufferRow)) + return unless /\w/.test(@editor.lineForBufferRow(bufferRow)) isRowComment = (row) => - @editSession.displayBuffer.tokenizedBuffer.lineForScreenRow(row).isComment() + @editor.displayBuffer.tokenizedBuffer.lineForScreenRow(row).isComment() if isRowComment(bufferRow) isOriginalRowComment = true @@ -199,22 +199,22 @@ class LanguageMode [firstRow, lastRow] = range or [bufferRow, bufferRow] else isOriginalRowComment = false - [firstRow, lastRow] = [0, @editSession.getLastBufferRow()-1] + [firstRow, lastRow] = [0, @editor.getLastBufferRow()-1] startRow = bufferRow while startRow > firstRow break if isRowComment(startRow - 1) != isOriginalRowComment - break unless /\w/.test(@editSession.lineForBufferRow(startRow - 1)) + break unless /\w/.test(@editor.lineForBufferRow(startRow - 1)) startRow-- endRow = bufferRow - lastRow = @editSession.getLastBufferRow() + lastRow = @editor.getLastBufferRow() while endRow < lastRow break if isRowComment(endRow + 1) != isOriginalRowComment - break unless /\w/.test(@editSession.lineForBufferRow(endRow + 1)) + break unless /\w/.test(@editor.lineForBufferRow(endRow + 1)) endRow++ - new Range([startRow, 0], [endRow, @editSession.lineLengthForBufferRow(endRow)]) + new Range([startRow, 0], [endRow, @editor.lineLengthForBufferRow(endRow)]) # Given a buffer row, this returns a suggested indentation level. # @@ -224,8 +224,8 @@ class LanguageMode # # Returns a {Number}. suggestedIndentForBufferRow: (bufferRow) -> - currentIndentLevel = @editSession.indentationForBufferRow(bufferRow) - scopes = @editSession.scopesForBufferPosition([bufferRow, 0]) + currentIndentLevel = @editor.indentationForBufferRow(bufferRow) + scopes = @editor.scopesForBufferPosition([bufferRow, 0]) return currentIndentLevel unless increaseIndentRegex = @increaseIndentRegexForScopes(scopes) currentLine = @buffer.lineForRow(bufferRow) @@ -233,8 +233,8 @@ class LanguageMode return currentIndentLevel unless precedingRow? precedingLine = @buffer.lineForRow(precedingRow) - desiredIndentLevel = @editSession.indentationForBufferRow(precedingRow) - desiredIndentLevel += 1 if increaseIndentRegex.test(precedingLine) and not @editSession.isBufferRowCommented(precedingRow) + desiredIndentLevel = @editor.indentationForBufferRow(precedingRow) + desiredIndentLevel += 1 if increaseIndentRegex.test(precedingLine) and not @editor.isBufferRowCommented(precedingRow) return desiredIndentLevel unless decreaseIndentRegex = @decreaseIndentRegexForScopes(scopes) desiredIndentLevel -= 1 if decreaseIndentRegex.test(currentLine) @@ -248,7 +248,7 @@ class LanguageMode # # Returns a {Number} of the indent level of the block of lines. minIndentLevelForRowRange: (startRow, endRow) -> - indents = (@editSession.indentationForBufferRow(row) for row in [startRow..endRow] when not @editSession.isBufferRowBlank(row)) + indents = (@editor.indentationForBufferRow(row) for row in [startRow..endRow] when not @editor.isBufferRowBlank(row)) indents = [0] unless indents.length Math.min(indents...) @@ -264,13 +264,13 @@ class LanguageMode # bufferRow - The row {Number} autoIndentBufferRow: (bufferRow) -> indentLevel = @suggestedIndentForBufferRow(bufferRow) - @editSession.setIndentationForBufferRow(bufferRow, indentLevel) + @editor.setIndentationForBufferRow(bufferRow, indentLevel) # Given a buffer row, this decreases the indentation. # # bufferRow - The row {Number} autoDecreaseIndentForBufferRow: (bufferRow) -> - scopes = @editSession.scopesForBufferPosition([bufferRow, 0]) + scopes = @editor.scopesForBufferPosition([bufferRow, 0]) increaseIndentRegex = @increaseIndentRegexForScopes(scopes) decreaseIndentRegex = @decreaseIndentRegexForScopes(scopes) return unless increaseIndentRegex and decreaseIndentRegex @@ -278,16 +278,16 @@ class LanguageMode line = @buffer.lineForRow(bufferRow) return unless decreaseIndentRegex.test(line) - currentIndentLevel = @editSession.indentationForBufferRow(bufferRow) + currentIndentLevel = @editor.indentationForBufferRow(bufferRow) return if currentIndentLevel is 0 precedingRow = @buffer.previousNonBlankRow(bufferRow) return unless precedingRow? precedingLine = @buffer.lineForRow(precedingRow) - desiredIndentLevel = @editSession.indentationForBufferRow(precedingRow) + desiredIndentLevel = @editor.indentationForBufferRow(precedingRow) desiredIndentLevel -= 1 unless increaseIndentRegex.test(precedingLine) if desiredIndentLevel >= 0 and desiredIndentLevel < currentIndentLevel - @editSession.setIndentationForBufferRow(bufferRow, desiredIndentLevel) + @editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel) tokenizeLine: (line, stack, firstLine) -> {tokens, stack} = @grammar.tokenizeLine(line, stack, firstLine) diff --git a/src/pane.coffee b/src/pane.coffee index 514e37e93..a33c7388b 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -7,7 +7,7 @@ PaneColumn = require './pane-column' # Public: A container which can contains multiple items to be switched between. # -# Items can be almost anything however most commonly they're {Editor}s. +# Items can be almost anything however most commonly they're {EditorView}s. # # Most packages won't need to use this class, unless you're interested in # building a package that deals with switching between panes or tiems. diff --git a/src/project.coffee b/src/project.coffee index 4ad5065ae..9b4d477a5 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -7,7 +7,7 @@ Q = require 'q' telepath = require 'telepath' TextBuffer = require './text-buffer' -EditSession = require './edit-session' +Editor = require './editor' {Emitter} = require 'emissary' Directory = require './directory' Task = require './task' @@ -37,7 +37,7 @@ class Project extends telepath.Model buffer.once 'destroyed', => @removeBuffer(buffer) @openers = [] - @editSessions = [] + @editors = [] @setPath(@path) # Private: Called by telepath. @@ -52,7 +52,7 @@ class Project extends telepath.Model # Private: destroy: -> - editSession.destroy() for editSession in @getEditSessions() + editor.destroy() for editor in @getEditSessions() buffer.release() for buffer in @getBuffers() @destroyRepo() @@ -133,14 +133,14 @@ class Project extends telepath.Model @rootDirectory?.contains(pathToCheck) ? false # Public: Given a path to a file, this constructs and associates a new - # {EditSession}, showing the file. + # {Editor}, showing the file. # # * filePath: # The {String} path of the file to associate with - # * editSessionOptions: - # Options that you can pass to the {EditSession} constructor + # * options: + # Options that you can pass to the {Editor} constructor # - # Returns a promise that resolves to an {EditSession}. + # Returns a promise that resolves to an {Editor}. open: (filePath, options={}) -> filePath = @resolve(filePath) resource = null @@ -160,20 +160,20 @@ class Project extends telepath.Model @buildEditSessionForBuffer(@bufferForPathSync(filePath), options) - # Public: Retrieves all {EditSession}s for all open files. + # Public: Retrieves all {Editor}s for all open files. # - # Returns an {Array} of {EditSession}s. + # Returns an {Array} of {Editor}s. getEditSessions: -> - new Array(@editSessions...) + new Array(@editors...) - # Public: Add the given {EditSession}. - addEditSession: (editSession) -> - @editSessions.push editSession - @emit 'edit-session-created', editSession + # Public: Add the given {Editor}. + addEditSession: (editor) -> + @editors.push editor + @emit 'editor-created', editor - # Public: Return and removes the given {EditSession}. - removeEditSession: (editSession) -> - _.remove(@editSessions, editSession) + # Public: Return and removes the given {Editor}. + removeEditSession: (editor) -> + _.remove(@editors, editor) # Private: Retrieves all the {TextBuffer}s in the project; that is, the # buffers for all open files. @@ -331,15 +331,15 @@ class Project extends telepath.Model deferred.promise # Private: - buildEditSessionForBuffer: (buffer, editSessionOptions) -> - editSession = new EditSession(_.extend({buffer}, editSessionOptions)) - @addEditSession(editSession) - editSession + buildEditSessionForBuffer: (buffer, editorOptions) -> + editor = new Editor(_.extend({buffer}, editorOptions)) + @addEditSession(editor) + editor # Private: eachEditSession: (callback) -> - callback(editSession) for editSession in @getEditSessions() - @on 'edit-session-created', (editSession) -> callback(editSession) + callback(editor) for editor in @getEditSessions() + @on 'editor-created', (editor) -> callback(editor) # Private: eachBuffer: (args...) -> diff --git a/src/root-view.coffee b/src/root-view.coffee index d1d071272..fd8e02417 100644 --- a/src/root-view.coffee +++ b/src/root-view.coffee @@ -5,12 +5,12 @@ Q = require 'q' _ = require 'underscore-plus' fs = require 'fs-plus' telepath = require 'telepath' -Editor = require './editor' +EditorView = require './editor-view' Pane = require './pane' PaneColumn = require './pane-column' PaneRow = require './pane-row' PaneContainer = require './pane-container' -EditSession = require './edit-session' +Editor = require './editor' # Public: The container for the entire Atom application. # @@ -38,7 +38,7 @@ EditSession = require './edit-session' # module.exports = class RootView extends View - registerDeserializers(this, Pane, PaneRow, PaneColumn, Editor) + registerDeserializers(this, Pane, PaneRow, PaneColumn, EditorView) @version: 1 @@ -169,26 +169,26 @@ class RootView extends View # * options # + initialLine: The buffer line number to open to. # - # Returns a promise that resolves to the {EditSession} for the file URI. + # Returns a promise that resolves to the {Editor} for the file URI. open: (filePath, options={}) -> changeFocus = options.changeFocus ? true filePath = project.resolve(filePath) initialLine = options.initialLine activePane = @getActivePane() - editSession = activePane.itemForUri(project.relativize(filePath)) if activePane and filePath - promise = project.open(filePath, {initialLine}) if not editSession + editor = activePane.itemForUri(project.relativize(filePath)) if activePane and filePath + promise = project.open(filePath, {initialLine}) if not editor - Q(editSession ? promise) - .then (editSession) => + Q(editor ? promise) + .then (editor) => if not activePane - activePane = new Pane(editSession) + activePane = new Pane(editor) @panes.setRoot(activePane) - activePane.showItem(editSession) + activePane.showItem(editor) activePane.focus() if changeFocus @trigger "uri-opened" - editSession + editor .catch (error) -> console.error(error.stack ? error) @@ -249,7 +249,7 @@ class RootView extends View setTitle: (title) -> document.title = title - # Private: Returns an Array of all of the application's {Editor}s. + # Private: Returns an Array of all of the application's {EditorView}s. getEditors: -> @panes.find('.pane > .item-views > .editor').map(-> $(this).view()).toArray() @@ -259,7 +259,7 @@ class RootView extends View getModifiedBuffers: -> modifiedBuffers = [] for pane in @getPanes() - for item in pane.getItems() when item instanceof EditSession + for item in pane.getItems() when item instanceof Editor modifiedBuffers.push item.buffer if item.buffer.isModified() modifiedBuffers @@ -267,7 +267,7 @@ class RootView extends View # # Returns an {Array} of {String}s. getOpenBufferPaths: -> - _.uniq(_.flatten(@getEditors().map (editor) -> editor.getOpenBufferPaths())) + _.uniq(_.flatten(@getEditors().map (editorView) -> editorView.getOpenBufferPaths())) # Public: Returns the currently focused {Pane}. getActivePane: -> @@ -308,14 +308,14 @@ class RootView extends View indexOfPane: (pane) -> @panes.indexOfPane(pane) - # Private: Fires a callback on each open {Editor}. + # Private: Fires a callback on each open {EditorView}. eachEditor: (callback) -> callback(editor) for editor in @getEditors() attachedCallback = (e, editor) -> callback(editor) @on('editor:attached', attachedCallback) off: => @off('editor:attached', attachedCallback) - # Public: Fires a callback on each open {EditSession}. + # Public: Fires a callback on each open {Editor}. eachEditSession: (callback) -> project.eachEditSession(callback) @@ -325,6 +325,6 @@ class RootView extends View # Private: Destroys everything. remove: -> - editor.remove() for editor in @getEditors() + editorView.remove() for editorView in @getEditors() project?.destroy() super diff --git a/src/select-list.coffee b/src/select-list.coffee index 78a305945..3c858758d 100644 --- a/src/select-list.coffee +++ b/src/select-list.coffee @@ -1,5 +1,5 @@ {$, View} = require './space-pen-extensions' -Editor = require './editor' +EditorView = require './editor-view' fuzzyFilter = require('fuzzaldrin').filter # Public: Provides a widget for users to make a selection from a list of @@ -10,7 +10,7 @@ class SelectList extends View # Private: @content: -> @div class: @viewClass(), => - @subview 'miniEditor', new Editor(mini: true) + @subview 'miniEditor', new EditorView(mini: true) @div class: 'error-message', outlet: 'error' @div class: 'loading', outlet: 'loadingArea', => @span class: 'loading-message', outlet: 'loading' diff --git a/src/selection-view.coffee b/src/selection-view.coffee index e01ea384d..6dc301bab 100644 --- a/src/selection-view.coffee +++ b/src/selection-view.coffee @@ -11,12 +11,12 @@ class SelectionView extends View regions: null needsRemoval: false - initialize: ({@editor, @selection} = {}) -> + initialize: ({@editorView, @selection} = {}) -> @regions = [] - @selection.on 'screen-range-changed', => @editor.requestDisplayUpdate() + @selection.on 'screen-range-changed', => @editorView.requestDisplayUpdate() @selection.on 'destroyed', => @needsRemoval = true - @editor.requestDisplayUpdate() + @editorView.requestDisplayUpdate() if @selection.marker.isRemote() @addClass("site-#{@selection.marker.getOriginSiteId()}") @@ -26,7 +26,7 @@ class SelectionView extends View range = @getScreenRange() @trigger 'selection:changed' - @editor.highlightFoldsContainingBufferRange(@getBufferRange()) + @editorView.highlightFoldsContainingBufferRange(@getBufferRange()) return if range.isEmpty() rowSpan = range.end.row - range.start.row @@ -40,11 +40,11 @@ class SelectionView extends View @appendRegion(1, { row: range.end.row, column: 0 }, range.end) appendRegion: (rows, start, end) -> - { lineHeight, charWidth } = @editor - css = @editor.pixelPositionForScreenPosition(start) + { lineHeight, charWidth } = @editorView + css = @editorView.pixelPositionForScreenPosition(start) css.height = lineHeight * rows if end - css.width = @editor.pixelPositionForScreenPosition(end).left - css.left + css.width = @editorView.pixelPositionForScreenPosition(end).left - css.left else css.right = 0 @@ -57,7 +57,7 @@ class SelectionView extends View startRow = start.row endRow = end.row endRow-- if end.column == 0 - @editor.pixelPositionForScreenPosition([((startRow + endRow + 1) / 2), start.column]) + @editorView.pixelPositionForScreenPosition([((startRow + endRow + 1) / 2), start.column]) clearRegions: -> region.remove() for region in @regions @@ -85,5 +85,5 @@ class SelectionView extends View @removeClass('highlighted') remove: -> - @editor.removeSelectionView(this) + @editorView.removeSelectionView(this) super diff --git a/src/selection.coffee b/src/selection.coffee index f0a0dc623..99c790994 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -2,27 +2,27 @@ {Emitter} = require 'emissary' {pick} = require 'underscore-plus' -# Public: Represents a selection in the {EditSession}. +# Public: Represents a selection in the {Editor}. module.exports = class Selection Emitter.includeInto(this) cursor: null marker: null - editSession: null + editor: null initialScreenRange: null wordwise: false needsAutoscroll: null # Private: - constructor: ({@cursor, @marker, @editSession}) -> + constructor: ({@cursor, @marker, @editor}) -> @cursor.selection = this @marker.on 'changed', => @screenRangeChanged() @marker.on 'destroyed', => @destroyed = true - @editSession.removeSelection(this) - @emit 'destroyed' unless @editSession.destroyed + @editor.removeSelection(this) + @emit 'destroyed' unless @editor.destroyed # Private: destroy: -> @@ -65,7 +65,7 @@ class Selection # * options: # + A hash of options matching those found in {.setBufferRange} setScreenRange: (screenRange, options) -> - @setBufferRange(@editSession.bufferRangeForScreenRange(screenRange), options) + @setBufferRange(@editor.bufferRangeForScreenRange(screenRange), options) # Public: Returns the buffer {Range} for the selection. getBufferRange: -> @@ -79,12 +79,12 @@ class Selection # + preserveFolds: # if `true`, the fold settings are preserved after the selection moves # + autoscroll: - # if `true`, the {EditSession} scrolls to the new selection + # if `true`, the {Editor} scrolls to the new selection setBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) @needsAutoscroll = options.autoscroll options.isReversed ?= @isReversed() - @editSession.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds + @editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds @modifySelection => @cursor.needsAutoscroll = false if options.autoscroll? @marker.setBufferRange(bufferRange, options) @@ -102,7 +102,7 @@ class Selection # Public: Returns the text in the selection. getText: -> - @editSession.buffer.getTextInRange(@getBufferRange()) + @editor.buffer.getTextInRange(@getBufferRange()) # Public: Clears the selection, moving the marker to the head. clear: -> @@ -132,7 +132,7 @@ class Selection # * row: # The line Number to select (default: the row of the cursor) selectLine: (row=@cursor.getBufferPosition().row) -> - range = @editSession.bufferRangeForBufferRow(row, includeNewline: true) + range = @editor.bufferRangeForBufferRow(row, includeNewline: true) @setBufferRange(range) @linewise = true @wordwise = false @@ -202,7 +202,7 @@ class Selection # Public: Selects all the text in the buffer. selectAll: -> - @setBufferRange(@editSession.buffer.getRange(), autoscroll: false) + @setBufferRange(@editor.buffer.getRange(), autoscroll: false) # Public: Selects all the text from the current cursor position to the # beginning of the line. @@ -247,17 +247,17 @@ class Selection range = (@getGoalBufferRange() ? @getBufferRange()).copy() nextRow = range.end.row + 1 - for row in [nextRow..@editSession.getLastBufferRow()] + for row in [nextRow..@editor.getLastBufferRow()] range.start.row = row range.end.row = row - clippedRange = @editSession.clipBufferRange(range) + clippedRange = @editor.clipBufferRange(range) if range.isEmpty() continue if range.end.column > 0 and clippedRange.end.column is 0 else continue if clippedRange.isEmpty() - @editSession.addSelectionForBufferRange(range, goalBufferRange: range) + @editor.addSelectionForBufferRange(range, goalBufferRange: range) break # Public: @@ -274,14 +274,14 @@ class Selection for row in [previousRow..0] range.start.row = row range.end.row = row - clippedRange = @editSession.clipBufferRange(range) + clippedRange = @editor.clipBufferRange(range) if range.isEmpty() continue if range.end.column > 0 and clippedRange.end.column is 0 else continue if clippedRange.isEmpty() - @editSession.addSelectionForBufferRange(range, goalBufferRange: range) + @editor.addSelectionForBufferRange(range, goalBufferRange: range) break # Public: Replaces text at the current selection. @@ -302,7 +302,7 @@ class Selection # if `skip`, skips the undo stack for this operation. insertText: (text, options={}) -> oldBufferRange = @getBufferRange() - @editSession.destroyFoldsContainingBufferRow(oldBufferRange.end.row) + @editor.destroyFoldsContainingBufferRow(oldBufferRange.end.row) wasReversed = @isReversed() @clear() @cursor.needsAutoscroll = @cursor.isLastCursor() @@ -310,18 +310,18 @@ class Selection if options.indentBasis? and not options.autoIndent text = @normalizeIndents(text, options.indentBasis) - newBufferRange = @editSession.buffer.change(oldBufferRange, text, pick(options, 'undo')) + newBufferRange = @editor.buffer.change(oldBufferRange, text, pick(options, 'undo')) if options.select @setBufferRange(newBufferRange, isReversed: wasReversed) else @cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed if options.autoIndent - @editSession.autoIndentBufferRow(row) for row in newBufferRange.getRows() + @editor.autoIndentBufferRow(row) for row in newBufferRange.getRows() else if options.autoIndentNewline and text == '\n' - @editSession.autoIndentBufferRow(newBufferRange.end.row) + @editor.autoIndentBufferRow(newBufferRange.end.row) else if options.autoDecreaseIndent and /\S/.test text - @editSession.autoDecreaseIndentForBufferRow(newBufferRange.start.row) + @editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row) newBufferRange @@ -336,9 +336,9 @@ class Selection isCursorInsideExistingLine = /\S/.test(textPrecedingCursor) lines = text.split('\n') - firstLineIndentLevel = @editSession.indentLevelForLine(lines[0]) + firstLineIndentLevel = @editor.indentLevelForLine(lines[0]) if isCursorInsideExistingLine - minimumIndentLevel = @editSession.indentationForBufferRow(@cursor.getBufferRow()) + minimumIndentLevel = @editor.indentationForBufferRow(@cursor.getBufferRow()) else minimumIndentLevel = @cursor.getIndentLevel() @@ -349,7 +349,7 @@ class Selection else if line == '' # remove all indentation from empty lines indentLevel = 0 else - lineIndentLevel = @editSession.indentLevelForLine(lines[i]) + lineIndentLevel = @editor.indentLevelForLine(lines[i]) indentLevel = minimumIndentLevel + (lineIndentLevel - indentBasis) normalizedLines.push(@setIndentationForLine(line, indentLevel)) @@ -361,19 +361,19 @@ class Selection # * options - A hash with one key, # + autoIndent: # If `true`, the indentation is performed appropriately. Otherwise, - # {EditSession.getTabText} is used + # {Editor.getTabText} is used indent: ({ autoIndent }={})-> { row, column } = @cursor.getBufferPosition() if @isEmpty() @cursor.skipLeadingWhitespace() - desiredIndent = @editSession.suggestedIndentForBufferRow(row) + desiredIndent = @editor.suggestedIndentForBufferRow(row) delta = desiredIndent - @cursor.getIndentLevel() if autoIndent and delta > 0 - @insertText(@editSession.buildIndentString(delta)) + @insertText(@editor.buildIndentString(delta)) else - @insertText(@editSession.getTabText()) + @insertText(@editor.getTabText()) else @indentSelectedRows() @@ -381,18 +381,18 @@ class Selection indentSelectedRows: -> [start, end] = @getBufferRowRange() for row in [start..end] - @editSession.buffer.insert([row, 0], @editSession.getTabText()) unless @editSession.buffer.lineLengthForRow(row) == 0 + @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) == 0 # Public: ? setIndentationForLine: (line, indentLevel) -> desiredIndentLevel = Math.max(0, indentLevel) - desiredIndentString = @editSession.buildIndentString(desiredIndentLevel) + desiredIndentString = @editor.buildIndentString(desiredIndentLevel) line.replace(/^[\t ]*/, desiredIndentString) # Public: Removes the first character before the selection if the selection # is empty otherwise it deletes the selection. backspace: -> - @selectLeft() if @isEmpty() and not @editSession.isFoldedAtScreenRow(@cursor.getScreenRow()) + @selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow()) @deleteSelectedText() # Public: Removes from the start of the selection to the beginning of the @@ -414,7 +414,7 @@ class Selection # selection if the selection is empty. delete: -> if @isEmpty() - if @cursor.isAtEndOfLine() and fold = @editSession.largestFoldStartingAtScreenRow(@cursor.getScreenRow() + 1) + if @cursor.isAtEndOfLine() and fold = @editor.largestFoldStartingAtScreenRow(@cursor.getScreenRow() + 1) @selectToBufferPosition(fold.getBufferRange().end) else @selectRight() @@ -429,9 +429,9 @@ class Selection # Public: Removes only the selected text. deleteSelectedText: -> bufferRange = @getBufferRange() - if bufferRange.isEmpty() and fold = @editSession.largestFoldContainingBufferRow(bufferRange.start.row) + if bufferRange.isEmpty() and fold = @editor.largestFoldContainingBufferRow(bufferRange.start.row) bufferRange = bufferRange.union(fold.getBufferRange(includeNewline: true)) - @editSession.buffer.delete(bufferRange) unless bufferRange.isEmpty() + @editor.buffer.delete(bufferRange) unless bufferRange.isEmpty() @cursor?.setBufferPosition(bufferRange.start) # Public: Removes the line at the beginning of the selection if the selection @@ -440,18 +440,18 @@ class Selection deleteLine: -> if @isEmpty() start = @cursor.getScreenRow() - range = @editSession.bufferRowsForScreenRows(start, start + 1) + range = @editor.bufferRowsForScreenRows(start, start + 1) if range[1] > range[0] - @editSession.buffer.deleteRows(range[0], range[1] - 1) + @editor.buffer.deleteRows(range[0], range[1] - 1) else - @editSession.buffer.deleteRow(range[0]) + @editor.buffer.deleteRow(range[0]) else range = @getBufferRange() start = range.start.row end = range.end.row - if end isnt @editSession.buffer.getLastRow() and range.end.column is 0 + if end isnt @editor.buffer.getLastRow() and range.end.column is 0 end-- - @editSession.buffer.deleteRows(start, end) + @editor.buffer.deleteRows(start, end) # Public: Joins the current line with the one below it. # @@ -459,16 +459,16 @@ class Selection joinLine: -> selectedRange = @getBufferRange() if selectedRange.isEmpty() - return if selectedRange.start.row is @editSession.buffer.getLastRow() + return if selectedRange.start.row is @editor.buffer.getLastRow() else - joinMarker = @editSession.markBufferRange(selectedRange, invalidationStrategy: 'never') + joinMarker = @editor.markBufferRange(selectedRange, invalidationStrategy: 'never') rowCount = Math.max(1, selectedRange.getRowCount() - 1) for row in [0...rowCount] @cursor.setBufferPosition([selectedRange.start.row]) @cursor.moveToEndOfLine() nextRow = selectedRange.start.row + 1 - if nextRow <= @editSession.buffer.getLastRow() and @editSession.buffer.lineLengthForRow(nextRow) > 0 + if nextRow <= @editor.buffer.getLastRow() and @editor.buffer.lineLengthForRow(nextRow) > 0 @insertText(' ') @cursor.moveToEndOfLine() @modifySelection => @@ -484,8 +484,8 @@ class Selection # Public: Removes one level of indent from the currently selected rows. outdentSelectedRows: -> [start, end] = @getBufferRowRange() - buffer = @editSession.buffer - leadingTabRegex = new RegExp("^ {1,#{@editSession.getTabLength()}}|\t") + buffer = @editor.buffer + leadingTabRegex = new RegExp("^ {1,#{@editor.getTabLength()}}|\t") for row in [start..end] if matchLength = buffer.lineForRow(row).match(leadingTabRegex)?[0].length buffer.delete [[row, 0], [row, matchLength]] @@ -494,7 +494,7 @@ class Selection # by the relevant grammars. autoIndentSelectedRows: -> [start, end] = @getBufferRowRange() - @editSession.autoIndentBufferRows(start, end) + @editor.autoIndentBufferRows(start, end) # Public: Wraps the selected lines in comments if they aren't currently part # of a comment. @@ -503,7 +503,7 @@ class Selection # # Returns an Array of the commented {Range}s. toggleLineComments: -> - @editSession.toggleLineCommentsForBufferRows(@getBufferRowRange()...) + @editor.toggleLineCommentsForBufferRows(@getBufferRowRange()...) # Public: Cuts the selection until the end of the line. # @@ -527,19 +527,19 @@ class Selection # ? copy: (maintainPasteboard=false) -> return if @isEmpty() - text = @editSession.buffer.getTextInRange(@getBufferRange()) + text = @editor.buffer.getTextInRange(@getBufferRange()) if maintainPasteboard [currentText, metadata] = atom.pasteboard.read() text = currentText + '\n' + text else - metadata = { indentBasis: @editSession.indentationForBufferRow(@getBufferRange().start.row) } + metadata = { indentBasis: @editor.indentationForBufferRow(@getBufferRange().start.row) } atom.pasteboard.write(text, metadata) # Public: Creates a fold containing the current selection. fold: -> range = @getBufferRange() - @editSession.createFold(range.start.row, range.end.row) + @editor.createFold(range.start.row, range.end.row) @cursor.setBufferPosition([range.end.row + 1, 0]) # Public: ? diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 52d494fb0..e90c836fb 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -126,12 +126,12 @@ class TextBuffer extends telepath.Model # Identifies if the buffer belongs to multiple editors. # - # For example, if the {Editor} was split. + # For example, if the {EditorView} was split. # # Returns a {Boolean}. hasMultipleEditors: -> @refcount > 1 - # Reloads a file in the {EditSession}. + # Reloads a file in the {Editor}. # # Sets the buffer's content to the cached disk contents reload: -> diff --git a/src/window.coffee b/src/window.coffee index 8b5e001f0..1bd367c69 100644 --- a/src/window.coffee +++ b/src/window.coffee @@ -49,7 +49,7 @@ window.startEditorWindow = -> atom.restoreDimensions() atom.config.load() atom.config.setDefaults('core', require('./root-view').configDefaults) - atom.config.setDefaults('editor', require('./editor').configDefaults) + atom.config.setDefaults('editor', require('./editor-view').configDefaults) atom.keymap.loadBundledKeymaps() atom.themes.loadBaseStylesheets() atom.packages.loadPackages()