diff --git a/spec/text-buffer-spec.coffee b/spec/text-buffer-spec.coffee deleted file mode 100644 index 70e1a773f..000000000 --- a/spec/text-buffer-spec.coffee +++ /dev/null @@ -1,1058 +0,0 @@ -{_, fs} = require 'atom' -path = require 'path' -temp = require 'temp' -TextBuffer = require '../src/text-buffer' - -describe 'TextBuffer', -> - [filePath, fileContents, buffer] = [] - - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - fileContents = fs.readFileSync(filePath, 'utf8') - buffer = atom.project.bufferForPathSync(filePath) - - afterEach -> - buffer?.destroy() - - describe 'constructor', -> - beforeEach -> - buffer.destroy() - 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 = atom.project.bufferForPathSync(filePath) - expect(buffer.getText()).toBe fs.readFileSync(filePath, 'utf8') - - it "does not allow the initial state of the buffer to be undone", -> - filePath = require.resolve './fixtures/sample.txt' - buffer = atom.project.bufferForPathSync(filePath) - buffer.undo() - expect(buffer.getText()).toBe fs.readFileSync(filePath, 'utf8') - - describe "when no file exists for the path", -> - it "is not modified and is initially empty", -> - filePath = "does-not-exist.txt" - expect(fs.existsSync(filePath)).toBeFalsy() - buffer = atom.project.bufferForPathSync(filePath) - expect(buffer.isModified()).not.toBeTruthy() - expect(buffer.getText()).toBe '' - - describe "when no path is given", -> - it "creates an empty buffer", -> - buffer = atom.project.bufferForPathSync(null) - expect(buffer .getText()).toBe "" - - describe "path-changed event", -> - [filePath, newPath, bufferToChange, eventHandler] = [] - - beforeEach -> - filePath = path.join(__dirname, "fixtures", "atom-manipulate-me") - newPath = "#{filePath}-i-moved" - fs.writeFileSync(filePath, "") - bufferToChange = atom.project.bufferForPathSync(filePath) - eventHandler = jasmine.createSpy('eventHandler') - bufferToChange.on 'path-changed', eventHandler - - afterEach -> - bufferToChange.destroy() - fs.removeSync(filePath) if fs.existsSync(filePath) - fs.removeSync(newPath) if fs.existsSync(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") - - fs.removeSync(newPath) if fs.existsSync(newPath) - fs.moveSync(filePath, newPath) - - waitsFor "buffer path change", -> - eventHandler.callCount > 0 - - runs -> - expect(eventHandler).toHaveBeenCalledWith(bufferToChange) - - describe "when the buffer's on-disk contents change", -> - filePath = null - - beforeEach -> - buffer.release() - filePath = temp.openSync('atom').path - fs.writeFileSync(filePath, "first") - buffer = atom.project.bufferForPathSync(filePath).retain() - - afterEach -> - buffer.release() - buffer = null - - 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 - fs.writeFileSync(filePath, "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, 0]] - expect(event.newRange).toEqual [[0, 0], [0, 6]] - expect(event.oldText).toBe "" - expect(event.newText).toBe "second" - - [event] = changeHandler.argsForCall[1] - expect(event.oldRange).toEqual [[0, 6], [0, 11]] - expect(event.newRange).toEqual [[0, 6], [0, 6]] - expect(event.oldText).toBe "first" - expect(event.newText).toBe "" - - 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") - fs.writeFileSync(filePath, "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.setText("a change") - buffer.save() - buffer.insert([0, 0], "a second change") - - handler = jasmine.createSpy('fileChange') - fs.writeFileSync(filePath, "a disk change") - 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)", -> - [filePath, bufferToDelete] = [] - - beforeEach -> - filePath = path.join(temp.dir, 'atom-file-to-delete.txt') - fs.writeFileSync(filePath, 'delete me') - bufferToDelete = atom.project.bufferForPathSync(filePath) - filePath = bufferToDelete.getPath() # symlinks may have been converted - - expect(bufferToDelete.getPath()).toBe filePath - - afterEach -> - bufferToDelete.destroy() - - describe "when the file is modified", -> - beforeEach -> - bufferToDelete.setText("I WAS MODIFIED") - expect(bufferToDelete.isModified()).toBeTruthy() - - removeHandler = jasmine.createSpy('removeHandler') - bufferToDelete.file.on 'removed', removeHandler - fs.removeSync(filePath) - waitsFor "file to be removed", -> - removeHandler.callCount > 0 - - it "retains its path and reports the buffer as modified", -> - expect(bufferToDelete.getPath()).toBe filePath - expect(bufferToDelete.isModified()).toBeTruthy() - - describe "when the file is not modified", -> - beforeEach -> - expect(bufferToDelete.isModified()).toBeFalsy() - - removeHandler = jasmine.createSpy('removeHandler') - bufferToDelete.file.on 'removed', removeHandler - fs.removeSync(filePath) - waitsFor "file to be removed", -> - removeHandler.callCount > 0 - - it "retains its path and reports the buffer as not modified", -> - expect(bufferToDelete.getPath()).toBe filePath - expect(bufferToDelete.isModified()).toBeFalsy() - - it "resumes watching of the file when it is re-saved", -> - bufferToDelete.save() - expect(fs.existsSync(bufferToDelete.getPath())).toBeTruthy() - expect(bufferToDelete.isInConflict()).toBeFalsy() - - fs.writeFileSync(filePath, '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 false after a modified buffer is saved", -> - filePath = path.join(temp.dir, 'atom-tmp-file') - fs.writeFileSync(filePath, '') - buffer.release() - buffer = atom.project.bufferForPathSync(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 = path.join(temp.dir, 'atom-tmp-file') - fs.writeFileSync(filePath, '') - buffer.release() - buffer = atom.project.bufferForPathSync(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 = path.join(temp.dir, 'atom-tmp-file') - fs.removeSync(filePath) if fs.existsSync(filePath) - expect(fs.existsSync(filePath)).toBeFalsy() - buffer.release() - buffer = atom.project.bufferForPathSync(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(fs.existsSync(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 = atom.project.bufferForPathSync(null) - expect(buffer.isModified()).toBeFalsy() - - it "returns true for a non-empty buffer with no path", -> - buffer.release() - buffer = atom.project.bufferForPathSync(null) - buffer.setText('a') - expect(buffer.isModified()).toBeTruthy() - buffer.setText('\n') - expect(buffer.isModified()).toBeTruthy() - - it "returns false until the buffer is fully loaded", -> - buffer.release() - buffer = new TextBuffer({filePath: temp.openSync('atom').path}) - atom.project.addBuffer(buffer) - - expect(buffer.isModified()).toBeFalsy() - - waitsForPromise -> - buffer.load() - - runs -> - expect(buffer.isModified()).toBeFalsy() - - 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.once '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 ".setTextViaDiff(text)", -> - it "can change the entire contents of the buffer when there are no newlines", -> - buffer.setText('BUFFER CHANGE') - newText = 'DISK CHANGE' - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - describe "with standard newlines", -> - it "can change the entire contents of the buffer with no newline at the end", -> - newText = "I know you are.\nBut what am I?" - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "can change the entire contents of the buffer with a newline at the end", -> - newText = "I know you are.\nBut what am I?\n" - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "can change a few lines at the beginning in the buffer", -> - newText = buffer.getText().replace(/function/g, 'omgwow') - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "can change a few lines in the middle of the buffer", -> - newText = buffer.getText().replace(/shift/g, 'omgwow') - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "can adds a newline at the end", -> - newText = buffer.getText() + '\n' - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - describe "with windows newlines", -> - beforeEach -> - buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) - - it "adds a newline at the end", -> - newText = buffer.getText() + '\r\n' - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "changes the entire contents of the buffer with smaller content with no newline at the end", -> - newText = "I know you are.\r\nBut what am I?" - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "changes the entire contents of the buffer with smaller content with newline at the end", -> - newText = "I know you are.\r\nBut what am I?\r\n" - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "changes a few lines at the beginning in the buffer", -> - newText = buffer.getText().replace(/function/g, 'omgwow') - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "changes a few lines in the middle of the buffer", -> - newText = buffer.getText().replace(/shift/g, 'omgwow') - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - describe ".save()", -> - saveBuffer = null - - afterEach -> - saveBuffer.release() - - describe "when the buffer has a path", -> - filePath = null - - beforeEach -> - filePath = path.join(temp.dir, 'temp.txt') - fs.writeFileSync(filePath, "") - saveBuffer = atom.project.bufferForPathSync(filePath) - saveBuffer.setText("blah") - - it "saves the contents of the buffer to the path", -> - saveBuffer.setText 'Buffer contents!' - saveBuffer.save() - expect(fs.readFileSync(filePath, 'utf8')).toEqual 'Buffer contents!' - - it "fires will-be-saved and saved events around the call to fs.writeFileSync", -> - 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(fs, 'writeFileSync').andCallFake -> events.push 'fs.writeFileSync' - saveBuffer.on 'saved', afterSave1 - saveBuffer.on 'saved', afterSave2 - - saveBuffer.save() - expect(events).toEqual ['beforeSave1', 'beforeSave2', 'fs.writeFileSync', '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'] - - it "no longer reports being in conflict", -> - saveBuffer.setText('a') - saveBuffer.save() - saveBuffer.setText('ab') - - fs.writeFileSync(saveBuffer.getPath(), 'c') - conflictHandler = jasmine.createSpy('conflictHandler') - saveBuffer.on 'contents-conflicted', conflictHandler - - waitsFor -> - conflictHandler.callCount > 0 - - runs -> - expect(saveBuffer.isInConflict()).toBe true - saveBuffer.save() - expect(saveBuffer.isInConflict()).toBe false - - describe "when the buffer has no path", -> - it "throws an exception", -> - saveBuffer = atom.project.bufferForPathSync(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 = path.join(temp.dir, 'temp.txt') - fs.removeSync filePath if fs.existsSync(filePath) - - saveAsBuffer = atom.project.bufferForPathSync(null).retain() - eventHandler = jasmine.createSpy('eventHandler') - saveAsBuffer.on 'path-changed', eventHandler - - saveAsBuffer.setText 'Buffer contents!' - saveAsBuffer.saveAs(filePath) - expect(fs.readFileSync(filePath, 'utf8')).toEqual 'Buffer contents!' - - expect(eventHandler).toHaveBeenCalledWith(saveAsBuffer) - - it "stops listening to events on previous path and begins listening to events on new path", -> - originalPath = path.join(temp.dir, 'original.txt') - newPath = path.join(temp.dir, 'new.txt') - fs.writeFileSync(originalPath, "") - - saveAsBuffer = atom.project.bufferForPathSync(originalPath).retain() - changeHandler = jasmine.createSpy('changeHandler') - saveAsBuffer.on 'changed', changeHandler - saveAsBuffer.saveAs(newPath) - expect(changeHandler).not.toHaveBeenCalled() - - fs.writeFileSync(originalPath, "should not trigger buffer event") - waits 20 - runs -> - expect(changeHandler).not.toHaveBeenCalled() - fs.writeFileSync(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 ".scan(regex, fn)", -> - it "retunrns lineText and lineTextOffset", -> - matches = [] - buffer.scan /current/, (match) -> - matches.push(match) - expect(matches.length).toBe 1 - - expect(matches[0].matchText).toEqual 'current' - expect(matches[0].lineText).toEqual ' var pivot = items.shift(), current, left = [], right = [];' - expect(matches[0].lineTextOffset).toBe 0 - - 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 ".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 ".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 "serialization", -> - buffer2 = null - - beforeEach -> - buffer.destroy() - - filePath = temp.openSync('atom').path - fs.writeFileSync(filePath, "words") - buffer = atom.project.bufferForPathSync(filePath).retain() - - afterEach -> - buffer2?.destroy() - - describe "when the serialized buffer had no unsaved changes", -> - it "loads the current contents of the file at the serialized path", -> - expect(buffer.isModified()).toBeFalsy() - buffer2 = buffer.testSerialization() - - waitsForPromise -> - buffer2.load() - - runs -> - expect(buffer2.isModified()).toBeFalsy() - expect(buffer2.getPath()).toBe(buffer.getPath()) - expect(buffer2.getText()).toBe(buffer.getText()) - - describe "when the serialized buffer had unsaved changes", -> - describe "when the disk contents were changed since serialization", -> - it "loads the disk contents instead of the previous unsaved state", -> - buffer.setText("BUFFER CHANGE") - fs.writeFileSync(filePath, "DISK CHANGE") - - buffer2 = buffer.testSerialization() - - waitsFor -> - buffer2.cachedDiskContents - - runs -> - expect(buffer2.getPath()).toBe(buffer.getPath()) - expect(buffer2.getText()).toBe("DISK CHANGE") - expect(buffer2.isModified()).toBeFalsy() - - describe "when the disk contents are the same since serialization", -> - it "restores the previous unsaved state of the buffer", -> - previousText = buffer.getText() - buffer.setText("abc") - buffer.retain() - - buffer2 = buffer.testSerialization() - - waitsForPromise -> - buffer2.load() - - runs -> - expect(buffer2.getPath()).toBe(buffer.getPath()) - expect(buffer2.getText()).toBe(buffer.getText()) - expect(buffer2.isModified()).toBeTruthy() - buffer2.setText(previousText) - expect(buffer2.isModified()).toBeFalsy() - - describe "when the serialized buffer was unsaved and had no path", -> - it "restores the previous unsaved state of the buffer", -> - buffer.destroy() - - buffer = atom.project.bufferForPathSync() - buffer.setText("abc") - - buffer2 = buffer.testSerialization() - expect(buffer2.getPath()).toBeUndefined() - expect(buffer2.getText()).toBe("abc") diff --git a/src/atom.coffee b/src/atom.coffee index 2f1cfaffd..e85bbb120 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -161,7 +161,8 @@ class Atom extends Model @subscribe @packages, 'activated', => @watchThemes() Project = require './project' - TextBuffer = require './text-buffer' + TextBuffer = require 'text-buffer' + @deserializers.add(TextBuffer) TokenizedBuffer = require './tokenized-buffer' DisplayBuffer = require './display-buffer' Editor = require './editor' diff --git a/src/editor-view.coffee b/src/editor-view.coffee index b64f41379..4aa277192 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -1,5 +1,4 @@ {View, $, $$$} = require './space-pen-extensions' -TextBuffer = require './text-buffer' GutterView = require './gutter-view' {Point, Range} = require 'text-buffer' Editor = require './editor' @@ -7,6 +6,7 @@ CursorView = require './cursor-view' SelectionView = require './selection-view' fs = require 'fs-plus' _ = require 'underscore-plus' +TextBuffer = require 'text-buffer' MeasureRange = document.createRange() TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT } diff --git a/src/project.coffee b/src/project.coffee index d93ef4e4c..ea7574db9 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -7,8 +7,8 @@ Q = require 'q' {Model} = require 'theorist' {Emitter, Subscriber} = require 'emissary' Serializable = require 'serializable' +TextBuffer = require 'text-buffer' -TextBuffer = require './text-buffer' Editor = require './editor' Directory = require './directory' Task = require './task' diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee deleted file mode 100644 index e41a792e2..000000000 --- a/src/text-buffer.coffee +++ /dev/null @@ -1,404 +0,0 @@ -_ = require 'underscore-plus' -Q = require 'q' -{P} = require 'scandal' -Serializable = require 'serializable' -TextBufferCore = require 'text-buffer' -{Point, Range} = TextBufferCore -{Subscriber, Emitter} = require 'emissary' -{File} = require 'pathwatcher' - -# Represents the contents of a file. -# -# The `TextBuffer` is often associated with a {File}. However, this is not -# always the case, as a `TextBuffer` could contain an unsaved chunk of text. -module.exports = -class TextBuffer extends TextBufferCore - atom.deserializers.add(this) - - Serializable.includeInto(this) - Subscriber.includeInto(this) - Emitter.includeInto(this) - - stoppedChangingDelay: 300 - stoppedChangingTimeout: null - cachedDiskContents: null - conflict: false - file: null - refcount: 0 - - constructor: ({filePath, @modifiedWhenLastPersisted, @digestWhenLastPersisted, loadWhenAttached}={}) -> - super - @loaded = false - @modifiedWhenLastPersisted ?= false - - @useSerializedText = @modifiedWhenLastPersisted != false - - @subscribe this, 'changed', @handleTextChange - - @setPath(filePath) - - @load() if loadWhenAttached - - serializeParams: -> - params = super - _.extend params, - filePath: @getPath() - modifiedWhenLastPersisted: @isModified() - digestWhenLastPersisted: @file?.getDigest() - - deserializeParams: (params) -> - params = super(params) - params.loadWhenAttached = true - params - - loadSync: -> - @updateCachedDiskContentsSync() - @finishLoading() - - load: -> - @updateCachedDiskContents().then => @finishLoading() - - finishLoading: -> - if @isAlive() - @loaded = true - if @useSerializedText and @digestWhenLastPersisted is @file?.getDigest() - @emitModifiedStatusChanged(true) - else - @reload() - @clearUndoStack() - this - - handleTextChange: (event) => - @conflict = false if @conflict and !@isModified() - @scheduleModifiedEvents() - - destroy: -> - unless @destroyed - @cancelStoppedChangingTimeout() - @file?.off() - @unsubscribe() - @destroyed = true - @emit 'destroyed' - - isAlive: -> not @destroyed - - isDestroyed: -> @destroyed - - isRetained: -> @refcount > 0 - - retain: -> - @refcount++ - this - - release: -> - @refcount-- - @destroy() unless @isRetained() - this - - subscribeToFile: -> - @file.on "contents-changed", => - @conflict = true if @isModified() - previousContents = @cachedDiskContents - - # Synchrounously update the disk contents because the {File} has already cached them. If the - # contents updated asynchrounously multiple `conlict` events could trigger for the same disk - # contents. - @updateCachedDiskContentsSync() - return if previousContents == @cachedDiskContents - - if @conflict - @emit "contents-conflicted" - else - @reload() - - @file.on "removed", => - modified = @getText() != @cachedDiskContents - @wasModifiedBeforeRemove = modified - if modified - @updateCachedDiskContents() - else - @destroy() - - @file.on "moved", => - @emit "path-changed", this - - # Identifies if the buffer belongs to multiple editors. - # - # For example, if the {EditorView} was split. - # - # Returns a {Boolean}. - hasMultipleEditors: -> @refcount > 1 - - # Reloads a file in the {Editor}. - # - # Sets the buffer's content to the cached disk contents - reload: -> - @emit 'will-reload' - @setTextViaDiff(@cachedDiskContents) - @emitModifiedStatusChanged(false) - @emit 'reloaded' - - # Rereads the contents of the file, and stores them in the cache. - updateCachedDiskContentsSync: -> - @cachedDiskContents = @file?.readSync() ? "" - - # Rereads the contents of the file, and stores them in the cache. - updateCachedDiskContents: -> - Q(@file?.read() ? "").then (contents) => - @cachedDiskContents = contents - - # Gets the file's basename--that is, the file without any directory information. - # - # Returns a {String}. - getBaseName: -> - @file?.getBaseName() - - # Retrieves the path for the file. - # - # Returns a {String}. - getPath: -> - @file?.getPath() - - getUri: -> - atom.project.relativize(@getPath()) - - # Sets the path for the file. - # - # filePath - A {String} representing the new file path - setPath: (filePath) -> - return if filePath == @getPath() - - @file?.off() - - if filePath - @file = new File(filePath) - @subscribeToFile() - else - @file = null - - @emit "path-changed", this - - # Deprecated: Use ::getEndPosition instead - getEofPosition: -> @getEndPosition() - - # Saves the buffer. - save: -> - @saveAs(@getPath()) if @isModified() - - # Saves the buffer at a specific path. - # - # filePath - The path to save at. - saveAs: (filePath) -> - unless filePath then throw new Error("Can't save buffer with no file path") - - @emit 'will-be-saved', this - @setPath(filePath) - @file.write(@getText()) - @cachedDiskContents = @getText() - @conflict = false - @emitModifiedStatusChanged(false) - @emit 'saved', this - - # Identifies if the buffer was modified. - # - # Returns a {Boolean}. - isModified: -> - return false unless @loaded - if @file - if @file.exists() - @getText() != @cachedDiskContents - else - @wasModifiedBeforeRemove ? not @isEmpty() - else - not @isEmpty() - - # Is the buffer's text in conflict with the text on disk? - # - # This occurs when the buffer's file changes on disk while the buffer has - # unsaved changes. - # - # Returns a {Boolean}. - isInConflict: -> @conflict - - destroyMarker: (id) -> - @getMarker(id)?.destroy() - - # Identifies if a character sequence is within a certain range. - # - # regex - The {RegExp} to check - # startIndex - The starting row {Number} - # endIndex - The ending row {Number} - # - # Returns an {Array} of {RegExp}s, representing the matches. - matchesInCharacterRange: (regex, startIndex, endIndex) -> - text = @getText() - matches = [] - - regex.lastIndex = startIndex - while match = regex.exec(text) - matchLength = match[0].length - matchStartIndex = match.index - matchEndIndex = matchStartIndex + matchLength - - if matchEndIndex > endIndex - regex.lastIndex = 0 - if matchStartIndex < endIndex and submatch = regex.exec(text[matchStartIndex...endIndex]) - submatch.index = matchStartIndex - matches.push submatch - break - - matchEndIndex++ if matchLength is 0 - regex.lastIndex = matchEndIndex - matches.push match - - matches - - # Scans for text in the buffer, calling a function on each match. - # - # regex - A {RegExp} representing the text to find - # iterator - A {Function} that's called on each match - scan: (regex, iterator) -> - @scanInRange regex, @getRange(), (result) => - result.lineText = @lineForRow(result.range.start.row) - result.lineTextOffset = 0 - iterator(result) - - # Replace all matches of regex with replacementText - # - # regex: A {RegExp} representing the text to find - # replacementText: A {String} representing the text to replace - # - # Returns the number of replacements made - replace: (regex, replacementText) -> - doSave = !@isModified() - replacements = 0 - - @transact => - @scan regex, ({matchText, replace}) -> - replace(matchText.replace(regex, replacementText)) - replacements++ - - @save() if doSave - - replacements - - # Scans for text in a given range, calling a function on each match. - # - # regex - A {RegExp} representing the text to find - # range - A {Range} in the buffer to search within - # iterator - A {Function} that's called on each match - # reverse - A {Boolean} indicating if the search should be backwards (default: `false`) - scanInRange: (regex, range, iterator, reverse=false) -> - range = @clipRange(range) - global = regex.global - flags = "gm" - flags += "i" if regex.ignoreCase - regex = new RegExp(regex.source, flags) - - startIndex = @characterIndexForPosition(range.start) - endIndex = @characterIndexForPosition(range.end) - - matches = @matchesInCharacterRange(regex, startIndex, endIndex) - lengthDelta = 0 - - keepLooping = null - replacementText = null - stop = -> keepLooping = false - replace = (text) -> replacementText = text - - matches.reverse() if reverse - for match in matches - matchLength = match[0].length - matchStartIndex = match.index - matchEndIndex = matchStartIndex + matchLength - - startPosition = @positionForCharacterIndex(matchStartIndex + lengthDelta) - endPosition = @positionForCharacterIndex(matchEndIndex + lengthDelta) - range = new Range(startPosition, endPosition) - keepLooping = true - replacementText = null - matchText = match[0] - iterator({ match, matchText, range, stop, replace }) - - if replacementText? - @change(range, replacementText) - lengthDelta += replacementText.length - matchLength unless reverse - - break unless global and keepLooping - - # Scans for text in a given range _backwards_, calling a function on each match. - # - # regex - A {RegExp} representing the text to find - # range - A {Range} in the buffer to search within - # iterator - A {Function} that's called on each match - backwardsScanInRange: (regex, range, iterator) -> - @scanInRange regex, range, iterator, true - - # Given a row, identifies if it is blank. - # - # row - A row {Number} to check - # - # Returns a {Boolean}. - isRowBlank: (row) -> - not /\S/.test @lineForRow(row) - - # Given a row, this finds the next row above it that's empty. - # - # startRow - A {Number} identifying the row to start checking at - # - # Returns the row {Number} of the first blank row. - # Returns `null` if there's no other blank row. - previousNonBlankRow: (startRow) -> - return null if startRow == 0 - - startRow = Math.min(startRow, @getLastRow()) - for row in [(startRow - 1)..0] - return row unless @isRowBlank(row) - null - - # Given a row, this finds the next row that's blank. - # - # startRow - A row {Number} to check - # - # Returns the row {Number} of the next blank row. - # Returns `null` if there's no other blank row. - nextNonBlankRow: (startRow) -> - lastRow = @getLastRow() - if startRow < lastRow - for row in [(startRow + 1)..lastRow] - return row unless @isRowBlank(row) - null - - # Identifies if the buffer has soft tabs anywhere. - # - # Returns a {Boolean}, - usesSoftTabs: -> - for row in [0..@getLastRow()] - if match = @lineForRow(row).match(/^\s/) - return match[0][0] != '\t' - undefined - - change: (oldRange, newText, options={}) -> - @setTextInRange(oldRange, newText, options.normalizeLineEndings) - - cancelStoppedChangingTimeout: -> - clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout - - scheduleModifiedEvents: -> - @cancelStoppedChangingTimeout() - stoppedChangingCallback = => - @stoppedChangingTimeout = null - modifiedStatus = @isModified() - @emit 'contents-modified', modifiedStatus - @emitModifiedStatusChanged(modifiedStatus) - @stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay) - - emitModifiedStatusChanged: (modifiedStatus) -> - return if modifiedStatus is @previousModifiedStatus - @previousModifiedStatus = modifiedStatus - @emit 'modified-status-changed', modifiedStatus - - logLines: (start=0, end=@getLastRow())-> - for row in [start..end] - line = @lineForRow(row) - console.log row, line, line.length