mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-21 16:08:24 +03:00
4ca00f7347
Instead of marker-added and marker-removed events which are emitted when markers are created/invalidated/revalidated/destroyed, we now just have marker-created events that are triggered *only* when markers are created for the first time. The marker itself emits a 'destroyed' event when it is destroyed. The marker already notifies observers when its validation status changes, so that's covered.
1358 lines
54 KiB
CoffeeScript
1358 lines
54 KiB
CoffeeScript
Project = require 'project'
|
|
Buffer = require 'text-buffer'
|
|
fsUtils = require 'fs-utils'
|
|
_ = require 'underscore'
|
|
|
|
describe 'Buffer', ->
|
|
[filePath, fileContents, buffer] = []
|
|
|
|
beforeEach ->
|
|
filePath = require.resolve('fixtures/sample.js')
|
|
fileContents = fsUtils.read(filePath)
|
|
buffer = project.bufferForPath(filePath)
|
|
|
|
afterEach ->
|
|
buffer?.release()
|
|
|
|
describe 'constructor', ->
|
|
beforeEach ->
|
|
buffer.release()
|
|
buffer = null
|
|
|
|
describe "when given a path", ->
|
|
describe "when a file exists for the path", ->
|
|
it "loads the contents of that file", ->
|
|
filePath = require.resolve 'fixtures/sample.txt'
|
|
buffer = project.bufferForPath(filePath)
|
|
expect(buffer.getText()).toBe fsUtils.read(filePath)
|
|
|
|
it "is not modified and has no undo history", ->
|
|
buffer = project.bufferForPath(filePath)
|
|
expect(buffer.isModified()).toBeFalsy()
|
|
expect(buffer.undoManager.undoHistory.length).toBe 0
|
|
|
|
describe "when no file exists for the path", ->
|
|
it "is modified and is initially empty", ->
|
|
filePath = "does-not-exist.txt"
|
|
expect(fsUtils.exists(filePath)).toBeFalsy()
|
|
buffer = project.bufferForPath(filePath)
|
|
expect(buffer.isModified()).toBeTruthy()
|
|
expect(buffer.getText()).toBe ''
|
|
|
|
describe "when no path is given", ->
|
|
it "creates an empty buffer", ->
|
|
buffer = project.bufferForPath(null)
|
|
expect(buffer .getText()).toBe ""
|
|
|
|
describe "path-changed event", ->
|
|
[path, newPath, bufferToChange, eventHandler] = []
|
|
|
|
beforeEach ->
|
|
path = fsUtils.join(fsUtils.resolveOnLoadPath("fixtures"), "atom-manipulate-me")
|
|
newPath = "#{path}-i-moved"
|
|
fsUtils.write(path, "")
|
|
bufferToChange = project.bufferForPath(path)
|
|
eventHandler = jasmine.createSpy('eventHandler')
|
|
bufferToChange.on 'path-changed', eventHandler
|
|
|
|
afterEach ->
|
|
bufferToChange.destroy()
|
|
fsUtils.remove(path) if fsUtils.exists(path)
|
|
fsUtils.remove(newPath) if fsUtils.exists(newPath)
|
|
|
|
it "triggers a `path-changed` event when path is changed", ->
|
|
bufferToChange.saveAs(newPath)
|
|
expect(eventHandler).toHaveBeenCalledWith(bufferToChange)
|
|
|
|
it "triggers a `path-changed` event when the file is moved", ->
|
|
jasmine.unspy(window, "setTimeout")
|
|
|
|
fsUtils.remove(newPath) if fsUtils.exists(newPath)
|
|
fsUtils.move(path, newPath)
|
|
|
|
waitsFor "buffer path change", ->
|
|
eventHandler.callCount > 0
|
|
|
|
runs ->
|
|
expect(eventHandler).toHaveBeenCalledWith(bufferToChange)
|
|
|
|
describe "when the buffer's on-disk contents change", ->
|
|
path = null
|
|
beforeEach ->
|
|
path = "/tmp/tmp.txt"
|
|
fsUtils.write(path, "first")
|
|
buffer.release()
|
|
buffer = project.bufferForPath(path).retain()
|
|
|
|
afterEach ->
|
|
buffer.release()
|
|
buffer = null
|
|
fsUtils.remove(path) if fsUtils.exists(path)
|
|
|
|
it "does not trigger a change event when Atom modifies the file", ->
|
|
buffer.insert([0,0], "HELLO!")
|
|
changeHandler = jasmine.createSpy("buffer changed")
|
|
buffer.on "changed", changeHandler
|
|
buffer.save()
|
|
|
|
waits 30
|
|
runs ->
|
|
expect(changeHandler).not.toHaveBeenCalled()
|
|
|
|
describe "when the buffer is in an unmodified state before the on-disk change", ->
|
|
it "changes the memory contents of the buffer to match the new disk contents and triggers a 'changed' event", ->
|
|
changeHandler = jasmine.createSpy('changeHandler')
|
|
buffer.on 'changed', changeHandler
|
|
fsUtils.write(path, "second")
|
|
|
|
expect(changeHandler.callCount).toBe 0
|
|
waitsFor "file to trigger change event", ->
|
|
changeHandler.callCount > 0
|
|
|
|
runs ->
|
|
[event] = changeHandler.argsForCall[0]
|
|
expect(event.oldRange).toEqual [[0, 0], [0, 5]]
|
|
expect(event.newRange).toEqual [[0, 0], [0, 6]]
|
|
expect(event.oldText).toBe "first"
|
|
expect(event.newText).toBe "second"
|
|
expect(buffer.isModified()).toBeFalsy()
|
|
|
|
describe "when the buffer's memory contents differ from the *previous* disk contents", ->
|
|
it "leaves the buffer in a modified state (does not update its memory contents)", ->
|
|
fileChangeHandler = jasmine.createSpy('fileChange')
|
|
buffer.file.on 'contents-changed', fileChangeHandler
|
|
|
|
buffer.insert([0, 0], "a change")
|
|
fsUtils.write(path, "second")
|
|
|
|
expect(fileChangeHandler.callCount).toBe 0
|
|
waitsFor "file to trigger 'contents-changed' event", ->
|
|
fileChangeHandler.callCount > 0
|
|
|
|
runs ->
|
|
expect(buffer.isModified()).toBeTruthy()
|
|
|
|
it "fires a single contents-conflicted event", ->
|
|
buffer.insert([0, 0], "a change")
|
|
buffer.save()
|
|
buffer.insert([0, 0], "a second change")
|
|
|
|
handler = jasmine.createSpy('fileChange')
|
|
fsUtils.write(path, "second")
|
|
buffer.on 'contents-conflicted', handler
|
|
|
|
expect(handler.callCount).toBe 0
|
|
waitsFor ->
|
|
handler.callCount > 0
|
|
|
|
runs ->
|
|
expect(handler.callCount).toBe 1
|
|
|
|
describe "when the buffer's file is deleted (via another process)", ->
|
|
[path, bufferToDelete] = []
|
|
|
|
beforeEach ->
|
|
path = "/tmp/atom-file-to-delete.txt"
|
|
fsUtils.write(path, 'delete me')
|
|
bufferToDelete = project.bufferForPath(path)
|
|
path = bufferToDelete.getPath() # symlinks may have been converted
|
|
|
|
expect(bufferToDelete.getPath()).toBe path
|
|
expect(bufferToDelete.isModified()).toBeFalsy()
|
|
|
|
removeHandler = jasmine.createSpy('removeHandler')
|
|
bufferToDelete.file.on 'removed', removeHandler
|
|
fsUtils.remove(path)
|
|
waitsFor "file to be removed", ->
|
|
removeHandler.callCount > 0
|
|
|
|
afterEach ->
|
|
bufferToDelete.destroy()
|
|
|
|
it "retains its path and reports the buffer as modified", ->
|
|
expect(bufferToDelete.getPath()).toBe path
|
|
expect(bufferToDelete.isModified()).toBeTruthy()
|
|
|
|
it "resumes watching of the file when it is re-saved", ->
|
|
bufferToDelete.save()
|
|
expect(bufferToDelete.fileExists()).toBeTruthy()
|
|
expect(bufferToDelete.isInConflict()).toBeFalsy()
|
|
|
|
fsUtils.write(path, 'moo')
|
|
|
|
changeHandler = jasmine.createSpy('changeHandler')
|
|
bufferToDelete.on 'changed', changeHandler
|
|
waitsFor 'change event', ->
|
|
changeHandler.callCount > 0
|
|
|
|
describe "modified status", ->
|
|
it "reports the modified status changing to true or false after the user changes buffer", ->
|
|
modifiedHandler = jasmine.createSpy("modifiedHandler")
|
|
buffer.on 'modified-status-changed', modifiedHandler
|
|
|
|
expect(buffer.isModified()).toBeFalsy()
|
|
buffer.insert([0,0], "hi")
|
|
expect(buffer.isModified()).toBe true
|
|
|
|
advanceClock(buffer.stoppedChangingDelay)
|
|
expect(modifiedHandler).toHaveBeenCalledWith(true)
|
|
|
|
modifiedHandler.reset()
|
|
buffer.insert([0,2], "ho")
|
|
advanceClock(buffer.stoppedChangingDelay)
|
|
expect(modifiedHandler).not.toHaveBeenCalled()
|
|
|
|
modifiedHandler.reset()
|
|
buffer.undo()
|
|
buffer.undo()
|
|
advanceClock(buffer.stoppedChangingDelay)
|
|
expect(modifiedHandler).toHaveBeenCalledWith(false)
|
|
|
|
it "reports the modified status changing to true after the underlying file is deleted", ->
|
|
buffer.release()
|
|
filePath = "/tmp/atom-tmp-file"
|
|
fsUtils.write(filePath, 'delete me')
|
|
buffer = project.bufferForPath(filePath)
|
|
modifiedHandler = jasmine.createSpy("modifiedHandler")
|
|
buffer.on 'modified-status-changed', modifiedHandler
|
|
|
|
fsUtils.remove(filePath)
|
|
|
|
waitsFor "modified status to change", -> modifiedHandler.callCount
|
|
runs -> expect(buffer.isModified()).toBe true
|
|
|
|
it "reports the modified status changing to false after a modified buffer is saved", ->
|
|
filePath = "/tmp/atom-tmp-file"
|
|
fsUtils.write(filePath, '')
|
|
buffer.release()
|
|
buffer = project.bufferForPath(filePath)
|
|
modifiedHandler = jasmine.createSpy("modifiedHandler")
|
|
buffer.on 'modified-status-changed', modifiedHandler
|
|
|
|
buffer.insert([0,0], "hi")
|
|
advanceClock(buffer.stoppedChangingDelay)
|
|
expect(buffer.isModified()).toBe true
|
|
modifiedHandler.reset()
|
|
|
|
buffer.save()
|
|
|
|
expect(modifiedHandler).toHaveBeenCalledWith(false)
|
|
expect(buffer.isModified()).toBe false
|
|
modifiedHandler.reset()
|
|
|
|
buffer.insert([0, 0], 'x')
|
|
advanceClock(buffer.stoppedChangingDelay)
|
|
expect(modifiedHandler).toHaveBeenCalledWith(true)
|
|
expect(buffer.isModified()).toBe true
|
|
|
|
it "reports the modified status changing to false after a modified buffer is reloaded", ->
|
|
filePath = "/tmp/atom-tmp-file"
|
|
fsUtils.write(filePath, '')
|
|
buffer.release()
|
|
buffer = project.bufferForPath(filePath)
|
|
modifiedHandler = jasmine.createSpy("modifiedHandler")
|
|
buffer.on 'modified-status-changed', modifiedHandler
|
|
|
|
buffer.insert([0,0], "hi")
|
|
advanceClock(buffer.stoppedChangingDelay)
|
|
expect(buffer.isModified()).toBe true
|
|
modifiedHandler.reset()
|
|
|
|
buffer.reload()
|
|
expect(modifiedHandler).toHaveBeenCalledWith(false)
|
|
expect(buffer.isModified()).toBe false
|
|
modifiedHandler.reset()
|
|
|
|
buffer.insert([0, 0], 'x')
|
|
advanceClock(buffer.stoppedChangingDelay)
|
|
expect(modifiedHandler).toHaveBeenCalledWith(true)
|
|
expect(buffer.isModified()).toBe true
|
|
|
|
it "reports the modified status changing to false after a buffer to a non-existent file is saved", ->
|
|
filePath = "/tmp/atom-tmp-file"
|
|
fsUtils.remove(filePath) if fsUtils.exists(filePath)
|
|
expect(fsUtils.exists(filePath)).toBeFalsy()
|
|
buffer.release()
|
|
buffer = project.bufferForPath(filePath)
|
|
modifiedHandler = jasmine.createSpy("modifiedHandler")
|
|
buffer.on 'modified-status-changed', modifiedHandler
|
|
|
|
buffer.insert([0,0], "hi")
|
|
advanceClock(buffer.stoppedChangingDelay)
|
|
expect(buffer.isModified()).toBe true
|
|
modifiedHandler.reset()
|
|
|
|
buffer.save()
|
|
expect(fsUtils.exists(filePath)).toBeTruthy()
|
|
|
|
expect(modifiedHandler).toHaveBeenCalledWith(false)
|
|
expect(buffer.isModified()).toBe false
|
|
modifiedHandler.reset()
|
|
|
|
buffer.insert([0, 0], 'x')
|
|
advanceClock(buffer.stoppedChangingDelay)
|
|
expect(modifiedHandler).toHaveBeenCalledWith(true)
|
|
expect(buffer.isModified()).toBe true
|
|
|
|
it "returns false for an empty buffer with no path", ->
|
|
buffer.release()
|
|
buffer = project.bufferForPath(null)
|
|
expect(buffer.isModified()).toBeFalsy()
|
|
|
|
it "returns true for a non-empty buffer with no path", ->
|
|
buffer.release()
|
|
buffer = project.bufferForPath(null)
|
|
buffer.setText('a')
|
|
expect(buffer.isModified()).toBeTruthy()
|
|
buffer.setText('\n')
|
|
expect(buffer.isModified()).toBeTruthy()
|
|
|
|
describe ".getLines()", ->
|
|
it "returns an array of lines in the text contents", ->
|
|
expect(buffer.getLines().length).toBe fileContents.split("\n").length
|
|
expect(buffer.getLines().join('\n')).toBe fileContents
|
|
|
|
describe ".change(range, string)", ->
|
|
changeHandler = null
|
|
|
|
beforeEach ->
|
|
changeHandler = jasmine.createSpy('changeHandler')
|
|
buffer.on 'changed', changeHandler
|
|
|
|
describe "when used to insert (called with an empty range and a non-empty string)", ->
|
|
describe "when the given string has no newlines", ->
|
|
it "inserts the string at the location of the given range", ->
|
|
range = [[3, 4], [3, 4]]
|
|
buffer.change range, "foo"
|
|
|
|
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
|
|
expect(buffer.lineForRow(3)).toBe " foovar pivot = items.shift(), current, left = [], right = [];"
|
|
expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {"
|
|
|
|
expect(changeHandler).toHaveBeenCalled()
|
|
[event] = changeHandler.argsForCall[0]
|
|
expect(event.oldRange).toEqual range
|
|
expect(event.newRange).toEqual [[3, 4], [3, 7]]
|
|
expect(event.oldText).toBe ""
|
|
expect(event.newText).toBe "foo"
|
|
|
|
describe "when the given string has newlines", ->
|
|
it "inserts the lines at the location of the given range", ->
|
|
range = [[3, 4], [3, 4]]
|
|
|
|
buffer.change range, "foo\n\nbar\nbaz"
|
|
|
|
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
|
|
expect(buffer.lineForRow(3)).toBe " foo"
|
|
expect(buffer.lineForRow(4)).toBe ""
|
|
expect(buffer.lineForRow(5)).toBe "bar"
|
|
expect(buffer.lineForRow(6)).toBe "bazvar pivot = items.shift(), current, left = [], right = [];"
|
|
expect(buffer.lineForRow(7)).toBe " while(items.length > 0) {"
|
|
|
|
expect(changeHandler).toHaveBeenCalled()
|
|
[event] = changeHandler.argsForCall[0]
|
|
expect(event.oldRange).toEqual range
|
|
expect(event.newRange).toEqual [[3, 4], [6, 3]]
|
|
expect(event.oldText).toBe ""
|
|
expect(event.newText).toBe "foo\n\nbar\nbaz"
|
|
|
|
describe "when used to remove (called with a non-empty range and an empty string)", ->
|
|
describe "when the range is contained within a single line", ->
|
|
it "removes the characters within the range", ->
|
|
range = [[3, 4], [3, 7]]
|
|
buffer.change range, ""
|
|
|
|
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
|
|
expect(buffer.lineForRow(3)).toBe " pivot = items.shift(), current, left = [], right = [];"
|
|
expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {"
|
|
|
|
expect(changeHandler).toHaveBeenCalled()
|
|
[event] = changeHandler.argsForCall[0]
|
|
expect(event.oldRange).toEqual range
|
|
expect(event.newRange).toEqual [[3, 4], [3, 4]]
|
|
expect(event.oldText).toBe "var"
|
|
expect(event.newText).toBe ""
|
|
|
|
describe "when the range spans 2 lines", ->
|
|
it "removes the characters within the range and joins the lines", ->
|
|
range = [[3, 16], [4, 4]]
|
|
buffer.change range, ""
|
|
|
|
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
|
|
expect(buffer.lineForRow(3)).toBe " var pivot = while(items.length > 0) {"
|
|
expect(buffer.lineForRow(4)).toBe " current = items.shift();"
|
|
|
|
expect(changeHandler).toHaveBeenCalled()
|
|
[event] = changeHandler.argsForCall[0]
|
|
expect(event.oldRange).toEqual range
|
|
expect(event.newRange).toEqual [[3, 16], [3, 16]]
|
|
expect(event.oldText).toBe "items.shift(), current, left = [], right = [];\n "
|
|
expect(event.newText).toBe ""
|
|
|
|
describe "when the range spans more than 2 lines", ->
|
|
it "removes the characters within the range, joining the first and last line and removing the lines in-between", ->
|
|
buffer.change [[3, 16], [11, 9]], ""
|
|
|
|
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
|
|
expect(buffer.lineForRow(3)).toBe " var pivot = sort(Array.apply(this, arguments));"
|
|
expect(buffer.lineForRow(4)).toBe "};"
|
|
|
|
describe "when used to replace text with other text (called with non-empty range and non-empty string)", ->
|
|
it "replaces the old text with the new text", ->
|
|
range = [[3, 16], [11, 9]]
|
|
oldText = buffer.getTextInRange(range)
|
|
|
|
buffer.change range, "foo\nbar"
|
|
|
|
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
|
|
expect(buffer.lineForRow(3)).toBe " var pivot = foo"
|
|
expect(buffer.lineForRow(4)).toBe "barsort(Array.apply(this, arguments));"
|
|
expect(buffer.lineForRow(5)).toBe "};"
|
|
|
|
expect(changeHandler).toHaveBeenCalled()
|
|
[event] = changeHandler.argsForCall[0]
|
|
expect(event.oldRange).toEqual range
|
|
expect(event.newRange).toEqual [[3, 16], [4, 3]]
|
|
expect(event.oldText).toBe oldText
|
|
expect(event.newText).toBe "foo\nbar"
|
|
|
|
it "allows a 'changed' event handler to safely undo the change", ->
|
|
buffer.on 'changed', -> buffer.undo()
|
|
buffer.change([0, 0], "hello")
|
|
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
|
|
|
|
describe ".setText(text)", ->
|
|
it "changes the entire contents of the buffer and emits a change event", ->
|
|
lastRow = buffer.getLastRow()
|
|
expectedPreRange = [[0,0], [lastRow, buffer.lineForRow(lastRow).length]]
|
|
changeHandler = jasmine.createSpy('changeHandler')
|
|
buffer.on 'changed', changeHandler
|
|
|
|
newText = "I know you are.\nBut what am I?"
|
|
buffer.setText(newText)
|
|
|
|
expect(buffer.getText()).toBe newText
|
|
expect(changeHandler).toHaveBeenCalled()
|
|
|
|
[event] = changeHandler.argsForCall[0]
|
|
expect(event.newText).toBe newText
|
|
expect(event.oldRange).toEqual expectedPreRange
|
|
expect(event.newRange).toEqual [[0, 0], [1, 14]]
|
|
|
|
describe ".save()", ->
|
|
saveBuffer = null
|
|
|
|
afterEach ->
|
|
saveBuffer.release()
|
|
|
|
describe "when the buffer has a path", ->
|
|
filePath = null
|
|
|
|
beforeEach ->
|
|
filePath = '/tmp/temp.txt'
|
|
fsUtils.write(filePath, "")
|
|
saveBuffer = project.bufferForPath(filePath)
|
|
saveBuffer.setText("blah")
|
|
|
|
it "saves the contents of the buffer to the path", ->
|
|
saveBuffer.setText 'Buffer contents!'
|
|
saveBuffer.save()
|
|
expect(fsUtils.read(filePath)).toEqual 'Buffer contents!'
|
|
|
|
it "fires will-be-saved and saved events around the call to fsUtils.write", ->
|
|
events = []
|
|
beforeSave1 = -> events.push('beforeSave1')
|
|
beforeSave2 = -> events.push('beforeSave2')
|
|
afterSave1 = -> events.push('afterSave1')
|
|
afterSave2 = -> events.push('afterSave2')
|
|
|
|
saveBuffer.on 'will-be-saved', beforeSave1
|
|
saveBuffer.on 'will-be-saved', beforeSave2
|
|
spyOn(fsUtils, 'write').andCallFake -> events.push 'fsUtils.write'
|
|
saveBuffer.on 'saved', afterSave1
|
|
saveBuffer.on 'saved', afterSave2
|
|
|
|
saveBuffer.save()
|
|
expect(events).toEqual ['beforeSave1', 'beforeSave2', 'fsUtils.write', 'afterSave1', 'afterSave2']
|
|
|
|
it "fires will-reload and reloaded events when reloaded", ->
|
|
events = []
|
|
|
|
saveBuffer.on 'will-reload', -> events.push 'will-reload'
|
|
saveBuffer.on 'reloaded', -> events.push 'reloaded'
|
|
saveBuffer.reload()
|
|
expect(events).toEqual ['will-reload', 'reloaded']
|
|
|
|
describe "when the buffer has no path", ->
|
|
it "throws an exception", ->
|
|
saveBuffer = project.bufferForPath(null)
|
|
saveBuffer.setText "hi"
|
|
expect(-> saveBuffer.save()).toThrow()
|
|
|
|
describe "reload()", ->
|
|
it "reloads current text from disk and clears any conflicts", ->
|
|
buffer.setText("abc")
|
|
buffer.conflict = true
|
|
|
|
buffer.reload()
|
|
expect(buffer.isModified()).toBeFalsy()
|
|
expect(buffer.isInConflict()).toBeFalsy()
|
|
expect(buffer.getText()).toBe(fileContents)
|
|
|
|
describe ".saveAs(path)", ->
|
|
[filePath, saveAsBuffer] = []
|
|
|
|
afterEach ->
|
|
saveAsBuffer.release()
|
|
|
|
it "saves the contents of the buffer to the path", ->
|
|
filePath = '/tmp/temp.txt'
|
|
fsUtils.remove filePath if fsUtils.exists(filePath)
|
|
|
|
saveAsBuffer = project.bufferForPath(null).retain()
|
|
eventHandler = jasmine.createSpy('eventHandler')
|
|
saveAsBuffer.on 'path-changed', eventHandler
|
|
|
|
saveAsBuffer.setText 'Buffer contents!'
|
|
saveAsBuffer.saveAs(filePath)
|
|
expect(fsUtils.read(filePath)).toEqual 'Buffer contents!'
|
|
|
|
expect(eventHandler).toHaveBeenCalledWith(saveAsBuffer)
|
|
|
|
it "stops listening to events on previous path and begins listening to events on new path", ->
|
|
originalPath = "/tmp/original.txt"
|
|
newPath = "/tmp/new.txt"
|
|
fsUtils.write(originalPath, "")
|
|
|
|
saveAsBuffer = project.bufferForPath(originalPath).retain()
|
|
changeHandler = jasmine.createSpy('changeHandler')
|
|
saveAsBuffer.on 'changed', changeHandler
|
|
saveAsBuffer.saveAs(newPath)
|
|
expect(changeHandler).not.toHaveBeenCalled()
|
|
|
|
fsUtils.write(originalPath, "should not trigger buffer event")
|
|
waits 20
|
|
runs ->
|
|
expect(changeHandler).not.toHaveBeenCalled()
|
|
fsUtils.write(newPath, "should trigger buffer event")
|
|
|
|
waitsFor ->
|
|
changeHandler.callCount > 0
|
|
|
|
describe ".getTextInRange(range)", ->
|
|
describe "when range is empty", ->
|
|
it "returns an empty string", ->
|
|
range = [[1,1], [1,1]]
|
|
expect(buffer.getTextInRange(range)).toBe ""
|
|
|
|
describe "when range spans one line", ->
|
|
it "returns characters in range", ->
|
|
range = [[2,8], [2,13]]
|
|
expect(buffer.getTextInRange(range)).toBe "items"
|
|
|
|
lineLength = buffer.lineForRow(2).length
|
|
range = [[2,0], [2,lineLength]]
|
|
expect(buffer.getTextInRange(range)).toBe " if (items.length <= 1) return items;"
|
|
|
|
describe "when range spans multiple lines", ->
|
|
it "returns characters in range (including newlines)", ->
|
|
lineLength = buffer.lineForRow(2).length
|
|
range = [[2,0], [3,0]]
|
|
expect(buffer.getTextInRange(range)).toBe " if (items.length <= 1) return items;\n"
|
|
|
|
lineLength = buffer.lineForRow(2).length
|
|
range = [[2,10], [4,10]]
|
|
expect(buffer.getTextInRange(range)).toBe "ems.length <= 1) return items;\n var pivot = items.shift(), current, left = [], right = [];\n while("
|
|
|
|
describe "when the range starts before the start of the buffer", ->
|
|
it "clips the range to the start of the buffer", ->
|
|
expect(buffer.getTextInRange([[-Infinity, -Infinity], [0, Infinity]])).toBe buffer.lineForRow(0)
|
|
|
|
describe "when the range ends after the end of the buffer", ->
|
|
it "clips the range to the end of the buffer", ->
|
|
expect(buffer.getTextInRange([[12], [13, Infinity]])).toBe buffer.lineForRow(12)
|
|
|
|
describe ".scanInRange(range, regex, fn)", ->
|
|
describe "when given a regex with a ignore case flag", ->
|
|
it "does a case-insensitive search", ->
|
|
matches = []
|
|
buffer.scanInRange /cuRRent/i, [[0,0], [12,0]], ({match, range}) ->
|
|
matches.push(match)
|
|
expect(matches.length).toBe 1
|
|
|
|
describe "when given a regex with no global flag", ->
|
|
it "calls the iterator with the first match for the given regex in the given range", ->
|
|
matches = []
|
|
ranges = []
|
|
buffer.scanInRange /cu(rr)ent/, [[4,0], [6,44]], ({match, range}) ->
|
|
matches.push(match)
|
|
ranges.push(range)
|
|
|
|
expect(matches.length).toBe 1
|
|
expect(ranges.length).toBe 1
|
|
|
|
expect(matches[0][0]).toBe 'current'
|
|
expect(matches[0][1]).toBe 'rr'
|
|
expect(ranges[0]).toEqual [[5,6], [5,13]]
|
|
|
|
describe "when given a regex with a global flag", ->
|
|
it "calls the iterator with each match for the given regex in the given range", ->
|
|
matches = []
|
|
ranges = []
|
|
buffer.scanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({match, range}) ->
|
|
matches.push(match)
|
|
ranges.push(range)
|
|
|
|
expect(matches.length).toBe 3
|
|
expect(ranges.length).toBe 3
|
|
|
|
expect(matches[0][0]).toBe 'current'
|
|
expect(matches[0][1]).toBe 'rr'
|
|
expect(ranges[0]).toEqual [[5,6], [5,13]]
|
|
|
|
expect(matches[1][0]).toBe 'current'
|
|
expect(matches[1][1]).toBe 'rr'
|
|
expect(ranges[1]).toEqual [[6,6], [6,13]]
|
|
|
|
expect(matches[2][0]).toBe 'current'
|
|
expect(matches[2][1]).toBe 'rr'
|
|
expect(ranges[2]).toEqual [[6,34], [6,41]]
|
|
|
|
describe "when the last regex match exceeds the end of the range", ->
|
|
describe "when the portion of the match within the range also matches the regex", ->
|
|
it "calls the iterator with the truncated match", ->
|
|
matches = []
|
|
ranges = []
|
|
buffer.scanInRange /cu(r*)/g, [[4,0], [6,9]], ({match, range}) ->
|
|
matches.push(match)
|
|
ranges.push(range)
|
|
|
|
expect(matches.length).toBe 2
|
|
expect(ranges.length).toBe 2
|
|
|
|
expect(matches[0][0]).toBe 'curr'
|
|
expect(matches[0][1]).toBe 'rr'
|
|
expect(ranges[0]).toEqual [[5,6], [5,10]]
|
|
|
|
expect(matches[1][0]).toBe 'cur'
|
|
expect(matches[1][1]).toBe 'r'
|
|
expect(ranges[1]).toEqual [[6,6], [6,9]]
|
|
|
|
describe "when the portion of the match within the range does not matches the regex", ->
|
|
it "calls the iterator with the truncated match", ->
|
|
matches = []
|
|
ranges = []
|
|
buffer.scanInRange /cu(r*)e/g, [[4,0], [6,9]], ({match, range}) ->
|
|
matches.push(match)
|
|
ranges.push(range)
|
|
|
|
expect(matches.length).toBe 1
|
|
expect(ranges.length).toBe 1
|
|
|
|
expect(matches[0][0]).toBe 'curre'
|
|
expect(matches[0][1]).toBe 'rr'
|
|
expect(ranges[0]).toEqual [[5,6], [5,11]]
|
|
|
|
describe "when the iterator calls the 'replace' control function with a replacement string", ->
|
|
it "replaces each occurrence of the regex match with the string", ->
|
|
ranges = []
|
|
buffer.scanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({range, replace}) ->
|
|
ranges.push(range)
|
|
replace("foo")
|
|
|
|
expect(ranges[0]).toEqual [[5,6], [5,13]]
|
|
expect(ranges[1]).toEqual [[6,6], [6,13]]
|
|
expect(ranges[2]).toEqual [[6,30], [6,37]]
|
|
|
|
expect(buffer.lineForRow(5)).toBe ' foo = items.shift();'
|
|
expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(foo) : right.push(current);'
|
|
|
|
it "allows the match to be replaced with the empty string", ->
|
|
buffer.scanInRange /current/g, [[4,0], [6,59]], ({replace}) ->
|
|
replace("")
|
|
|
|
expect(buffer.lineForRow(5)).toBe ' = items.shift();'
|
|
expect(buffer.lineForRow(6)).toBe ' < pivot ? left.push() : right.push(current);'
|
|
|
|
describe "when the iterator calls the 'stop' control function", ->
|
|
it "stops the traversal", ->
|
|
ranges = []
|
|
buffer.scanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({range, stop}) ->
|
|
ranges.push(range)
|
|
stop() if ranges.length == 2
|
|
|
|
expect(ranges.length).toBe 2
|
|
|
|
describe ".backwardsScanInRange(range, regex, fn)", ->
|
|
describe "when given a regex with no global flag", ->
|
|
it "calls the iterator with the last match for the given regex in the given range", ->
|
|
matches = []
|
|
ranges = []
|
|
buffer.backwardsScanInRange /cu(rr)ent/, [[4,0], [6,44]], ({match, range}) ->
|
|
matches.push(match)
|
|
ranges.push(range)
|
|
|
|
expect(matches.length).toBe 1
|
|
expect(ranges.length).toBe 1
|
|
|
|
expect(matches[0][0]).toBe 'current'
|
|
expect(matches[0][1]).toBe 'rr'
|
|
expect(ranges[0]).toEqual [[6,34], [6,41]]
|
|
|
|
describe "when given a regex with a global flag", ->
|
|
it "calls the iterator with each match for the given regex in the given range, starting with the last match", ->
|
|
matches = []
|
|
ranges = []
|
|
buffer.backwardsScanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({match, range}) ->
|
|
matches.push(match)
|
|
ranges.push(range)
|
|
|
|
expect(matches.length).toBe 3
|
|
expect(ranges.length).toBe 3
|
|
|
|
expect(matches[0][0]).toBe 'current'
|
|
expect(matches[0][1]).toBe 'rr'
|
|
expect(ranges[0]).toEqual [[6,34], [6,41]]
|
|
|
|
expect(matches[1][0]).toBe 'current'
|
|
expect(matches[1][1]).toBe 'rr'
|
|
expect(ranges[1]).toEqual [[6,6], [6,13]]
|
|
|
|
expect(matches[2][0]).toBe 'current'
|
|
expect(matches[2][1]).toBe 'rr'
|
|
expect(ranges[2]).toEqual [[5,6], [5,13]]
|
|
|
|
describe "when the iterator calls the 'replace' control function with a replacement string", ->
|
|
it "replaces each occurrence of the regex match with the string", ->
|
|
ranges = []
|
|
buffer.backwardsScanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({range, replace}) ->
|
|
ranges.push(range)
|
|
replace("foo") unless range.start.isEqual([6,6])
|
|
|
|
expect(ranges[0]).toEqual [[6,34], [6,41]]
|
|
expect(ranges[1]).toEqual [[6,6], [6,13]]
|
|
expect(ranges[2]).toEqual [[5,6], [5,13]]
|
|
|
|
expect(buffer.lineForRow(5)).toBe ' foo = items.shift();'
|
|
expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(foo) : right.push(current);'
|
|
|
|
describe "when the iterator calls the 'stop' control function", ->
|
|
it "stops the traversal", ->
|
|
ranges = []
|
|
buffer.backwardsScanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({range, stop}) ->
|
|
ranges.push(range)
|
|
stop() if ranges.length == 2
|
|
|
|
expect(ranges.length).toBe 2
|
|
expect(ranges[0]).toEqual [[6,34], [6,41]]
|
|
expect(ranges[1]).toEqual [[6,6], [6,13]]
|
|
|
|
describe ".characterIndexForPosition(position)", ->
|
|
it "returns the total number of characters that precede the given position", ->
|
|
expect(buffer.characterIndexForPosition([0, 0])).toBe 0
|
|
expect(buffer.characterIndexForPosition([0, 1])).toBe 1
|
|
expect(buffer.characterIndexForPosition([0, 29])).toBe 29
|
|
expect(buffer.characterIndexForPosition([1, 0])).toBe 30
|
|
expect(buffer.characterIndexForPosition([2, 0])).toBe 61
|
|
expect(buffer.characterIndexForPosition([12, 2])).toBe 408
|
|
expect(buffer.characterIndexForPosition([Infinity])).toBe 408
|
|
|
|
describe "when the buffer contains crlf line endings", ->
|
|
it "returns the total number of characters that precede the given position", ->
|
|
buffer.setText("line1\r\nline2\nline3\r\nline4")
|
|
expect(buffer.characterIndexForPosition([1])).toBe 7
|
|
expect(buffer.characterIndexForPosition([2])).toBe 13
|
|
expect(buffer.characterIndexForPosition([3])).toBe 20
|
|
|
|
describe ".positionForCharacterIndex(position)", ->
|
|
it "returns the position based on character index", ->
|
|
expect(buffer.positionForCharacterIndex(0)).toEqual [0, 0]
|
|
expect(buffer.positionForCharacterIndex(1)).toEqual [0, 1]
|
|
expect(buffer.positionForCharacterIndex(29)).toEqual [0, 29]
|
|
expect(buffer.positionForCharacterIndex(30)).toEqual [1, 0]
|
|
expect(buffer.positionForCharacterIndex(61)).toEqual [2, 0]
|
|
expect(buffer.positionForCharacterIndex(408)).toEqual [12, 2]
|
|
|
|
describe "when the buffer contains crlf line endings", ->
|
|
it "returns the position based on character index", ->
|
|
buffer.setText("line1\r\nline2\nline3\r\nline4")
|
|
expect(buffer.positionForCharacterIndex(7)).toEqual [1, 0]
|
|
expect(buffer.positionForCharacterIndex(13)).toEqual [2, 0]
|
|
expect(buffer.positionForCharacterIndex(20)).toEqual [3, 0]
|
|
|
|
describe "markers", ->
|
|
markerCreatedHandler = null
|
|
|
|
beforeEach ->
|
|
buffer.on('marker-created', markerCreatedHandler = jasmine.createSpy("markerCreatedHandler"))
|
|
|
|
describe "marker creation", ->
|
|
it "allows markers to be created with ranges and positions", ->
|
|
marker1 = buffer.markRange([[4, 20], [4, 23]])
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
expect(marker1.getHeadPosition()).toEqual [4, 23]
|
|
expect(marker1.getTailPosition()).toEqual [4, 20]
|
|
|
|
marker2 = buffer.markPosition([4, 20])
|
|
expect(marker2.getRange()).toEqual [[4, 20], [4, 20]]
|
|
expect(marker2.getHeadPosition()).toEqual [4, 20]
|
|
expect(marker2.getTailPosition()).toEqual [4, 20]
|
|
|
|
it "allows markers to be created in a reversed orientation", ->
|
|
marker = buffer.markRange([[4, 20], [4, 23]], reverse: true)
|
|
expect(marker.isReversed()).toBeTruthy()
|
|
expect(marker.getRange()).toEqual [[4, 20], [4, 23]]
|
|
expect(marker.getHeadPosition()).toEqual [4, 20]
|
|
expect(marker.getTailPosition()).toEqual [4, 23]
|
|
|
|
it "emits the 'marker-created' event when markers are created", ->
|
|
marker = buffer.markRange([[4, 20], [4, 23]])
|
|
expect(markerCreatedHandler).toHaveBeenCalledWith(marker)
|
|
|
|
describe "marker manipulation", ->
|
|
marker = null
|
|
beforeEach ->
|
|
marker = buffer.markRange([[4, 20], [4, 23]])
|
|
|
|
it "allows a marker's head and tail positions to be changed", ->
|
|
marker.setHeadPosition([5, 3])
|
|
expect(marker.getRange()).toEqual [[4, 20], [5, 3]]
|
|
|
|
marker.setTailPosition([6, 3])
|
|
expect(marker.getRange()).toEqual [[5, 3], [6, 3]]
|
|
expect(marker.isReversed()).toBeTruthy()
|
|
|
|
it "clips head and tail positions to ensure they are in bounds", ->
|
|
marker.setHeadPosition([-100, -5])
|
|
expect(marker.getRange()).toEqual([[0, 0], [4, 20]])
|
|
marker.setTailPosition([Infinity, Infinity])
|
|
expect(marker.getRange()).toEqual([[0, 0], [12, 2]])
|
|
|
|
it "allows a marker's tail to be placed and cleared", ->
|
|
marker.clearTail()
|
|
expect(marker.getRange()).toEqual [[4, 23], [4, 23]]
|
|
marker.placeTail()
|
|
marker.setHeadPosition([2, 0])
|
|
expect(marker.getRange()).toEqual [[2, 0], [4, 23]]
|
|
expect(marker.isReversed()).toBeTruthy()
|
|
|
|
it "returns whether the position changed", ->
|
|
expect(marker.setHeadPosition([5, 3])).toBeTruthy()
|
|
expect(marker.setHeadPosition([5, 3])).toBeFalsy()
|
|
|
|
expect(marker.setTailPosition([6, 3])).toBeTruthy()
|
|
expect(marker.setTailPosition([6, 3])).toBeFalsy()
|
|
|
|
describe ".observeMarker(marker, callback)", ->
|
|
[observeHandler, marker, subscription] = []
|
|
|
|
beforeEach ->
|
|
observeHandler = jasmine.createSpy("observeHandler")
|
|
marker = buffer.markRange([[4, 20], [4, 23]])
|
|
subscription = marker.observe(observeHandler)
|
|
|
|
it "calls the callback when the marker's head position changes", ->
|
|
marker.setHeadPosition([6, 2])
|
|
expect(observeHandler).toHaveBeenCalled()
|
|
expect(observeHandler.argsForCall[0][0]).toEqual {
|
|
oldHeadPosition: [4, 23]
|
|
newHeadPosition: [6, 2]
|
|
oldTailPosition: [4, 20]
|
|
newTailPosition: [4, 20]
|
|
bufferChanged: false
|
|
valid: true
|
|
}
|
|
observeHandler.reset()
|
|
|
|
buffer.insert([6, 0], '...')
|
|
expect(observeHandler.argsForCall[0][0]).toEqual {
|
|
oldTailPosition: [4, 20]
|
|
newTailPosition: [4, 20]
|
|
oldHeadPosition: [6, 2]
|
|
newHeadPosition: [6, 5]
|
|
bufferChanged: true
|
|
valid: true
|
|
}
|
|
|
|
it "calls the given callback when the marker's tail position changes", ->
|
|
marker.setTailPosition([6, 2])
|
|
expect(observeHandler).toHaveBeenCalled()
|
|
expect(observeHandler.argsForCall[0][0]).toEqual {
|
|
oldHeadPosition: [4, 23]
|
|
newHeadPosition: [4, 23]
|
|
oldTailPosition: [4, 20]
|
|
newTailPosition: [6, 2]
|
|
bufferChanged: false
|
|
valid: true
|
|
}
|
|
observeHandler.reset()
|
|
|
|
buffer.insert([6, 0], '...')
|
|
|
|
expect(observeHandler.argsForCall[0][0]).toEqual {
|
|
oldHeadPosition: [4, 23]
|
|
newHeadPosition: [4, 23]
|
|
oldTailPosition: [6, 2]
|
|
newTailPosition: [6, 5]
|
|
bufferChanged: true
|
|
valid: true
|
|
}
|
|
|
|
it "calls the callback when the selection's tail is cleared", ->
|
|
marker.clearTail()
|
|
expect(observeHandler).toHaveBeenCalled()
|
|
expect(observeHandler.argsForCall[0][0]).toEqual {
|
|
oldHeadPosition: [4, 23]
|
|
newHeadPosition: [4, 23]
|
|
oldTailPosition: [4, 20]
|
|
newTailPosition: [4, 23]
|
|
bufferChanged: false
|
|
valid: true
|
|
}
|
|
|
|
it "only calls the callback once when both the marker's head and tail positions change due to the same operation", ->
|
|
buffer.insert([4, 0], '...')
|
|
expect(observeHandler.callCount).toBe 1
|
|
expect(observeHandler.argsForCall[0][0]).toEqual {
|
|
oldTailPosition: [4, 20]
|
|
newTailPosition: [4, 23]
|
|
oldHeadPosition: [4, 23]
|
|
newHeadPosition: [4, 26]
|
|
bufferChanged: true
|
|
valid: true
|
|
}
|
|
observeHandler.reset()
|
|
|
|
marker.setRange([[0, 0], [1, 1]])
|
|
expect(observeHandler.callCount).toBe 1
|
|
expect(observeHandler.argsForCall[0][0]).toEqual {
|
|
oldTailPosition: [4, 23]
|
|
newTailPosition: [0, 0]
|
|
oldHeadPosition: [4, 26]
|
|
newHeadPosition: [1, 1]
|
|
bufferChanged: false
|
|
valid: true
|
|
}
|
|
|
|
it "calls the callback with the valid flag set to false when the marker is invalidated", ->
|
|
buffer.deleteRow(4)
|
|
expect(observeHandler.callCount).toBe 1
|
|
expect(observeHandler.argsForCall[0][0]).toEqual {
|
|
oldTailPosition: [4, 20]
|
|
newTailPosition: [4, 20]
|
|
oldHeadPosition: [4, 23]
|
|
newHeadPosition: [4, 23]
|
|
bufferChanged: true
|
|
valid: false
|
|
}
|
|
|
|
observeHandler.reset()
|
|
buffer.undo()
|
|
expect(observeHandler.callCount).toBe 1
|
|
expect(observeHandler.argsForCall[0][0]).toEqual {
|
|
oldTailPosition: [4, 20]
|
|
newTailPosition: [4, 20]
|
|
oldHeadPosition: [4, 23]
|
|
newHeadPosition: [4, 23]
|
|
bufferChanged: true
|
|
valid: true
|
|
}
|
|
|
|
it "allows the observation subscription to be cancelled", ->
|
|
subscription.cancel()
|
|
marker.setHeadPosition([6, 2])
|
|
expect(observeHandler).not.toHaveBeenCalled()
|
|
|
|
describe ".findMarkers(attributes)", ->
|
|
[marker1, marker2, marker3, marker4] = []
|
|
|
|
beforeEach ->
|
|
marker1 = buffer.markRange([[0, 0], [3, 0]], class: 'a')
|
|
marker2 = buffer.markRange([[0, 0], [5, 0]], class: 'a')
|
|
marker3 = buffer.markRange([[6, 0], [7, 0]], class: 'a')
|
|
marker4 = buffer.markRange([[9, 0], [10, 0]], class: 'b')
|
|
|
|
it "returns the markers matching the given attributes, sorted by the buffer location and size of their ranges", ->
|
|
expect(buffer.findMarkers(class: 'a')).toEqual [marker2, marker1, marker3]
|
|
|
|
it "allows the startRow and endRow to be specified", ->
|
|
expect(buffer.findMarkers(class: 'a', startRow: 0)).toEqual [marker2, marker1]
|
|
expect(buffer.findMarkers(class: 'a', startRow: 0, endRow: 3)).toEqual [marker1]
|
|
expect(buffer.findMarkers(endRow: 10)).toEqual [marker4]
|
|
|
|
describe "marker destruction", ->
|
|
marker = null
|
|
|
|
beforeEach ->
|
|
marker = buffer.markRange([[4, 20], [4, 23]])
|
|
|
|
it "allows a marker to be destroyed", ->
|
|
marker.destroy()
|
|
expect(buffer.getMarker(marker.id)).toBeUndefined()
|
|
|
|
it "does not restore invalidated markers that have been destroyed", ->
|
|
buffer.delete([[4, 15], [4, 25]])
|
|
expect(buffer.getMarker(marker.id)).toBeUndefined()
|
|
marker.destroy()
|
|
buffer.undo()
|
|
expect(buffer.getMarker(marker.id)).toBeUndefined()
|
|
|
|
# even "invalidationStrategy: never" markers get destroyed properly
|
|
marker2 = buffer.markRange([[4, 20], [4, 23]], invalidationStrategy: 'never')
|
|
buffer.delete([[4, 15], [4, 25]])
|
|
marker2.destroy()
|
|
buffer.undo()
|
|
expect(buffer.getMarker(marker2.id)).toBeUndefined()
|
|
|
|
it "emits 'destroyed' on the marker when it is destroyed", ->
|
|
marker.on 'destroyed', destroyedHandler = jasmine.createSpy("destroyedHandler")
|
|
marker.destroy()
|
|
expect(destroyedHandler).toHaveBeenCalled()
|
|
|
|
describe "marker updates due to buffer changes", ->
|
|
[marker1, marker2, marker3] = []
|
|
|
|
beforeEach ->
|
|
marker1 = buffer.markRange([[4, 20], [4, 23]])
|
|
marker2 = buffer.markRange([[4, 20], [4, 23]], invalidationStrategy: 'never')
|
|
marker3 = buffer.markRange([[4, 20], [4, 23]], invalidationStrategy: 'between')
|
|
|
|
describe "when the buffer changes due to a new operation", ->
|
|
describe "when the change precedes the marker range", ->
|
|
it "moves the marker", ->
|
|
buffer.insert([4, 5], '...')
|
|
expect(marker1.getRange()).toEqual [[4, 23], [4, 26]]
|
|
buffer.delete([[4, 5], [4, 8]])
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
buffer.insert([0, 0], '\nhi\n')
|
|
expect(marker1.getRange()).toEqual [[6, 20], [6, 23]]
|
|
|
|
# undo works
|
|
buffer.undo()
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
buffer.undo()
|
|
expect(marker1.getRange()).toEqual [[4, 23], [4, 26]]
|
|
|
|
describe "when the change follows the marker range", ->
|
|
it "does not move the marker", ->
|
|
buffer.insert([6, 5], '...')
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
buffer.delete([[6, 5], [6, 8]])
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
buffer.insert([10, 0], '\nhi\n')
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the change is an insertion at the start of the marker range", ->
|
|
it "does not move the start point, but does move the end point", ->
|
|
buffer.insert([4, 20], '...')
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 26]]
|
|
|
|
describe "when the invalidation strategy is 'between'", ->
|
|
it "invalidates the marker", ->
|
|
buffer.insert([4, 20], '...')
|
|
expect(marker3.isValid()).toBeFalsy()
|
|
|
|
describe "when the change is an insertion at the end of the marker range", ->
|
|
it "moves the end point", ->
|
|
buffer.insert([4, 23], '...')
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 26]]
|
|
|
|
describe "when the invalidation strategy is 'between'", ->
|
|
it "invalidates the marker", ->
|
|
buffer.insert([4, 23], '...')
|
|
expect(marker3.isValid()).toBeFalsy()
|
|
|
|
describe "when the change surrounds the marker range", ->
|
|
describe "when the marker's invalidation strategy is 'contains' (the default)", ->
|
|
it "invalidates the marker", ->
|
|
buffer.delete([[4, 15], [4, 25]])
|
|
expect(marker1.isValid()).toBeFalsy()
|
|
buffer.undo()
|
|
expect(marker1.isValid()).toBeTruthy()
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the marker's invalidation strategy is 'between'", ->
|
|
it "invalidates the marker", ->
|
|
buffer.delete([[4, 15], [4, 25]])
|
|
expect(marker3.isValid()).toBeFalsy()
|
|
buffer.undo()
|
|
expect(marker3.isValid()).toBeTruthy()
|
|
expect(marker3.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the marker's invalidation strategy is 'never'", ->
|
|
it "does not invalidate the marker, but sets it to an empty range at the end of the change", ->
|
|
buffer.change([[4, 15], [4, 25]], "...")
|
|
expect(marker2.isValid()).toBeTruthy()
|
|
expect(marker2.getRange()).toEqual [[4, 18], [4, 18]]
|
|
buffer.undo()
|
|
expect(marker2.isValid()).toBeTruthy()
|
|
expect(marker2.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the change straddles the start of the marker range", ->
|
|
describe "when the marker's invalidation strategy is 'contains' (the default)", ->
|
|
it "invalidates the marker", ->
|
|
buffer.delete([[4, 15], [4, 22]])
|
|
expect(marker1.isValid()).toBeFalsy()
|
|
buffer.undo()
|
|
expect(marker1.isValid()).toBeTruthy()
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the marker's invalidation strategy is 'between'", ->
|
|
it "invalidates the marker", ->
|
|
buffer.delete([[4, 15], [4, 22]])
|
|
expect(marker3.isValid()).toBeFalsy()
|
|
buffer.undo()
|
|
expect(marker3.isValid()).toBeTruthy()
|
|
expect(marker3.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the marker's invalidation strategy is 'never'", ->
|
|
it "moves the start of the marker range to the end of the change", ->
|
|
buffer.delete([[4, 15], [4, 22]])
|
|
expect(marker2.isValid()).toBeTruthy()
|
|
expect(marker2.getRange()).toEqual [[4, 15], [4, 16]]
|
|
buffer.undo()
|
|
expect(marker2.isValid()).toBeTruthy()
|
|
expect(marker2.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the change straddles the end of the marker range", ->
|
|
describe "when the marker's invalidation strategy is 'contains' (the default)", ->
|
|
it "invalidates the marker", ->
|
|
buffer.delete([[4, 22], [4, 25]])
|
|
expect(marker1.isValid()).toBeFalsy()
|
|
buffer.undo()
|
|
expect(marker1.isValid()).toBeTruthy()
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the marker's invalidation strategy is 'between'", ->
|
|
it "invalidates the marker", ->
|
|
buffer.delete([[4, 22], [4, 25]])
|
|
expect(marker3.isValid()).toBeFalsy()
|
|
buffer.undo()
|
|
expect(marker3.isValid()).toBeTruthy()
|
|
expect(marker3.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the marker's invalidation strategy is 'never'", ->
|
|
it "moves the end of the marker range to the start of the change", ->
|
|
buffer.delete([[4, 22], [4, 25]])
|
|
expect(marker2.isValid()).toBeTruthy()
|
|
expect(marker2.getRange()).toEqual [[4, 20], [4, 22]]
|
|
buffer.undo()
|
|
expect(marker2.isValid()).toBeTruthy()
|
|
expect(marker2.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the change is between the start and the end of the marker range", ->
|
|
describe "when the marker's invalidation strategy is 'contains' (the default)", ->
|
|
it "does not invalidate the marker", ->
|
|
buffer.insert([4, 21], 'x')
|
|
expect(marker1.isValid()).toBeTruthy()
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 24]]
|
|
buffer.undo()
|
|
expect(marker1.isValid()).toBeTruthy()
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the marker's invalidation strategy is 'between'", ->
|
|
it "invalidates the marker", ->
|
|
buffer.insert([4, 21], 'x')
|
|
expect(marker3.isValid()).toBeFalsy()
|
|
buffer.undo()
|
|
expect(marker3.isValid()).toBeTruthy()
|
|
expect(marker3.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the marker's invalidation strategy is 'never'", ->
|
|
it "moves the end of the marker range to the start of the change", ->
|
|
buffer.insert([4, 21], 'x')
|
|
expect(marker2.isValid()).toBeTruthy()
|
|
expect(marker2.getRange()).toEqual [[4, 20], [4, 24]]
|
|
buffer.undo()
|
|
expect(marker2.isValid()).toBeTruthy()
|
|
expect(marker2.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe "when the buffer changes due to the undo or redo of a previous operation", ->
|
|
it "restores invalidated markers when undoing/redoing in the other direction", ->
|
|
buffer.change([[4, 21], [4, 24]], "foo")
|
|
expect(marker1.isValid()).toBeFalsy()
|
|
marker3 = buffer.markRange([[4, 20], [4, 23]])
|
|
buffer.undo()
|
|
expect(marker1.isValid()).toBeTruthy()
|
|
expect(marker1.getRange()).toEqual [[4, 20], [4, 23]]
|
|
expect(marker3.isValid()).toBeFalsy()
|
|
marker4 = buffer.markRange([[4, 20], [4, 23]])
|
|
buffer.redo()
|
|
expect(marker3.isValid()).toBeTruthy()
|
|
expect(marker3.getRange()).toEqual [[4, 20], [4, 23]]
|
|
expect(marker4.isValid()).toBeFalsy()
|
|
buffer.undo()
|
|
expect(marker4.isValid()).toBeTruthy()
|
|
expect(marker4.getRange()).toEqual [[4, 20], [4, 23]]
|
|
|
|
describe ".markersForPosition(position)", ->
|
|
it "returns all markers that intersect the given position", ->
|
|
m1 = buffer.markRange([[3, 4], [3, 10]])
|
|
m2 = buffer.markRange([[3, 4], [3, 5]])
|
|
m3 = buffer.markPosition([3, 5])
|
|
expect(_.difference(buffer.markersForPosition([3, 5]), [m1, m2, m3]).length).toBe 0
|
|
expect(_.difference(buffer.markersForPosition([3, 4]), [m1, m2]).length).toBe 0
|
|
expect(_.difference(buffer.markersForPosition([3, 10]), [m1]).length).toBe 0
|
|
|
|
describe ".usesSoftTabs()", ->
|
|
it "returns true if the first indented line begins with tabs", ->
|
|
buffer.setText("function() {\n foo();\n}")
|
|
expect(buffer.usesSoftTabs()).toBeTruthy()
|
|
buffer.setText("function() {\n\tfoo();\n}")
|
|
expect(buffer.usesSoftTabs()).toBeFalsy()
|
|
buffer.setText("")
|
|
expect(buffer.usesSoftTabs()).toBeUndefined()
|
|
|
|
describe ".isEmpty()", ->
|
|
it "returns true for an empty buffer", ->
|
|
buffer.setText('')
|
|
expect(buffer.isEmpty()).toBeTruthy()
|
|
|
|
it "returns false for a non-empty buffer", ->
|
|
buffer.setText('a')
|
|
expect(buffer.isEmpty()).toBeFalsy()
|
|
buffer.setText('a\nb\nc')
|
|
expect(buffer.isEmpty()).toBeFalsy()
|
|
buffer.setText('\n')
|
|
expect(buffer.isEmpty()).toBeFalsy()
|
|
|
|
describe "'contents-modified' event", ->
|
|
it "triggers the 'contents-modified' event with the current modified status when the buffer changes, rate-limiting events with a delay", ->
|
|
delay = buffer.stoppedChangingDelay
|
|
contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler")
|
|
buffer.on 'contents-modified', contentsModifiedHandler
|
|
|
|
buffer.insert([0, 0], 'a')
|
|
expect(contentsModifiedHandler).not.toHaveBeenCalled()
|
|
|
|
advanceClock(delay / 2)
|
|
|
|
buffer.insert([0, 0], 'b')
|
|
expect(contentsModifiedHandler).not.toHaveBeenCalled()
|
|
|
|
advanceClock(delay / 2)
|
|
expect(contentsModifiedHandler).not.toHaveBeenCalled()
|
|
|
|
advanceClock(delay / 2)
|
|
expect(contentsModifiedHandler).toHaveBeenCalledWith(true)
|
|
|
|
contentsModifiedHandler.reset()
|
|
buffer.undo()
|
|
buffer.undo()
|
|
advanceClock(delay)
|
|
expect(contentsModifiedHandler).toHaveBeenCalledWith(false)
|
|
|
|
describe ".append(text)", ->
|
|
it "adds text to the end of the buffer", ->
|
|
buffer.setText("")
|
|
buffer.append("a")
|
|
expect(buffer.getText()).toBe "a"
|
|
buffer.append("b\nc");
|
|
expect(buffer.getText()).toBe "ab\nc"
|
|
|
|
describe "line ending support", ->
|
|
describe ".lineEndingForRow(line)", ->
|
|
it "return the line ending for each buffer line", ->
|
|
buffer.setText("a\r\nb\nc")
|
|
expect(buffer.lineEndingForRow(0)).toBe '\r\n'
|
|
expect(buffer.lineEndingForRow(1)).toBe '\n'
|
|
expect(buffer.lineEndingForRow(2)).toBeUndefined()
|
|
|
|
describe ".lineForRow(line)", ->
|
|
it "returns the line text without the line ending for both lf and crlf lines", ->
|
|
buffer.setText("a\r\nb\nc")
|
|
expect(buffer.lineForRow(0)).toBe 'a'
|
|
expect(buffer.lineForRow(1)).toBe 'b'
|
|
expect(buffer.lineForRow(2)).toBe 'c'
|
|
|
|
describe ".getText()", ->
|
|
it "returns the text with the corrent line endings for each row", ->
|
|
buffer.setText("a\r\nb\nc")
|
|
expect(buffer.getText()).toBe "a\r\nb\nc"
|
|
buffer.setText("a\r\nb\nc\n")
|
|
expect(buffer.getText()).toBe "a\r\nb\nc\n"
|
|
|
|
describe "when editing a line", ->
|
|
it "preserves the existing line ending", ->
|
|
buffer.setText("a\r\nb\nc")
|
|
buffer.insert([0, 1], "1")
|
|
expect(buffer.getText()).toBe "a1\r\nb\nc"
|
|
|
|
describe "when inserting text with multiple lines", ->
|
|
describe "when the current line has a line ending", ->
|
|
it "uses the same line ending as the line where the text is inserted", ->
|
|
buffer.setText("a\r\n")
|
|
buffer.insert([0,1], "hello\n1\n\n2")
|
|
expect(buffer.getText()).toBe "ahello\r\n1\r\n\r\n2\r\n"
|
|
|
|
describe "when the current line has no line ending (because it's the last line of the buffer)", ->
|
|
describe "when the buffer contains only a single line", ->
|
|
it "honors the line endings in the inserted text", ->
|
|
buffer.setText("initialtext")
|
|
buffer.append("hello\n1\r\n2\n")
|
|
expect(buffer.getText()).toBe "initialtexthello\n1\r\n2\n"
|
|
|
|
describe "when the buffer contains a preceding line", ->
|
|
it "uses the line ending of the preceding line", ->
|
|
buffer.setText("\ninitialtext")
|
|
buffer.append("hello\n1\r\n2\n")
|
|
expect(buffer.getText()).toBe "\ninitialtexthello\n1\n2\n"
|
|
|
|
describe ".clipPosition(position)", ->
|
|
describe "when the position is before the start of the buffer", ->
|
|
it "returns the first position in the buffer", ->
|
|
expect(buffer.clipPosition([-1,0])).toEqual [0,0]
|
|
expect(buffer.clipPosition([0,-1])).toEqual [0,0]
|
|
expect(buffer.clipPosition([-1,-1])).toEqual [0,0]
|
|
|
|
describe "when the position is after the end of the buffer", ->
|
|
it "returns the last position in the buffer", ->
|
|
buffer.setText('some text')
|
|
expect(buffer.clipPosition([1, 0])).toEqual [0,9]
|
|
expect(buffer.clipPosition([0,10])).toEqual [0,9]
|
|
expect(buffer.clipPosition([10,Infinity])).toEqual [0,9]
|
|
|
|
describe "serialization", ->
|
|
serializedState = null
|
|
|
|
reloadBuffer = () ->
|
|
serializedState = buffer.serialize()
|
|
buffer.release()
|
|
buffer = Buffer.deserialize(serializedState)
|
|
|
|
describe "when the serialized buffer had no unsaved changes", ->
|
|
it "loads the current contents of the file at the serialized path", ->
|
|
path = buffer.getPath()
|
|
text = buffer.getText()
|
|
reloadBuffer()
|
|
expect(serializedState.text).toBeUndefined()
|
|
expect(buffer.getPath()).toBe(path)
|
|
expect(buffer.getText()).toBe(text)
|
|
|
|
describe "when the serialized buffer had unsaved changes", ->
|
|
it "restores the previous unsaved state of the buffer", ->
|
|
path = buffer.getPath()
|
|
previousText = buffer.getText()
|
|
buffer.setText("abc")
|
|
reloadBuffer()
|
|
expect(serializedState.text).toBe "abc"
|
|
expect(buffer.getPath()).toBe(path)
|
|
expect(buffer.getText()).toBe("abc")
|
|
buffer.setText(previousText)
|
|
expect(buffer.isModified()).toBeFalsy()
|
|
|
|
describe "when the serialized buffer was unsaved and had no path", ->
|
|
it "restores the previous unsaved state of the buffer", ->
|
|
buffer.setPath(undefined)
|
|
buffer.setText("abc")
|
|
reloadBuffer()
|
|
expect(serializedState.path).toBeUndefined()
|
|
expect(buffer.getPath()).toBeUndefined()
|
|
expect(buffer.getText()).toBe("abc")
|
|
|
|
it "never deserializes two separate instances of the same buffer", ->
|
|
serializedState = buffer.serialize()
|
|
buffer.release()
|
|
buffer = Buffer.deserialize(serializedState)
|
|
expect(Buffer.deserialize(serializedState)).toBe buffer
|