Merge branch 'master' into ns-telepathic-atom-global

Conflicts:
	package.json
This commit is contained in:
Nathan Sobo
2013-12-13 09:32:13 -08:00
13 changed files with 216 additions and 60 deletions

View File

@@ -1,4 +1,4 @@
{Document, Point, Range} = require 'telepath'
{Document, Model, Point, Range} = require 'telepath'
module.exports =
_: require 'underscore-plus'
@@ -9,6 +9,7 @@ module.exports =
File: require '../src/file'
fs: require 'fs-plus'
Git: require '../src/git'
Model: Model
Point: Point
Range: Range

View File

@@ -23,6 +23,7 @@
"clear-cut": "0.2.0",
"coffee-script": "1.6.3",
"coffeestack": "0.6.0",
"diff": "git://github.com/benogle/jsdiff.git",
"emissary": "0.19.0",
"first-mate": "0.5.0",
"fs-plus": "0.11.0",
@@ -42,7 +43,7 @@
"scandal": "0.8.0",
"season": "0.14.0",
"semver": "1.1.4",
"space-pen": "2.0.1",
"space-pen": "2.0.2",
"telepath": "0.73.0",
"temp": "0.5.0",
"underscore-plus": "0.5.0"
@@ -73,12 +74,13 @@
"github-releases": "~0.2.0"
},
"packageDependencies": {
"atom-dark-syntax": "0.8.0",
"atom-dark-syntax": "0.10.0",
"atom-dark-ui": "0.17.0",
"atom-light-syntax": "0.9.0",
"atom-light-syntax": "0.10.0",
"atom-light-ui": "0.16.0",
"base16-tomorrow-dark-theme": "0.7.0",
"solarized-dark-syntax": "0.5.0",
"base16-tomorrow-dark-theme": "0.8.0",
"solarized-dark-syntax": "0.6.0",
"solarized-light-syntax": "0.2.0",
"archive-view": "0.16.0",
"autocomplete": "0.18.0",
"autoflow": "0.11.0",
@@ -87,14 +89,14 @@
"bracket-matcher": "0.15.0",
"command-logger": "0.8.0",
"command-palette": "0.13.0",
"dev-live-reload": "0.18.0",
"editor-stats": "0.8.0",
"exception-reporting": "0.8.0",
"dev-live-reload": "0.20.0",
"editor-stats": "0.9.0",
"exception-reporting": "0.9.0",
"feedback": "0.16.0",
"find-and-replace": "0.58.0",
"find-and-replace": "0.59.0",
"fuzzy-finder": "0.28.0",
"gists": "0.13.0",
"git-diff": "0.20.0",
"git-diff": "0.21.0",
"github-sign-in": "0.15.0",
"go-to-line": "0.12.0",
"grammar-selector": "0.13.0",
@@ -115,7 +117,7 @@
"terminal": "0.23.0",
"timecop": "0.11.0",
"to-the-hubs": "0.15.0",
"tree-view": "0.44.0",
"tree-view": "0.48.0",
"visual-bell": "0.6.0",
"welcome": "0.3.0",
"whitespace": "0.10.0",

View File

@@ -2624,6 +2624,7 @@ describe "Editor", ->
describe ".shouldPromptToSave()", ->
it "returns false when an edit session's buffer is in use by more than one session", ->
jasmine.unspy(editor, 'shouldPromptToSave')
expect(editor.shouldPromptToSave()).toBeFalsy()
buffer.setText('changed')
expect(editor.shouldPromptToSave()).toBeTruthy()

View File

@@ -1,6 +1,6 @@
PaneContainer = require '../src/pane-container'
Pane = require '../src/pane'
{$, View} = require 'atom'
{fs, $, View} = require 'atom'
path = require 'path'
temp = require 'temp'
@@ -147,6 +147,7 @@ describe "Pane", ->
describe "if the item is modified", ->
beforeEach ->
jasmine.unspy(editor2, 'shouldPromptToSave')
spyOn(editor2, 'save')
spyOn(editor2, 'saveAs')
@@ -347,14 +348,14 @@ describe "Pane", ->
describe "when the current item has a saveAs method", ->
it "opens a save dialog and saves the current item as the selected path", ->
spyOn(editor2, 'saveAs')
editor2.buffer.setPath(undefined)
pane.showItem(editor2)
newEditor = atom.project.openSync()
spyOn(newEditor, 'saveAs')
pane.showItem(newEditor)
pane.trigger 'core:save'
expect(atom.showSaveDialogSync).toHaveBeenCalled()
expect(editor2.saveAs).toHaveBeenCalledWith('/selected/path')
expect(newEditor.saveAs).toHaveBeenCalledWith('/selected/path')
describe "when the current item has no saveAs method", ->
it "does nothing", ->
@@ -421,6 +422,17 @@ describe "Pane", ->
view2.trigger 'title-changed'
expect(activeItemTitleChangedHandler).toHaveBeenCalled()
describe "when an unmodifed buffer's path is deleted", ->
it "removes the pane item", ->
filePath = temp.openSync('atom').path
editor = atom.project.openSync(filePath)
pane.showItem(editor)
expect(pane.items).toHaveLength(5)
fs.removeSync(filePath)
waitsFor ->
pane.items.length == 4
describe ".remove()", ->
it "destroys all the pane's items", ->
pane.remove()

View File

@@ -9,6 +9,7 @@ Keymap = require '../src/keymap'
Config = require '../src/config'
{Point} = require 'telepath'
Project = require '../src/project'
Editor = require '../src/editor'
EditorView = require '../src/editor-view'
TokenizedBuffer = require '../src/tokenized-buffer'
pathwatcher = require 'pathwatcher'
@@ -91,6 +92,7 @@ beforeEach ->
spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout
spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout
spyOn(File.prototype, "detectResurrectionAfterDelay").andCallFake -> @detectResurrection()
spyOn(Editor.prototype, "shouldPromptToSave").andReturn false
# make tokenization synchronous
TokenizedBuffer.prototype.chunkSize = Infinity

View File

@@ -34,11 +34,11 @@ describe 'TextBuffer', ->
expect(buffer.getText()).toBe fs.readFileSync(filePath, 'utf8')
describe "when no file exists for the path", ->
it "is modified and is initially empty", ->
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()).toBeTruthy()
expect(buffer.isModified()).not.toBeTruthy()
expect(buffer.getText()).toBe ''
describe "when no path is given", ->
@@ -113,10 +113,17 @@ describe 'TextBuffer', ->
runs ->
[event] = changeHandler.argsForCall[0]
expect(event.oldRange).toEqual [[0, 0], [0, 5]]
expect(event.oldRange).toEqual [[0, 0], [0, 0]]
expect(event.newRange).toEqual [[0, 0], [0, 6]]
expect(event.oldText).toBe "first"
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", ->
@@ -160,20 +167,38 @@ describe 'TextBuffer', ->
filePath = bufferToDelete.getPath() # symlinks may have been converted
expect(bufferToDelete.getPath()).toBe filePath
expect(bufferToDelete.isModified()).toBeFalsy()
removeHandler = jasmine.createSpy('removeHandler')
bufferToDelete.file.on 'removed', removeHandler
fs.removeSync(filePath)
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 filePath
expect(bufferToDelete.isModified()).toBeTruthy()
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()
@@ -210,19 +235,6 @@ describe 'TextBuffer', ->
advanceClock(buffer.stoppedChangingDelay)
expect(modifiedHandler).toHaveBeenCalledWith(false)
it "reports the modified status changing to true after the underlying file is deleted", ->
buffer.release()
filePath = path.join(temp.dir, 'atom-tmp-file')
fs.writeFileSync(filePath, 'delete me')
buffer = atom.project.bufferForPathSync(filePath)
modifiedHandler = jasmine.createSpy("modifiedHandler")
buffer.on 'modified-status-changed', modifiedHandler
fs.removeSync(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 = path.join(temp.dir, 'atom-tmp-file')
fs.writeFileSync(filePath, '')
@@ -454,6 +466,68 @@ describe 'TextBuffer', ->
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

View File

@@ -1,5 +1,6 @@
{$, $$, fs} = require 'atom'
path = require 'path'
Editor = require '../src/editor'
WindowEventHandler = require '../src/window-event-handler'
describe "Window", ->
@@ -54,6 +55,7 @@ describe "Window", ->
[beforeUnloadEvent] = []
beforeEach ->
jasmine.unspy(Editor.prototype, "shouldPromptToSave")
beforeUnloadEvent = $.Event(new Event('beforeunload'))
describe "when pane items are are modified", ->

View File

@@ -191,7 +191,7 @@ class Atom extends Model
@menu.update()
$(window).on 'unload', =>
$(document.body).hide()
$(document.body).css('visibility', 'hidden')
@unloadEditorWindow()
false

View File

@@ -780,6 +780,14 @@ class EditorView extends View
afterAttach: (onDom) ->
return unless onDom
# TODO: Remove this guard when we understand why this is happening
unless @editor.isAlive()
if atom.isReleasedVersion()
return
else
throw new Error("Assertion failure: EditorView is getting attached to a dead editor. Why?")
@redraw() if @redrawOnReattach
return if @attached
@attached = true

View File

@@ -110,6 +110,7 @@ class Editor extends Model
@subscribe @buffer, "contents-modified", => @emit "contents-modified"
@subscribe @buffer, "contents-conflicted", => @emit "contents-conflicted"
@subscribe @buffer, "modified-status-changed", => @emit "modified-status-changed"
@subscribe @buffer, "destroyed", => @destroy()
@preserveCursorPositionOnBufferReload()
# Private:

View File

@@ -31,6 +31,7 @@ class Pane extends View
# Private:
initialize: (args...) ->
@items = []
if args[0] instanceof telepath.Document
@state = args[0]
@items = _.compact @state.get('items').map (item) ->
@@ -43,6 +44,8 @@ class Pane extends View
deserializer: 'Pane'
items: @items.map (item) -> item.getState?() ? item.serialize()
@handleItemEvents(item) for item in @items
@subscribe @state.get('items'), 'changed', ({index, removedValues, insertedValues, siteId}) =>
return if siteId is @state.siteId
for itemState in removedValues
@@ -188,8 +191,14 @@ class Pane extends View
@state.get('items').splice(index, 0, item.getState?() ? item.serialize()) if options.updateState ? true
@items.splice(index, 0, item)
@trigger 'pane:item-added', [item, index]
@handleItemEvents(item)
item
handleItemEvents: (item) ->
if _.isFunction(item.on)
@subscribe item, 'destroyed', =>
@destroyItem(item) if @state.isAlive()
# Public: Remove the currently active item.
destroyActiveItem: =>
@destroyItem(@activeItem)
@@ -197,11 +206,11 @@ class Pane extends View
# Public: Remove the specified item.
destroyItem: (item) ->
@unsubscribe(item) if _.isFunction(item.off)
@trigger 'pane:before-item-destroyed', [item]
container = @getContainer()
if @promptToSaveItem(item)
container.itemDestroyed(item)
@getContainer()?.itemDestroyed(item)
@removeItem(item)
item.destroy?()
true

View File

@@ -4,11 +4,10 @@ url = require 'url'
_ = require 'underscore-plus'
fs = require 'fs-plus'
Q = require 'q'
telepath = require 'telepath'
{Model} = require 'telepath'
TextBuffer = require './text-buffer'
Editor = require './editor'
{Emitter} = require 'emissary'
Directory = require './directory'
Task = require './task'
Git = require './git'
@@ -18,8 +17,7 @@ Git = require './git'
# Ultimately, a project is a git directory that's been opened. It's a collection
# of directories and files that you can operate on.
module.exports =
class Project extends telepath.Model
Emitter.includeInto(this)
class Project extends Model
@properties
buffers: []

View File

@@ -1,5 +1,5 @@
_ = require 'underscore-plus'
{Emitter, Subscriber} = require 'emissary'
diff = require 'diff'
Q = require 'q'
{P} = require 'scandal'
telepath = require 'telepath'
@@ -14,9 +14,6 @@ File = require './file'
# the case, as a `TextBuffer` could be an unsaved chunk of text.
module.exports =
class TextBuffer extends telepath.Model
Emitter.includeInto(this)
Subscriber.includeInto(this)
@properties
text: -> new telepath.String('', replicated: false)
filePath: null
@@ -87,7 +84,6 @@ class TextBuffer extends telepath.Model
@file?.off()
@unsubscribe()
@alreadyDestroyed = true
@emit 'destroyed', this
isRetained: -> @refcount > 0
@@ -117,8 +113,12 @@ class TextBuffer extends telepath.Model
@reload()
@file.on "removed", =>
@updateCachedDiskContents().done =>
@emitModifiedStatusChanged(@isModified())
modified = @getText() != @cachedDiskContents
@wasModifiedBeforeRemove = modified
if modified
@updateCachedDiskContents()
else
@destroy()
@file.on "moved", =>
@emit "path-changed", this
@@ -137,7 +137,7 @@ class TextBuffer extends telepath.Model
# Sets the buffer's content to the cached disk contents
reload: ->
@emit 'will-reload'
@setText(@cachedDiskContents)
@setTextViaDiff(@cachedDiskContents)
@emitModifiedStatusChanged(false)
@emit 'reloaded'
@@ -202,6 +202,52 @@ class TextBuffer extends telepath.Model
setText: (text) ->
@change(@getRange(), text, normalizeLineEndings: false)
# Private: Replaces the current buffer contents. Only apply the differences.
#
# text - A {String} containing the new buffer contents.
setTextViaDiff: (text) ->
currentText = @getText()
return if currentText == text
endsWithNewline = (str) ->
/[\r\n]+$/g.test(str)
computeBufferColumn = (str) ->
newlineIndex = Math.max(str.lastIndexOf('\n'), str.lastIndexOf('\r'))
if endsWithNewline(str)
0
else if newlineIndex == -1
str.length
else
str.length - newlineIndex - 1
@transact =>
row = 0
column = 0
currentPosition = [0, 0]
lineDiff = diff.diffLines(currentText, text)
changeOptions = normalizeLineEndings: false
for change in lineDiff
lineCount = change.value.match(/\n/g)?.length ? 0
currentPosition[0] = row
currentPosition[1] = column
if change.added
@change([currentPosition, currentPosition], change.value, changeOptions)
row += lineCount
column = computeBufferColumn(change.value)
else if change.removed
endRow = row + lineCount
endColumn = column + computeBufferColumn(change.value)
@change([currentPosition, [endRow, endColumn]], '', changeOptions)
else
row += lineCount
column = computeBufferColumn(change.value)
# Gets the range of the buffer contents.
#
# Returns a new {Range}, from `[0, 0]` to the end of the buffer.
@@ -403,7 +449,7 @@ class TextBuffer extends telepath.Model
if @file.exists()
@getText() != @cachedDiskContents
else
true
@wasModifiedBeforeRemove ? not @isEmpty()
else
not @isEmpty()