diff --git a/Atom/src/AtomController.mm b/Atom/src/AtomController.mm index 16ca86946..278036500 100644 --- a/Atom/src/AtomController.mm +++ b/Atom/src/AtomController.mm @@ -85,7 +85,19 @@ - (bool)keyEventOfType:(cef_handler_keyevent_type_t)type code:(int)code modifiers:(int)modifiers isSystemKey:(bool)isSystemKey isAfterJavaScript:(bool)isAfterJavaScript { if (isAfterJavaScript && type == KEYEVENT_RAWKEYDOWN && modifiers == KEY_META && code == 'R') { - _clientHandler->GetBrowser()->ReloadIgnoreCache(); + + CefRefPtr context = _clientHandler->GetBrowser()->GetMainFrame()->GetV8Context(); + CefRefPtr global = context->GetGlobal(); + + context->Enter(); + CefRefPtr retval; + CefRefPtr exception; + CefV8ValueList arguments; + global->GetValue("reload")->ExecuteFunction(global, arguments, retval, exception, true); + if (exception) _clientHandler->GetBrowser()->ReloadIgnoreCache(); + context->Exit(); + + return YES; } diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index ee48aac8e..b3435d978 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -16,7 +16,7 @@ NSString *stringFromCefV8Value(const CefRefPtr& value) { NativeHandler::NativeHandler() : CefV8Handler() { std::string extensionCode = "var $native = {}; (function() {"; - const char *functionNames[] = {"exists", "alert", "read", "write", "absolute", "list", "isFile", "isDirectory", "remove", "asyncList", "open", "openDialog", "quit", "writeToPasteboard", "readFromPasteboard", "showDevTools", "newWindow", "saveDialog", "exit", "watchPath", "unwatchPath", "makeDirectory", "move", "moveToTrash"}; + const char *functionNames[] = {"exists", "alert", "read", "write", "absolute", "list", "isFile", "isDirectory", "remove", "asyncList", "open", "openDialog", "quit", "writeToPasteboard", "readFromPasteboard", "showDevTools", "newWindow", "saveDialog", "exit", "watchPath", "unwatchPath", "makeDirectory", "move", "moveToTrash", "reload"}; NSUInteger arrayLength = sizeof(functionNames) / sizeof(const char *); for (NSUInteger i = 0; i < arrayLength; i++) { std::string functionName = std::string(functionNames[i]); @@ -375,5 +375,8 @@ bool NativeHandler::Execute(const CefString& name, return true; } + else if (name == "reload") { + CefV8Context::GetCurrentContext()->GetBrowser()->ReloadIgnoreCache(); + } return false; }; \ No newline at end of file diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 217546cbc..f1478a3bb 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -31,6 +31,43 @@ describe 'Buffer', -> buffer = new Buffer expect(buffer.getText()).toBe "" + 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) + + describe ".isModified()", -> + describe "when deserialized", -> + it "returns false", -> + buffer = Buffer.deserialize(buffer.serialize(), new Project) + expect(buffer.isModified()).toBe false + + buffer = Buffer.deserialize((new Buffer).serialize(), new Project) + expect(buffer.isModified()).toBe false + + it "returns is true if buffer no path and had changes", -> + buffer = new Buffer + buffer.insert([0,0], "oh hi") + expect(buffer.isModified()).toBe true + + it "returns true when user changes buffer", -> + expect(buffer.isModified()).toBeFalsy() + buffer.insert([0,0], "hi") + expect(buffer.isModified()).toBe true + + it "returns false after modified buffer is saved", -> + filePath = "/tmp/atom-tmp-file" + buffer = new Buffer(filePath) + expect(buffer.isModified()).toBe false + + buffer.insert([0,0], "hi") + expect(buffer.isModified()).toBe true + + buffer.save() + expect(buffer.isModified()).toBe false + describe '.deserialize(state, project)', -> project = null @@ -365,7 +402,7 @@ describe 'Buffer', -> expect(ranges.length).toBe 2 - describe "backwardsScanInRange(range, regex, fn)", -> + 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 = [] @@ -446,11 +483,3 @@ describe 'Buffer', -> expect(buffer.positionForCharacterIndex(30)).toEqual [1, 0] expect(buffer.positionForCharacterIndex(61)).toEqual [2, 0] expect(buffer.positionForCharacterIndex(408)).toEqual [12, 2] - - 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) - diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index f055e58c6..a1e186c83 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1549,6 +1549,7 @@ describe "Editor", -> expect(editor.buffer).toBe buffer it "calls remove on the editor if there is one edit session and mini is false", -> + originalBuffer = editor.buffer expect(editor.mini).toBeFalsy() expect(editor.editSessions.length).toBe 1 spyOn(editor, 'remove') @@ -1560,3 +1561,12 @@ describe "Editor", -> spyOn(editor, 'remove') editor.trigger 'close' expect(editor.remove).not.toHaveBeenCalled() + + describe "when buffer is modified", -> + it "triggers alert and does not close session", -> + spyOn(editor, 'remove') + spyOn($native, 'alert') + editor.insertText("I AM CHANGED!") + editor.trigger "close" + expect(editor.remove).not.toHaveBeenCalled() + expect($native.alert).toHaveBeenCalled() diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee index c83769218..3ae18b0c2 100644 --- a/spec/app/window-spec.coffee +++ b/spec/app/window-spec.coffee @@ -17,6 +17,20 @@ describe "Window", -> $(window).trigger 'close' expect(window.close).toHaveBeenCalled() + describe ".reload()", -> + it "returns false when no buffers are modified", -> + spyOn($native, "reload") + window.reload() + expect($native.reload).toHaveBeenCalled() + + it "shows alert when a modifed buffer exists", -> + rootView.activeEditor().insertText("hi") + spyOn($native, "alert") + spyOn($native, "reload") + window.reload() + expect($native.reload).not.toHaveBeenCalled() + expect($native.alert).toHaveBeenCalled() + describe "requireStylesheet(path)", -> it "synchronously loads the stylesheet at the given path and installs a style tag for it in the head", -> $('head style[path*="atom.css"]').remove() diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 396241bab..525b9a5eb 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -8,16 +8,19 @@ UndoManager = require 'undo-manager' module.exports = class Buffer @idCounter = 1 + modified: null lines: null path: null @deserialize: (state, project) -> if state.path - project.open(state.path) + buffer = project.open(state.path) else buffer = project.bufferWithId(state.id) ? project.open() buffer.setText(state.text) - buffer + buffer.modified = state.modified + + buffer constructor: (path) -> @id = @constructor.idCounter++ @@ -28,12 +31,13 @@ class Buffer else @setText('') @undoManager = new UndoManager(this) + @modified = false serialize: -> if @getPath() { path: @path } else - { text: @getText(), id: @id } + { text: @getText(), id: @id , modified: @modified} getPath: -> @path @@ -139,6 +143,8 @@ class Buffer newTextLines[lastLineIndex] += suffix @lines[oldRange.start.row..oldRange.end.row] = newTextLines + @modified = true + @trigger 'change', { oldRange, newRange, oldText, newText } newRange @@ -155,15 +161,19 @@ class Buffer @undoManager.redo() save: -> - if not @getPath() then throw new Error("Tried to save buffer with no file path") + if not @getPath() then throw new Error("Can't save buffer with no file path") @trigger 'before-save' fs.write @getPath(), @getText() + @modified = false @trigger 'after-save' saveAs: (path) -> @setPath(path) @save() + isModified: -> + @modified + getMode: -> return @mode if @mode extension = if @getPath() then @getPath().split('/').pop().split('.').pop() else null diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 086fb9c24..2be66de5c 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -7,6 +7,8 @@ Range = require 'range' EditSession = require 'edit-session' CursorView = require 'cursor-view' SelectionView = require 'selection-view' +Native = require 'native' +fs = require 'fs' $ = require 'jquery' _ = require 'underscore' @@ -604,12 +606,14 @@ class Editor extends View save: -> if not @buffer.getPath() - path = $native.saveDialog() - return if not path + path = Native.saveDialog() + return false if not path @buffer.saveAs(path) else @buffer.save() + true + clipScreenPosition: (screenPosition, options={}) -> @renderer.clipScreenPosition(screenPosition, options) @@ -778,7 +782,19 @@ class Editor extends View close: -> return if @mini - @removeActiveEditSession() + if @buffer.isModified() + filename = if @buffer.getPath() then fs.base(@buffer.getPath()) else "untitled buffer" + message = "'#{filename}' has changes, do you want to save them?" + detailedMessage = "Your changes will be lost if you don't save them" + buttons = [ + ["Save", => @save() and @removeActiveEditSession()] + ["Cancel", =>] + ["Don't save", => @removeActiveEditSession()] + ] + + Native.alert message, detailedMessage, buttons + else + @removeActiveEditSession() unsubscribeFromBuffer: -> @buffer.off ".editor#{@id}" diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 7ab1e92c9..ee438fa59 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -123,6 +123,14 @@ class RootView extends View editors: -> @panes.find('.editor').map -> $(this).view() + modifiedBuffers: -> + modifiedBuffers = [] + for editor in @editors() + for session in editor.editSessions + modifiedBuffers.push session.buffer if session.buffer.isModified() + + modifiedBuffers + activeEditor: -> if (editor = @panes.find('.editor.active')).length editor.view() diff --git a/src/app/window.coffee b/src/app/window.coffee index 5b6209d1d..545bd25b9 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -1,6 +1,7 @@ # This a weirdo file. We don't create a Window class, we just add stuff to # the DOM window. +Native = require 'native' fs = require 'fs' _ = require 'underscore' $ = require 'jquery' @@ -60,6 +61,19 @@ windowAdditions = return if $("head style[path='#{fullPath}']").length $('head').append "" + reload: -> + if rootView.modifiedBuffers().length > 0 + message = "There are unsaved buffers, reload anyway?" + detailedMessage = "You will lose all unsaved changes if you reload" + buttons = [ + ["Reload", -> Native.reload()] + ["Cancel", ->] + ] + + Native.alert(message, detailedMessage, buttons) + else + Native.reload() + showConsole: -> $native.showDevTools() diff --git a/src/stdlib/native.coffee b/src/stdlib/native.coffee index 163af0d17..e468e4026 100644 --- a/src/stdlib/native.coffee +++ b/src/stdlib/native.coffee @@ -2,4 +2,8 @@ module.exports = class Native @alert: (args...) -> $native.alert(args...) + @saveDialog: (args...) -> $native.saveDialog(args...) + + @reload: -> $native.reload() + @moveToTrash: (args...) -> $native.moveToTrash(args...)