mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-22 00:19:24 +03:00
457 lines
18 KiB
CoffeeScript
457 lines
18 KiB
CoffeeScript
Project = require 'project'
|
|
Buffer = require 'buffer'
|
|
fs = require 'fs'
|
|
|
|
describe 'Buffer', ->
|
|
[filePath, fileContents, buffer] = []
|
|
|
|
beforeEach ->
|
|
filePath = require.resolve('fixtures/sample.js')
|
|
fileContents = fs.read(filePath)
|
|
buffer = new Buffer(filePath)
|
|
|
|
describe 'constructor', ->
|
|
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 = new Buffer(filePath)
|
|
expect(buffer.getText()).toBe fs.read(filePath)
|
|
|
|
describe "when no file exists for the path", ->
|
|
it "creates an empty buffer", ->
|
|
filePath = "does-not-exist.txt"
|
|
expect(fs.exists(filePath)).toBeFalsy()
|
|
|
|
buffer = new Buffer(filePath)
|
|
expect(buffer.getText()).toBe ""
|
|
|
|
describe "when no path is given", ->
|
|
it "creates an empty buffer", ->
|
|
buffer = new Buffer
|
|
expect(buffer.getText()).toBe ""
|
|
|
|
describe '.deserialize(state, project)', ->
|
|
project = null
|
|
|
|
beforeEach ->
|
|
project = new Project(fs.directory(filePath))
|
|
|
|
describe 'when the state has a path', ->
|
|
it 'use the project to open the path', ->
|
|
savedBuffer = project.open(filePath)
|
|
buffer = Buffer.deserialize(savedBuffer.serialize(), project)
|
|
expect(buffer).toBe savedBuffer
|
|
|
|
describe 'when the state has text (and no path)', ->
|
|
it 'creates a new buffer with the given text', ->
|
|
unsavedBuffer = project.open()
|
|
unsavedBuffer.setText("OMGWTFBBQ")
|
|
buffer = Buffer.deserialize(unsavedBuffer.serialize(), project)
|
|
expect(buffer).toBe unsavedBuffer
|
|
|
|
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 'change', 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"
|
|
|
|
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 'change', 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()", ->
|
|
describe "when the buffer has a path", ->
|
|
[filePath, buffer] = []
|
|
|
|
beforeEach ->
|
|
filePath = '/tmp/temp.txt'
|
|
fs.remove filePath if fs.exists(filePath)
|
|
buffer = new Buffer filePath
|
|
|
|
afterEach ->
|
|
fs.remove filePath if fs.exists(filePath)
|
|
|
|
it "saves the contents of the buffer to the path", ->
|
|
buffer.setText 'Buffer contents!'
|
|
buffer.save()
|
|
expect(fs.read(filePath)).toEqual 'Buffer contents!'
|
|
|
|
it "fires beforeSave and afterSave events around the call to fs.write", ->
|
|
events = []
|
|
beforeSave1 = -> events.push('beforeSave1')
|
|
beforeSave2 = -> events.push('beforeSave2')
|
|
afterSave1 = -> events.push('afterSave1')
|
|
afterSave2 = -> events.push('afterSave2')
|
|
|
|
buffer.on 'before-save', beforeSave1
|
|
buffer.on 'before-save', beforeSave2
|
|
spyOn(fs, 'write').andCallFake -> events.push 'fs.write'
|
|
buffer.on 'after-save', afterSave1
|
|
buffer.on 'after-save', afterSave2
|
|
|
|
buffer.save()
|
|
expect(events).toEqual ['beforeSave1', 'beforeSave2', 'fs.write', 'afterSave1', 'afterSave2']
|
|
|
|
describe "when the buffer no path", ->
|
|
it "throws an exception", ->
|
|
buffer = new Buffer
|
|
expect(-> buffer.save()).toThrow()
|
|
|
|
describe ".saveAs(path)", ->
|
|
filePath = null
|
|
|
|
beforeEach ->
|
|
filePath = require.resolve('fixtures') + '/temp.txt'
|
|
expect(fs.exists(filePath)).toBeFalsy()
|
|
|
|
afterEach ->
|
|
fs.remove filePath
|
|
|
|
it "saves the contents of the buffer to the path", ->
|
|
buffer = new Buffer()
|
|
eventHandler = jasmine.createSpy('eventHandler')
|
|
buffer.on 'path-change', eventHandler
|
|
|
|
buffer.setText 'Buffer contents!'
|
|
buffer.saveAs(filePath)
|
|
expect(fs.read(filePath)).toEqual 'Buffer contents!'
|
|
|
|
expect(eventHandler).toHaveBeenCalledWith(buffer)
|
|
|
|
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 ".scanInRange(range, regex, fn)", ->
|
|
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]], (match, 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]], (match, range, { 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]], (match, 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]], (match, 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]], (match, 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 charachters 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
|
|
|
|
describe ".positionForCharacterIndex(position)", ->
|
|
it "returns the position based on charachter 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 "undo methods", ->
|
|
describe "path-change event", ->
|
|
it "emits path-change event when path is changed", ->
|
|
eventHandler = jasmine.createSpy('eventHandler')
|
|
buffer.on 'path-change', eventHandler
|
|
buffer.setPath("moo.text")
|
|
expect(eventHandler).toHaveBeenCalledWith(buffer)
|