diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c48073b2..e573e2d5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,3 +20,4 @@ * New packages go in `src/packages/` * Add 3rd-party packages by submoduling in `vendor/packages/` * Commit messages are in the present tense + * Files end with a newline diff --git a/README.md b/README.md index 04459faa3..00db8d6b6 100644 --- a/README.md +++ b/README.md @@ -27,30 +27,39 @@ A basic ~/.atom directory is installed when you run `rake install`. Take a look Atom doesn't have much in the way of menus yet. Use these keyboard shortcuts to explore features. -`cmd-o` : open file/directory +`meta-o` : open file/directory -`cmd-n` : new window +`meta-n` : new window -`cmd-t` : open fuzzy file finder +`meta-t` : open fuzzy file finder -`cmd-:` : open command prompt +`meta-:` : open command prompt -`cmd-f` : open command prompt with / +`meta-f` : open command prompt with / -`cmd-g` : repeat the last search +`meta-g` : repeat the last search -`cmd-r` : reload the current window +`meta-r` : reload the current window -`cmd-alt-ctrl-s` : run specs +`meta-alt-ctrl-s` : run specs -`cmd-alt-arrows` : split screen in direction of arrow +`meta-alt-arrows` : split screen in direction of arrow -`cmd-alt-w` : toggle word wrap +`meta-alt-w` : toggle word wrap -`cmd-alt-f` : fold selected lines +`meta-alt-f` : fold selected lines + +`meta-l` : go to line Most default OS X keybindings also work. +## TreeView Keyboard shortcuts +With the treeview focused: + +`a` : Add a new file or directory. Directories end with '/'. + +`m` : Rename a file or directory + ## Init Script Atom will require `~/.atom/user.coffee` whenever a window is opened or reloaded if it is present in your diff --git a/Rakefile b/Rakefile index e0dd5abd1..9e4b28940 100644 --- a/Rakefile +++ b/Rakefile @@ -25,7 +25,7 @@ task "bootstrap" do end desc "Creates symlink from `application_path() to /Applications/Atom and creates `atom` cli app" -task :install => :build do +task :install => [:clean, :build] do path = application_path() exit 1 if not path @@ -109,6 +109,7 @@ task :clean do output = `xcodebuild clean` `rm -rf #{application_path()}` `rm -rf #{BUILD_DIR}` + `rm -rf /tmp/atom-compiled-scripts` end desc "Run Atom" diff --git a/benchmark/benchmark-helper.coffee b/benchmark/benchmark-helper.coffee index 49b82d2ed..dff4cee35 100644 --- a/benchmark/benchmark-helper.coffee +++ b/benchmark/benchmark-helper.coffee @@ -10,7 +10,7 @@ require 'window' requireStylesheet "jasmine.css" # Load TextMate bundles, which specs rely on (but not other packages) -atom.loadPackages(atom.getAvailableTextMateBundles()) +atom.loadTextMatePackages() beforeEach -> # reset config after each benchmark; don't load or save from/to `config.json` diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 1f784071b..41e437f9b 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -17,9 +17,15 @@ describe "editor.", -> editor = rootView.getActiveEditor() afterEach -> - $(window).off 'beforeunload' - window.shutdown() - atom.setRootViewStateForPath(rootView.project.getPath(), null) + if editor.pendingDisplayUpdate + waitsFor "editor to finish rendering", (done) -> + editor.on 'editor:display-updated', done + + runs -> + projectPath = rootView.project.getPath() + $(window).off 'beforeunload' + window.shutdown() + atom.setRootViewStateForPath(projectPath, null) describe "keymap.", -> event = null diff --git a/native/atom_cef_client.cpp b/native/atom_cef_client.cpp index 27badf291..34e679124 100644 --- a/native/atom_cef_client.cpp +++ b/native/atom_cef_client.cpp @@ -17,8 +17,9 @@ AtomCefClient::AtomCefClient(){ } -AtomCefClient::AtomCefClient(bool handlePasteboardCommands) { +AtomCefClient::AtomCefClient(bool handlePasteboardCommands, bool ignoreTitleChanges) { m_HandlePasteboardCommands = handlePasteboardCommands; + m_IgnoreTitleChanges = ignoreTitleChanges; } AtomCefClient::~AtomCefClient() { diff --git a/native/atom_cef_client.h b/native/atom_cef_client.h index 85d9a031b..16a80298b 100644 --- a/native/atom_cef_client.h +++ b/native/atom_cef_client.h @@ -16,7 +16,7 @@ class AtomCefClient : public CefClient, public CefRequestHandler { public: AtomCefClient(); - AtomCefClient(bool handlePasteboardCommands); + AtomCefClient(bool handlePasteboardCommands, bool ignoreTitleChanges); virtual ~AtomCefClient(); CefRefPtr GetBrowser() { return m_Browser; } @@ -103,6 +103,7 @@ class AtomCefClient : public CefClient, protected: CefRefPtr m_Browser; bool m_HandlePasteboardCommands = false; + bool m_IgnoreTitleChanges = false; void FocusNextWindow(); void FocusPreviousWindow(); diff --git a/native/atom_cef_client_mac.mm b/native/atom_cef_client_mac.mm index d2f250ec0..fdb398732 100644 --- a/native/atom_cef_client_mac.mm +++ b/native/atom_cef_client_mac.mm @@ -99,6 +99,8 @@ void AtomCefClient::Confirm(int replyId, void AtomCefClient::OnTitleChange(CefRefPtr browser, const CefString& title) { + if (m_IgnoreTitleChanges) return; + NSWindow *window = [browser->GetHost()->GetWindowHandle() window]; [window setTitle:[NSString stringWithUTF8String:title.ToString().c_str()]]; } diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index 4a26e9601..498070981 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -146,7 +146,7 @@ [_splitView addSubview:_devToolsView]; [_splitView adjustSubviews]; - _cefDevToolsClient = new AtomCefClient(true); + _cefDevToolsClient = new AtomCefClient(true, true); std::string devtools_url = _cefClient->GetBrowser()->GetHost()->GetDevToolsURL(true); [self addBrowserToView:_devToolsView url:devtools_url.c_str() cefHandler:_cefDevToolsClient]; } diff --git a/native/mac/atom.icns b/native/mac/atom.icns index 259c290e2..5ee78e5d4 100644 Binary files a/native/mac/atom.icns and b/native/mac/atom.icns differ diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index 9ce5b4ee6..9ce163a30 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -485,6 +485,11 @@ namespace v8_extensions { }; } + CefRefPtr currentWorkingDirectory = options->GetValue("cwd"); + if (!currentWorkingDirectory->IsUndefined() && !currentWorkingDirectory->IsNull()) { + [task setCurrentDirectoryPath:stringFromCefV8Value(currentWorkingDirectory)]; + } + [task launch]; return true; diff --git a/spec/app/atom-package-spec.coffee b/spec/app/atom-package-spec.coffee new file mode 100644 index 000000000..f483f74ab --- /dev/null +++ b/spec/app/atom-package-spec.coffee @@ -0,0 +1,67 @@ +RootView = require 'root-view' +AtomPackage = require 'atom-package' +fs = require 'fs' + +describe "AtomPackage", -> + describe ".load()", -> + afterEach -> + rootView.deactivate() + + describe "when the package metadata includes activation events", -> + [packageMainModule, pack] = [] + + beforeEach -> + new RootView(fixturesProject.getPath()) + pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-activation-events')) + packageMainModule = require 'fixtures/packages/package-with-activation-events/main' + spyOn(packageMainModule, 'activate').andCallThrough() + pack.load() + + it "defers activating the package until an activation event bubbles to the root view", -> + expect(packageMainModule.activate).not.toHaveBeenCalled() + rootView.trigger 'activation-event' + expect(packageMainModule.activate).toHaveBeenCalled() + + it "triggers the activation event on all handlers registered during activation", -> + rootView.open('sample.js') + editor = rootView.getActiveEditor() + eventHandler = jasmine.createSpy("activation-event") + editor.command 'activation-event', eventHandler + editor.trigger 'activation-event' + expect(packageMainModule.activate.callCount).toBe 1 + expect(packageMainModule.activationEventCallCount).toBe 1 + expect(eventHandler.callCount).toBe 1 + editor.trigger 'activation-event' + expect(packageMainModule.activationEventCallCount).toBe 2 + expect(eventHandler.callCount).toBe 2 + expect(packageMainModule.activate.callCount).toBe 1 + + describe "when the package does not specify a main module", -> + describe "when the package has an index.coffee", -> + it "uses index.coffee as the main module", -> + new RootView(fixturesProject.getPath()) + pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-module')) + packageMainModule = require 'fixtures/packages/package-with-module' + spyOn(packageMainModule, 'activate').andCallThrough() + + expect(packageMainModule.activate).not.toHaveBeenCalled() + pack.load() + expect(packageMainModule.activate).toHaveBeenCalled() + + describe "when the package doesn't have an index.coffee", -> + it "does not throw an exception or log an error", -> + spyOn(console, "error") + spyOn(console, "warn") + new RootView(fixturesProject.getPath()) + pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-keymaps-manifest')) + + expect(-> pack.load()).not.toThrow() + expect(console.error).not.toHaveBeenCalled() + expect(console.warn).not.toHaveBeenCalled() + + describe "when a package is activated", -> + it "loads config defaults based on the `configDefaults` key", -> + expect(config.get('package-with-module.numbers.one')).toBeUndefined() + atom.loadPackage("package-with-module") + expect(config.get('package-with-module.numbers.one')).toBe 1 + expect(config.get('package-with-module.numbers.two')).toBe 2 diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index 73474a2a1..3638f0d61 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -2,11 +2,16 @@ RootView = require 'root-view' {$$} = require 'space-pen' describe "the `atom` global", -> + beforeEach -> + new RootView + + afterEach -> + rootView.deactivate() + describe ".loadPackage(name)", -> [extension, stylesheetPath] = [] beforeEach -> - rootView = new RootView extension = require "package-with-module" stylesheetPath = require.resolve("fixtures/packages/package-with-module/stylesheets/styles.css") @@ -14,9 +19,9 @@ describe "the `atom` global", -> removeStylesheet(stylesheetPath) it "requires and activates the package's main module if it exists", -> - spyOn(rootView, 'activatePackage').andCallThrough() + spyOn(atom, 'activateAtomPackage').andCallThrough() atom.loadPackage("package-with-module") - expect(rootView.activatePackage).toHaveBeenCalled() + expect(atom.activateAtomPackage).toHaveBeenCalled() it "logs warning instead of throwing an exception if a package fails to load", -> config.set("core.disabledPackages", []) @@ -70,6 +75,7 @@ describe "the `atom` global", -> syntax.on 'grammars-loaded', eventHandler disabledPackages = config.get("core.disabledPackages") disabledPackages.push('textmate-package.tmbundle') + disabledPackages.push('package-with-snippets') config.set "core.disabledPackages", disabledPackages atom.loadPackages() @@ -78,3 +84,61 @@ describe "the `atom` global", -> runs -> expect(Worker.prototype.terminate).toHaveBeenCalled() expect(Worker.prototype.terminate.calls.length).toBe 1 + + describe "package lifecycle", -> + [pack, packageModule] = [] + + beforeEach -> + pack = + name: "package" + packageMain: + activate: jasmine.createSpy("activate") + deactivate: -> + serialize: -> "it worked" + + packageModule = pack.packageMain + + describe ".activateAtomPackage(package)", -> + it "calls activate on the package", -> + atom.activateAtomPackage(pack) + expect(packageModule.activate).toHaveBeenCalledWith(undefined) + + it "calls activate on the package module with its previous state", -> + atom.activateAtomPackage(pack) + packageModule.activate.reset() + + serializedState = rootView.serialize() + rootView.deactivate() + RootView.deserialize(serializedState) + + atom.activateAtomPackage(pack) + expect(packageModule.activate).toHaveBeenCalledWith("it worked") + + describe ".deactivateAtomPackages()", -> + it "deactivates and removes the package module from the package module map", -> + atom.activateAtomPackage(pack) + spyOn(packageModule, "deactivate").andCallThrough() + atom.deactivateAtomPackages() + expect(packageModule.deactivate).toHaveBeenCalled() + expect(rootView.packages.length).toBe 0 + + describe ".serializeAtomPackages()", -> + it "absorbs exceptions that are thrown by the package module's serialize methods", -> + spyOn(console, 'error') + + atom.activateAtomPackage + name: "bad-egg" + packageMain: + activate: -> + serialize: -> throw new Error("I'm broken") + + atom.activateAtomPackage + name: "good-egg" + packageMain: + activate: -> + serialize: -> "I still get called" + + packageStates = atom.serializeAtomPackages() + expect(packageStates['good-egg']).toBe "I still get called" + expect(packageStates['bad-egg']).toBeUndefined() + expect(console.error).toHaveBeenCalled() diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 2a1eaa30c..86cd15dbc 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -666,95 +666,283 @@ describe 'Buffer', -> expect(buffer.positionForCharacterIndex(61)).toEqual [2, 0] expect(buffer.positionForCharacterIndex(408)).toEqual [12, 2] - describe "anchors", -> - [anchor, destroyHandler] = [] + describe "markers", -> + describe "marker creation", -> + it "allows markers to be created with ranges and positions", -> + marker1 = buffer.markRange([[4, 20], [4, 23]]) + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + expect(buffer.getMarkerPosition(marker1)).toEqual [4, 23] + expect(buffer.getMarkerTailPosition(marker1)).toEqual [4, 20] - beforeEach -> - destroyHandler = jasmine.createSpy("destroyHandler") - anchor = buffer.addAnchorAtPosition([4, 25]) - anchor.on 'destroyed', destroyHandler + marker2 = buffer.markPosition([4, 20]) + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 20], [4, 20]] + expect(buffer.getMarkerPosition(marker2)).toEqual [4, 20] + expect(buffer.getMarkerTailPosition(marker2)).toEqual [4, 20] - describe "when anchor.ignoreChangesStartingOnAnchor is true", -> + it "allows markers to be created in a reversed orientation", -> + marker = buffer.markRange([[4, 20], [4, 23]], reverse: true) + expect(buffer.isMarkerReversed(marker)).toBeTruthy() + expect(buffer.getMarkerRange(marker)).toEqual [[4, 20], [4, 23]] + expect(buffer.getMarkerHeadPosition(marker)).toEqual [4, 20] + expect(buffer.getMarkerTailPosition(marker)).toEqual [4, 23] + + describe "marker manipulation", -> + marker = null beforeEach -> - anchor.ignoreChangesStartingOnAnchor = true + marker = buffer.markRange([[4, 20], [4, 23]]) - describe "when the change ends before the anchor position", -> - it "moves the anchor", -> - buffer.change([[4, 23], [4, 24]], "...") - expect(anchor.getBufferPosition()).toEqual [4, 27] - expect(destroyHandler).not.toHaveBeenCalled() + it "allows a marker's head and tail positions to be changed", -> + buffer.setMarkerHeadPosition(marker, [5, 3]) + expect(buffer.getMarkerRange(marker)).toEqual [[4, 20], [5, 3]] - describe "when the change ends on the anchor position", -> - it "moves the anchor", -> - buffer.change([[4, 24], [4, 25]], "...") - expect(anchor.getBufferPosition()).toEqual [4, 27] - expect(destroyHandler).not.toHaveBeenCalled() + buffer.setMarkerTailPosition(marker, [6, 3]) + expect(buffer.getMarkerRange(marker)).toEqual [[5, 3], [6, 3]] + expect(buffer.isMarkerReversed(marker)).toBeTruthy() - describe "when the change begins on the anchor position", -> - it "doesn't move the anchor", -> - buffer.change([[4, 25], [4, 26]], ".....") - expect(anchor.getBufferPosition()).toEqual [4, 25] - expect(destroyHandler).not.toHaveBeenCalled() + it "clips head and tail positions to ensure they are in bounds", -> + buffer.setMarkerHeadPosition(marker, [-100, -5]) + expect(buffer.getMarkerRange(marker)).toEqual([[0, 0], [4, 20]]) + buffer.setMarkerTailPosition(marker, [Infinity, Infinity]) + expect(buffer.getMarkerRange(marker)).toEqual([[0, 0], [12, 2]]) - describe "when the change begins after the anchor position", -> - it "doesn't move the anchor", -> - buffer.change([[4, 26], [4, 27]], ".....") - expect(anchor.getBufferPosition()).toEqual [4, 25] - expect(destroyHandler).not.toHaveBeenCalled() + it "allows a marker's tail to be placed and cleared", -> + buffer.clearMarkerTail(marker) + expect(buffer.getMarkerRange(marker)).toEqual [[4, 23], [4, 23]] + buffer.placeMarkerTail(marker) + buffer.setMarkerHeadPosition(marker, [2, 0]) + expect(buffer.getMarkerRange(marker)).toEqual [[2, 0], [4, 23]] + expect(buffer.isMarkerReversed(marker)).toBeTruthy() - describe "when the buffer changes and the oldRange is equalTo than the newRange (text is replaced)", -> - describe "when the anchor is contained by the oldRange", -> - it "destroys the anchor", -> - buffer.change([[4, 20], [4, 26]], ".......") - expect(destroyHandler).toHaveBeenCalled() + it "returns whether the position changed", -> + expect(buffer.setMarkerHeadPosition(marker, [5, 3])).toBeTruthy() + expect(buffer.setMarkerHeadPosition(marker, [5, 3])).toBeFalsy() - describe "when the anchor is not contained by the oldRange", -> - it "does not move the anchor", -> - buffer.change([[4, 20], [4, 21]], ".") - expect(anchor.getBufferPosition()).toEqual [4, 25] - expect(destroyHandler).not.toHaveBeenCalled() + expect(buffer.setMarkerTailPosition(marker, [6, 3])).toBeTruthy() + expect(buffer.setMarkerTailPosition(marker, [6, 3])).toBeFalsy() - describe "when the buffer changes and the oldRange is smaller than the newRange (text is inserted)", -> - describe "when the buffer changes and the oldRange starts and ends before the anchor ", -> - it "updates the anchor position", -> - buffer.change([[4, 24], [4, 24]], "..") - expect(anchor.getBufferPosition()).toEqual [4, 27] - expect(destroyHandler).not.toHaveBeenCalled() + describe ".observeMarker(marker, callback)", -> + [observeHandler, marker, subscription] = [] - describe "when the buffer changes and the oldRange contains before the anchor ", -> - it "destroys the anchor", -> - buffer.change([[4, 24], [4, 26]], ".....") - expect(destroyHandler).toHaveBeenCalled() + beforeEach -> + observeHandler = jasmine.createSpy("observeHandler") + marker = buffer.markRange([[4, 20], [4, 23]]) + subscription = buffer.observeMarker(marker, observeHandler) - describe "when the buffer changes and the oldRange stars after the anchor", -> - it "does not move the anchor", -> - buffer.change([[4, 26], [4, 26]], "....") - expect(anchor.getBufferPosition()).toEqual [4, 25] - expect(destroyHandler).not.toHaveBeenCalled() + it "calls the callback when the marker's head position changes", -> + buffer.setMarkerHeadPosition(marker, [6, 2]) + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadPosition: [4, 23] + newHeadPosition: [6, 2] + oldTailPosition: [4, 20] + newTailPosition: [4, 20] + bufferChanged: false + } + observeHandler.reset() - describe "when the buffer changes and the oldRange is larger than the newRange (text is deleted)", -> - describe "when the buffer changes and the oldRange starts and ends before the anchor ", -> - it "updates the anchor position", -> - buffer.change([[4, 20], [4, 21]], "") - expect(anchor.getBufferPosition()).toEqual [4, 24] - expect(destroyHandler).not.toHaveBeenCalled() + buffer.insert([6, 0], '...') + expect(observeHandler.argsForCall[0][0]).toEqual { + oldTailPosition: [4, 20] + newTailPosition: [4, 20] + oldHeadPosition: [6, 2] + newHeadPosition: [6, 5] + bufferChanged: true + } - describe "when the buffer changes and the oldRange contains before the anchor ", -> - it "destroys the anchor", -> - buffer.change([[4, 24], [4, 26]], ".") - expect(destroyHandler).toHaveBeenCalled() + it "calls the given callback when the marker's tail position changes", -> + buffer.setMarkerTailPosition(marker, [6, 2]) + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadPosition: [4, 23] + newHeadPosition: [4, 23] + oldTailPosition: [4, 20] + newTailPosition: [6, 2] + bufferChanged: false + } + observeHandler.reset() - describe "when the oldRange stars after the anchor", -> - it "does not move the anchor", -> - buffer.change([[4, 26], [4, 27]], "") - expect(anchor.getBufferPosition()).toEqual [4, 25] - expect(destroyHandler).not.toHaveBeenCalled() + buffer.insert([6, 0], '...') - describe "when a buffer change surrounds an anchor", -> - it "destroys the anchor", -> - buffer.delete([[3, 0], [5, 0]]) - expect(destroyHandler).toHaveBeenCalled() - expect(buffer.getAnchors().indexOf(anchor)).toBe -1 + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadPosition: [4, 23] + newHeadPosition: [4, 23] + oldTailPosition: [6, 2] + newTailPosition: [6, 5] + bufferChanged: true + } + + it "calls the callback when the selection's tail is cleared", -> + buffer.clearMarkerTail(marker) + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadPosition: [4, 23] + newHeadPosition: [4, 23] + oldTailPosition: [4, 20] + newTailPosition: [4, 23] + bufferChanged: false + } + + it "only calls the callback once when both the marker's head and tail positions change due to the same operation", -> + buffer.insert([4, 0], '...') + expect(observeHandler.callCount).toBe 1 + expect(observeHandler.argsForCall[0][0]).toEqual { + oldTailPosition: [4, 20] + newTailPosition: [4, 23] + oldHeadPosition: [4, 23] + newHeadPosition: [4, 26] + bufferChanged: true + } + observeHandler.reset() + + buffer.setMarkerRange(marker, [[0, 0], [1, 1]]) + expect(observeHandler.callCount).toBe 1 + expect(observeHandler.argsForCall[0][0]).toEqual { + oldTailPosition: [4, 23] + newTailPosition: [0, 0] + oldHeadPosition: [4, 26] + newHeadPosition: [1, 1] + bufferChanged: false + } + + it "allows the observation subscription to be cancelled", -> + subscription.cancel() + buffer.setMarkerHeadPosition(marker, [6, 2]) + expect(observeHandler).not.toHaveBeenCalled() + + describe "marker destruction", -> + marker = null + + beforeEach -> + marker = buffer.markRange([[4, 20], [4, 23]]) + + it "allows a marker to be destroyed", -> + buffer.destroyMarker(marker) + expect(buffer.getMarkerRange(marker)).toBeUndefined() + + it "does not restore invalidated markers that have been destroyed", -> + buffer.delete([[4, 15], [4, 25]]) + expect(buffer.getMarkerRange(marker)).toBeUndefined() + buffer.destroyMarker(marker) + buffer.undo() + expect(buffer.getMarkerRange(marker)).toBeUndefined() + + # even "stayValid" markers get destroyed properly + marker2 = buffer.markRange([[4, 20], [4, 23]], stayValid: true) + buffer.delete([[4, 15], [4, 25]]) + buffer.destroyMarker(marker2) + buffer.undo() + expect(buffer.getMarkerRange(marker2)).toBeUndefined() + + describe "marker updates due to buffer changes", -> + [marker1, marker2] = [] + + beforeEach -> + marker1 = buffer.markRange([[4, 20], [4, 23]]) + marker2 = buffer.markRange([[4, 20], [4, 23]], stayValid: true) + + describe "when the buffer changes due to a new operation", -> + describe "when the change precedes the marker range", -> + it "moves the marker", -> + buffer.insert([4, 5], '...') + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 23], [4, 26]] + buffer.delete([[4, 5], [4, 8]]) + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + buffer.insert([0, 0], '\nhi\n') + expect(buffer.getMarkerRange(marker1)).toEqual [[6, 20], [6, 23]] + + # undo works + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 23], [4, 26]] + + describe "when the change follows the marker range", -> + it "does not move the marker", -> + buffer.insert([6, 5], '...') + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + buffer.delete([[6, 5], [6, 8]]) + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + buffer.insert([10, 0], '\nhi\n') + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the change is an insertion at the start of the marker range", -> + it "does not move the start point, but does move the end point", -> + buffer.insert([4, 20], '...') + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 26]] + + describe "when the change is an insertion at the end of the marker range", -> + it "moves the end point", -> + buffer.insert([4, 23], '...') + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 26]] + + describe "when the change surrounds the marker range", -> + describe "when the marker was created with stayValid: false (the default)", -> + it "invalidates the marker", -> + buffer.delete([[4, 15], [4, 25]]) + expect(buffer.getMarkerRange(marker1)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the marker was created with stayValid: true", -> + it "does not invalidate the marker, but sets it to an empty range at the end of the change", -> + buffer.change([[4, 15], [4, 25]], "...") + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 18], [4, 18]] + buffer.undo() + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 20], [4, 23]] + + describe "when the change straddles the start of the marker range", -> + describe "when the marker was created with stayValid: false (the default)", -> + it "invalidates the marker", -> + buffer.delete([[4, 15], [4, 22]]) + expect(buffer.getMarkerRange(marker1)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the marker was created with stayValid: true", -> + it "moves the start of the marker range to the end of the change", -> + buffer.delete([[4, 15], [4, 22]]) + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 15], [4, 16]] + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the change straddles the end of the marker range", -> + describe "when the marker was created with stayValid: false (the default)", -> + it "invalidates the marker", -> + buffer.delete([[4, 22], [4, 25]]) + expect(buffer.getMarkerRange(marker1)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the marker was created with stayValid: true", -> + it "moves the end of the marker range to the start of the change", -> + buffer.delete([[4, 22], [4, 25]]) + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 20], [4, 22]] + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the buffer changes due to the undo or redo of a previous operation", -> + it "restores invalidated markers when undoing/redoing in the other direction", -> + buffer.change([[4, 21], [4, 24]], "foo") + expect(buffer.getMarkerRange(marker1)).toBeUndefined() + marker3 = buffer.markRange([[4, 20], [4, 23]]) + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + expect(buffer.getMarkerRange(marker3)).toBeUndefined() + marker4 = buffer.markRange([[4, 20], [4, 23]]) + buffer.redo() + expect(buffer.getMarkerRange(marker3)).toEqual [[4, 20], [4, 23]] + expect(buffer.getMarkerRange(marker4)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker4)).toEqual [[4, 20], [4, 23]] + + describe ".markersForPosition(position)", -> + it "returns all markers that intersect the given position", -> + m1 = buffer.markRange([[3, 4], [3, 10]]) + m2 = buffer.markRange([[3, 4], [3, 5]]) + m3 = buffer.markPosition([3, 5]) + expect(_.difference(buffer.markersForPosition([3, 5]), [m1, m2, m3]).length).toBe 0 + expect(_.difference(buffer.markersForPosition([3, 4]), [m1, m2]).length).toBe 0 + expect(_.difference(buffer.markersForPosition([3, 10]), [m1]).length).toBe 0 describe ".usesSoftTabs()", -> it "returns true if the first indented line begins with tabs", -> @@ -800,7 +988,6 @@ describe 'Buffer', -> expect(contentsModifiedHandler).toHaveBeenCalledWith(differsFromDisk:true) bufferToDelete.destroy() - describe "when the buffer text has been changed", -> it "triggers the contents-modified event 'stoppedChangingDelay' ms after the last buffer change", -> delay = buffer.stoppedChangingDelay diff --git a/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee index 7444a3859..7b75bdc9f 100644 --- a/spec/app/display-buffer-spec.coffee +++ b/spec/app/display-buffer-spec.coffee @@ -118,6 +118,12 @@ describe "DisplayBuffer", -> expect(displayBuffer.screenPositionForBufferPosition([4, 5])).toEqual([5, 5]) expect(displayBuffer.bufferPositionForScreenPosition([5, 5])).toEqual([4, 5]) + # clip screen position inputs before translating + expect(displayBuffer.bufferPositionForScreenPosition([-5, -5])).toEqual([0, 0]) + expect(displayBuffer.bufferPositionForScreenPosition([Infinity, Infinity])).toEqual([12, 2]) + expect(displayBuffer.bufferPositionForScreenPosition([3, -5])).toEqual([3, 0]) + expect(displayBuffer.bufferPositionForScreenPosition([3, Infinity])).toEqual([3, 50]) + describe ".setSoftWrapColumn(length)", -> it "changes the length at which lines are wrapped and emits a change event for all screen lines", -> displayBuffer.setSoftWrapColumn(40) @@ -477,6 +483,10 @@ describe "DisplayBuffer", -> expect(displayBuffer.bufferPositionForScreenPosition([5, 0])).toEqual [8, 0] expect(displayBuffer.bufferPositionForScreenPosition([9, 2])).toEqual [12, 2] + # clip screen positions before translating + expect(displayBuffer.bufferPositionForScreenPosition([-5, -5])).toEqual([0, 0]) + expect(displayBuffer.bufferPositionForScreenPosition([Infinity, Infinity])).toEqual([200, 0]) + describe ".destroyFoldsContainingBufferRow(row)", -> it "destroys all folds containing the given row", -> displayBuffer.createFold(2, 4) @@ -579,3 +589,154 @@ describe "DisplayBuffer", -> describe ".maxLineLength()", -> it "returns the length of the longest screen line", -> expect(displayBuffer.maxLineLength()).toBe 65 + + describe "markers", -> + beforeEach -> + displayBuffer.foldBufferRow(4) + + describe "marker creation and manipulation", -> + it "allows markers to be created in terms of both screen and buffer coordinates", -> + marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]]) + marker2 = displayBuffer.markBufferRange([[8, 4], [8, 10]]) + expect(displayBuffer.getMarkerBufferRange(marker1)).toEqual [[8, 4], [8, 10]] + expect(displayBuffer.getMarkerScreenRange(marker2)).toEqual [[5, 4], [5, 10]] + + it "allows marker head and tail positions to be manipulated in both screen and buffer coordinates", -> + marker = displayBuffer.markScreenRange([[5, 4], [5, 10]]) + displayBuffer.setMarkerHeadScreenPosition(marker, [5, 4]) + displayBuffer.setMarkerTailBufferPosition(marker, [5, 4]) + expect(displayBuffer.isMarkerReversed(marker)).toBeFalsy() + expect(displayBuffer.getMarkerBufferRange(marker)).toEqual [[5, 4], [8, 4]] + + displayBuffer.setMarkerHeadBufferPosition(marker, [5, 4]) + displayBuffer.setMarkerTailScreenPosition(marker, [5, 4]) + expect(displayBuffer.isMarkerReversed(marker)).toBeTruthy() + expect(displayBuffer.getMarkerBufferRange(marker)).toEqual [[5, 4], [8, 4]] + + it "returns whether a position changed when it is assigned", -> + marker = displayBuffer.markScreenRange([[0, 0], [0, 0]]) + expect(displayBuffer.setMarkerHeadScreenPosition(marker, [5, 4])).toBeTruthy() + expect(displayBuffer.setMarkerHeadScreenPosition(marker, [5, 4])).toBeFalsy() + expect(displayBuffer.setMarkerHeadBufferPosition(marker, [1, 0])).toBeTruthy() + expect(displayBuffer.setMarkerHeadBufferPosition(marker, [1, 0])).toBeFalsy() + expect(displayBuffer.setMarkerTailScreenPosition(marker, [5, 4])).toBeTruthy() + expect(displayBuffer.setMarkerTailScreenPosition(marker, [5, 4])).toBeFalsy() + expect(displayBuffer.setMarkerTailBufferPosition(marker, [1, 0])).toBeTruthy() + expect(displayBuffer.setMarkerTailBufferPosition(marker, [1, 0])).toBeFalsy() + + describe ".observeMarker(marker, callback)", -> + [observeHandler, marker, subscription] = [] + + beforeEach -> + observeHandler = jasmine.createSpy("observeHandler") + marker = displayBuffer.markScreenRange([[5, 4], [5, 10]]) + subscription = displayBuffer.observeMarker(marker, observeHandler) + + it "calls the callback whenever the markers head's screen position changes in the buffer or on screen", -> + displayBuffer.setMarkerHeadScreenPosition(marker, [8, 20]) + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadScreenPosition: [5, 10] + oldHeadBufferPosition: [8, 10] + newHeadScreenPosition: [8, 20] + newHeadBufferPosition: [11, 20] + oldTailScreenPosition: [5, 4] + oldTailBufferPosition: [8, 4] + newTailScreenPosition: [5, 4] + newTailBufferPosition: [8, 4] + bufferChanged: false + } + observeHandler.reset() + + buffer.insert([11, 0], '...') + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadScreenPosition: [8, 20] + oldHeadBufferPosition: [11, 20] + newHeadScreenPosition: [8, 23] + newHeadBufferPosition: [11, 23] + oldTailScreenPosition: [5, 4] + oldTailBufferPosition: [8, 4] + newTailScreenPosition: [5, 4] + newTailBufferPosition: [8, 4] + bufferChanged: true + } + observeHandler.reset() + + displayBuffer.unfoldBufferRow(4) + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadScreenPosition: [8, 23] + oldHeadBufferPosition: [11, 23] + newHeadScreenPosition: [11, 23] + newHeadBufferPosition: [11, 23] + oldTailScreenPosition: [5, 4] + oldTailBufferPosition: [8, 4] + newTailScreenPosition: [8, 4] + newTailBufferPosition: [8, 4] + bufferChanged: false + } + observeHandler.reset() + + displayBuffer.foldBufferRow(4) + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadScreenPosition: [11, 23] + oldHeadBufferPosition: [11, 23] + newHeadScreenPosition: [8, 23] + newHeadBufferPosition: [11, 23] + oldTailScreenPosition: [8, 4] + oldTailBufferPosition: [8, 4] + newTailScreenPosition: [5, 4] + newTailBufferPosition: [8, 4] + bufferChanged: false + } + + it "calls the callback whenever the marker tail's position changes in the buffer or on screen", -> + displayBuffer.setMarkerTailScreenPosition(marker, [8, 20]) + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadScreenPosition: [5, 10] + oldHeadBufferPosition: [8, 10] + newHeadScreenPosition: [5, 10] + newHeadBufferPosition: [8, 10] + oldTailScreenPosition: [5, 4] + oldTailBufferPosition: [8, 4] + newTailScreenPosition: [8, 20] + newTailBufferPosition: [11, 20] + bufferChanged: false + } + observeHandler.reset() + + buffer.insert([11, 0], '...') + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadScreenPosition: [5, 10] + oldHeadBufferPosition: [8, 10] + newHeadScreenPosition: [5, 10] + newHeadBufferPosition: [8, 10] + oldTailScreenPosition: [8, 20] + oldTailBufferPosition: [11, 20] + newTailScreenPosition: [8, 23] + newTailBufferPosition: [11, 23] + bufferChanged: true + } + + it "does not call the callback for screen changes that don't change the position of the marker", -> + displayBuffer.createFold(10, 11) + expect(observeHandler).not.toHaveBeenCalled() + + it "allows observation subscriptions to be cancelled", -> + subscription.cancel() + displayBuffer.setMarkerHeadScreenPosition(marker, [8, 20]) + displayBuffer.unfoldBufferRow(4) + expect(observeHandler).not.toHaveBeenCalled() + + describe "marker destruction", -> + it "allows markers to be destroyed", -> + marker = displayBuffer.markScreenRange([[5, 4], [5, 10]]) + displayBuffer.destroyMarker(marker) + expect(displayBuffer.getMarkerBufferRange(marker)).toBeUndefined() + + + diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index c77ddd15c..f4ec19ac4 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -76,12 +76,12 @@ describe "EditSession", -> describe "when the cursor is on the first line", -> it "moves the cursor to the beginning of the line, but retains the goal column", -> - editSession.setCursorScreenPosition(row: 0, column: 4) + editSession.setCursorScreenPosition([0, 4]) editSession.moveCursorUp() - expect(editSession.getCursorScreenPosition()).toEqual(row: 0, column: 0) + expect(editSession.getCursorScreenPosition()).toEqual([0, 0]) editSession.moveCursorDown() - expect(editSession.getCursorScreenPosition()).toEqual(row: 1, column: 4) + expect(editSession.getCursorScreenPosition()).toEqual([1, 4]) it "merges cursors when they overlap", -> editSession.addCursorAtScreenPosition([1, 0]) @@ -185,9 +185,9 @@ describe "EditSession", -> describe "when the cursor is on the last column of a line", -> describe "when there is a subsequent line", -> it "wraps to the beginning of the next line", -> - editSession.setCursorScreenPosition(row: 0, column: buffer.lineForRow(0).length) + editSession.setCursorScreenPosition([0, buffer.lineForRow(0).length]) editSession.moveCursorRight() - expect(editSession.getCursorScreenPosition()).toEqual(row: 1, column: 0) + expect(editSession.getCursorScreenPosition()).toEqual [1, 0] describe "when the cursor is on the last line", -> it "remains in the same position", -> @@ -322,7 +322,6 @@ describe "EditSession", -> editSession.moveCursorToEndOfWord() expect(editSession.getCursorBufferPosition()).toEqual [11, 8] - describe ".getCurrentParagraphBufferRange()", -> it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", -> buffer.setText """ @@ -353,6 +352,31 @@ describe "EditSession", -> editSession.setCursorBufferPosition([3, 1]) expect(editSession.getCurrentParagraphBufferRange()).toBeUndefined() + describe "cursor-moved events", -> + cursorMovedHandler = null + + beforeEach -> + editSession.foldBufferRow(4) + editSession.setSelectedBufferRange([[8, 1], [9, 0]]) + cursorMovedHandler = jasmine.createSpy("cursorMovedHandler") + editSession.on 'cursor-moved', cursorMovedHandler + + describe "when the position of the cursor changes", -> + it "emits a cursor-moved event", -> + buffer.insert([9, 0], '...') + expect(cursorMovedHandler).toHaveBeenCalledWith( + oldBufferPosition: [9, 0] + oldScreenPosition: [6, 0] + newBufferPosition: [9, 3] + newScreenPosition: [6, 3] + bufferChanged: true + ) + + describe "when the position of the associated selection's tail changes, but not the cursor's position", -> + it "does not emit a cursor-moved event", -> + buffer.insert([8, 0], '...') + expect(cursorMovedHandler).not.toHaveBeenCalled() + describe "selection", -> selection = null @@ -638,6 +662,19 @@ describe "EditSession", -> editSession.setSelectedBufferRanges([[[2, 2], [3, 3]]], preserveFolds: true) expect(editSession.lineForScreenRow(1).fold).toBeDefined() + describe ".selectMarker(marker)", -> + describe "when the marker exists", -> + it "selects the marker's range and returns true", -> + marker = editSession.markBufferRange([[0, 1], [3, 3]]) + expect(editSession.selectMarker(marker)).toBeTruthy() + expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [3, 3]] + + describe "when the marker does not exist", -> + it "does not select the marker's range and returns false", -> + rangeBefore = editSession.getSelectedBufferRange() + expect(editSession.selectMarker('bogus')).toBeFalsy() + expect(editSession.getSelectedBufferRange()).toEqual rangeBefore + describe "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] @@ -1005,6 +1042,7 @@ describe "EditSession", -> expect(line).toBe " var ort = function(items) {" expect(editSession.getCursorScreenPosition()).toEqual {row: 1, column: 6} expect(changeScreenRangeHandler).toHaveBeenCalled() + expect(editSession.getCursor().isVisible()).toBeTruthy() describe "when the cursor is at the beginning of a line", -> it "joins it with the line above", -> @@ -1635,7 +1673,7 @@ describe "EditSession", -> expect(buffer.lineForRow(7)).toBe " }" it "preserves selection emptiness", -> - editSession.setSelectedBufferRange([[4, 0], [4, 0]]) + editSession.setCursorBufferPosition([4, 0]) editSession.toggleLineCommentsInSelection() expect(editSession.getSelection().isEmpty()).toBeTruthy() @@ -1647,7 +1685,7 @@ describe "EditSession", -> expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" it "uncomments when the line lacks the trailing whitespace in the comment regex", -> - editSession.setSelectedBufferRange([[10, 0], [10, 0]]) + editSession.setCursorBufferPosition([10, 0]) editSession.toggleLineCommentsInSelection() expect(buffer.lineForRow(10)).toBe "// " @@ -1660,7 +1698,7 @@ describe "EditSession", -> expect(editSession.getSelectedBufferRange()).toEqual [[10, 0], [10, 0]] it "uncomments when the line has leading whitespace", -> - editSession.setSelectedBufferRange([[10, 0], [10, 0]]) + editSession.setCursorBufferPosition([10, 0]) editSession.toggleLineCommentsInSelection() expect(buffer.lineForRow(10)).toBe "// " @@ -1763,18 +1801,18 @@ describe "EditSession", -> expect(cursor2.getScreenPosition()).toEqual [0, 8] expect(cursor3.getScreenPosition()).toEqual [1, 0] - it "does not destroy cursor or selection anchors when a change encompasses them", -> + it "does not destroy cursors or selections when a change encompasses them", -> cursor = editSession.getCursor() cursor.setBufferPosition [3, 3] editSession.buffer.delete([[3, 1], [3, 5]]) expect(cursor.getBufferPosition()).toEqual [3, 1] - expect(editSession.getAnchors().indexOf(cursor.anchor)).not.toBe -1 + expect(editSession.getCursors().indexOf(cursor)).not.toBe -1 selection = editSession.getLastSelection() selection.setBufferRange [[3, 5], [3, 10]] editSession.buffer.delete [[3, 3], [3, 8]] expect(selection.getBufferRange()).toEqual [[3, 3], [3, 5]] - expect(editSession.getAnchors().indexOf(selection.anchor)).not.toBe -1 + expect(editSession.getSelections().indexOf(selection)).not.toBe -1 it "merges cursors when the change causes them to overlap", -> editSession.setCursorScreenPosition([0, 0]) @@ -1798,34 +1836,6 @@ describe "EditSession", -> editSession.foldAll() expect(editSession.getCursorBufferPosition()).toEqual([5,5]) - describe "anchors", -> - [anchor, destroyHandler] = [] - - beforeEach -> - destroyHandler = jasmine.createSpy("destroyHandler") - anchor = editSession.addAnchorAtBufferPosition([4, 25]) - anchor.on 'destroyed', destroyHandler - - describe "when a buffer change precedes an anchor", -> - it "moves the anchor in accordance with the change", -> - editSession.setSelectedBufferRange([[3, 0], [4, 10]]) - editSession.delete() - expect(anchor.getBufferPosition()).toEqual [3, 15] - expect(destroyHandler).not.toHaveBeenCalled() - - describe "when a buffer change surrounds an anchor", -> - it "destroys the anchor", -> - editSession.setSelectedBufferRange([[3, 0], [5, 0]]) - editSession.delete() - expect(destroyHandler).toHaveBeenCalled() - expect(editSession.getAnchors().indexOf(anchor)).toBe -1 - - describe ".clipBufferPosition(bufferPosition)", -> - it "clips the given position to a valid position", -> - expect(editSession.clipBufferPosition([-1, -1])).toEqual [0,0] - expect(editSession.clipBufferPosition([Infinity, Infinity])).toEqual [12,2] - expect(editSession.clipBufferPosition([8, 57])).toEqual [8, 56] - describe ".deleteLine()", -> it "deletes the first line when the cursor is there", -> editSession.getCursor().moveToTop() @@ -1891,7 +1901,7 @@ describe "EditSession", -> expect(buffer.getLineCount()).toBe(1) expect(buffer.getText()).toBe('') - describe ".tranpose()", -> + describe ".transpose()", -> it "swaps two characters", -> editSession.buffer.setText("abc") editSession.setCursorScreenPosition([0, 1]) @@ -2046,3 +2056,9 @@ describe "EditSession", -> editSession.insertText("var i;\n}") editSession.autoDecreaseIndentForRow(1) expect(editSession.lineForBufferRow(1)).toBe "}" + + describe ".destroy()", -> + it "destroys all markers associated with the edit session", -> + expect(buffer.getMarkerCount()).toBeGreaterThan 0 + editSession.destroy() + expect(buffer.getMarkerCount()).toBe 0 diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 25620a25e..a9f020799 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -39,8 +39,9 @@ describe "Editor", -> rootView.remove() describe "construction", -> - it "throws an error if no editor session is given", -> + it "throws an error if no editor session is given unless deserializing", -> expect(-> new Editor).toThrow() + expect(-> new Editor(deserializing: true)).not.toThrow() describe ".copy()", -> it "builds a new editor with the same edit sessions, cursor position, and scroll position as the receiver", -> @@ -69,7 +70,7 @@ describe "Editor", -> newEditor.height(editor.height()) newEditor.width(editor.width()) - rootView.remove() + newEditor.attachToDom() expect(newEditor.scrollTop()).toBe editor.scrollTop() expect(newEditor.scrollView.scrollLeft()).toBe 44 @@ -1005,41 +1006,52 @@ describe "Editor", -> expect(editor.scrollTop()).toBe 0 expect(editor.scrollView.scrollTop()).toBe 0 - # does auto-scroll when the selection is cleared + # does autoscroll when the selection is cleared editor.moveCursorDown() expect(editor.scrollTop()).toBeGreaterThan(0) describe "selection autoscrolling and highlighting when setting selected buffer range", -> - it "only if autoscroll is true, centers the viewport on the selection if its vertical center is currently offscreen", -> + beforeEach -> setEditorHeightInLines(editor, 4) - editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) - expect(editor.scrollTop()).toBe 0 + describe "if autoscroll is true", -> + it "centers the viewport on the selection if its vertical center is currently offscreen", -> + editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) + expect(editor.scrollTop()).toBe 0 - editor.setSelectedBufferRange([[6, 0], [8, 0]], autoscroll: true) - expect(editor.scrollTop()).toBe 5 * editor.lineHeight + editor.setSelectedBufferRange([[6, 0], [8, 0]], autoscroll: true) + expect(editor.scrollTop()).toBe 5 * editor.lineHeight - editor.setSelectedBufferRange([[0, 0], [1, 0]]) # autoscroll is false, the default - expect(editor.scrollTop()).toBe 5 * editor.lineHeight + it "highlights the selection if autoscroll is true", -> + editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) + expect(editor.getSelectionView()).toHaveClass 'highlighted' + advanceClock(1000) + expect(editor.getSelectionView()).not.toHaveClass 'highlighted' - it "highlights the selection if autoscroll is true", -> - editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) - expect(editor.getSelectionView()).toHaveClass 'highlighted' - advanceClock(1000) - expect(editor.getSelectionView()).not.toHaveClass 'highlighted' + editor.setSelectedBufferRange([[3, 0], [5, 0]], autoscroll: true) + expect(editor.getSelectionView()).toHaveClass 'highlighted' - editor.setSelectedBufferRange([[3, 0], [5, 0]], autoscroll: true) - expect(editor.getSelectionView()).toHaveClass 'highlighted' + advanceClock(500) + spyOn(editor.getSelectionView(), 'removeClass').andCallThrough() + editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) + expect(editor.getSelectionView().removeClass).toHaveBeenCalledWith('highlighted') + expect(editor.getSelectionView()).toHaveClass 'highlighted' - advanceClock(500) - spyOn(editor.getSelectionView(), 'removeClass').andCallThrough() - editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) - expect(editor.getSelectionView().removeClass).toHaveBeenCalledWith('highlighted') - expect(editor.getSelectionView()).toHaveClass 'highlighted' + advanceClock(500) + expect(editor.getSelectionView()).toHaveClass 'highlighted' + describe "if autoscroll is false", -> + it "does not scroll to the selection or the cursor", -> + editor.scrollToBottom() + scrollTopBefore = editor.scrollTop() + editor.setSelectedBufferRange([[0, 0], [1, 0]], autoscroll: false) + expect(editor.scrollTop()).toBe scrollTopBefore - advanceClock(500) - expect(editor.getSelectionView()).toHaveClass 'highlighted' + describe "if autoscroll is not specified", -> + it "autoscrolls to the cursor as normal", -> + editor.scrollToBottom() + editor.setSelectedBufferRange([[0, 0], [1, 0]]) + expect(editor.scrollTop()).toBe 0 describe "cursor rendering", -> describe "when the cursor moves", -> @@ -1068,8 +1080,8 @@ describe "Editor", -> expect(editor.getSelection().isEmpty()).toBeTruthy() expect(cursorView).toBeVisible() - describe "auto-scrolling", -> - it "only auto-scrolls when the last cursor is moved", -> + describe "autoscrolling", -> + it "only autoscrolls when the last cursor is moved", -> editor.setCursorBufferPosition([11,0]) editor.addCursorAtBufferPosition([6,50]) [cursor1, cursor2] = editor.getCursors() @@ -1081,6 +1093,22 @@ describe "Editor", -> cursor2.setScreenPosition([11, 11]) expect(editor.scrollToPixelPosition).toHaveBeenCalled() + it "does not autoscroll if the 'autoscroll' option is false", -> + editor.setCursorBufferPosition([11,0]) + spyOn(editor, 'scrollToPixelPosition') + editor.setCursorScreenPosition([10, 10], autoscroll: false) + expect(editor.scrollToPixelPosition).not.toHaveBeenCalled() + + it "autoscrolls to cursor if autoscroll is true, even if the position does not change", -> + spyOn(editor, 'scrollToPixelPosition') + editor.setCursorScreenPosition([4, 10], autoscroll: false) + editor.setCursorScreenPosition([4, 10]) + expect(editor.scrollToPixelPosition).toHaveBeenCalled() + editor.scrollToPixelPosition.reset() + + editor.setCursorBufferPosition([4, 10]) + expect(editor.scrollToPixelPosition).toHaveBeenCalled() + describe "when the last cursor exceeds the upper or lower scroll margins", -> describe "when the editor is taller than twice the vertical scroll margin", -> it "sets the scrollTop so the cursor remains within the scroll margin", -> @@ -1756,12 +1784,6 @@ describe "Editor", -> expect(editor.gutter.find('.line-number:first').text()).toBe '2' expect(editor.gutter.find('.line-number:last').text()).toBe '11' - describe "when the insertion of lines causes the editor to scroll", -> - it "renders line numbers correctly", -> - oneHundredLines = [0..100].join("\n") - editor.insertText(oneHundredLines) - expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 6 + editor.lineOverdraw * 2 - describe "when wrapping is on", -> it "renders a • instead of line number for wrapped portions of lines", -> editor.setSoftWrapColumn(50) diff --git a/spec/app/language-mode-spec.coffee b/spec/app/language-mode-spec.coffee index 775baefa1..a68f778dc 100644 --- a/spec/app/language-mode-spec.coffee +++ b/spec/app/language-mode-spec.coffee @@ -19,192 +19,6 @@ describe "LanguageMode", -> expect(jsEditSession.languageMode.grammar.name).toBe "JavaScript" jsEditSession.destroy() - describe "bracket insertion", -> - beforeEach -> - editSession.buffer.setText("") - - describe "when more than one character is inserted", -> - it "does not insert a matching bracket", -> - editSession.insertText("woah(") - expect(editSession.buffer.getText()).toBe "woah(" - - describe "when there is a word character after the cursor", -> - it "does not insert a matching bracket", -> - editSession.buffer.setText("ab") - editSession.setCursorBufferPosition([0, 1]) - editSession.insertText("(") - - expect(editSession.buffer.getText()).toBe "a(b" - - describe "when there are multiple cursors", -> - it "inserts ) at each cursor", -> - editSession.buffer.setText("()\nab\n[]\n12") - editSession.setCursorBufferPosition([3, 1]) - editSession.addCursorAtBufferPosition([2, 1]) - editSession.addCursorAtBufferPosition([1, 1]) - editSession.addCursorAtBufferPosition([0, 1]) - editSession.insertText ')' - - expect(editSession.buffer.getText()).toBe "())\na)b\n[)]\n1)2" - - describe "when there is a non-word character after the cursor", -> - it "inserts a closing bracket after an opening bracket is inserted", -> - editSession.buffer.setText("}") - editSession.setCursorBufferPosition([0, 0]) - editSession.insertText '{' - expect(buffer.lineForRow(0)).toBe "{}}" - expect(editSession.getCursorBufferPosition()).toEqual([0,1]) - - describe "when the cursor is at the end of the line", -> - it "inserts a closing bracket after an opening bracket is inserted", -> - editSession.buffer.setText("") - editSession.insertText '{' - expect(buffer.lineForRow(0)).toBe "{}" - expect(editSession.getCursorBufferPosition()).toEqual([0,1]) - - editSession.buffer.setText("") - editSession.insertText '(' - expect(buffer.lineForRow(0)).toBe "()" - expect(editSession.getCursorBufferPosition()).toEqual([0,1]) - - editSession.buffer.setText("") - editSession.insertText '[' - expect(buffer.lineForRow(0)).toBe "[]" - expect(editSession.getCursorBufferPosition()).toEqual([0,1]) - - editSession.buffer.setText("") - editSession.insertText '"' - expect(buffer.lineForRow(0)).toBe '""' - expect(editSession.getCursorBufferPosition()).toEqual([0,1]) - - editSession.buffer.setText("") - editSession.insertText "'" - expect(buffer.lineForRow(0)).toBe "''" - expect(editSession.getCursorBufferPosition()).toEqual([0,1]) - - describe "when the cursor is on a closing bracket and a closing bracket is inserted", -> - describe "when the closing bracket was there previously", -> - it "inserts a closing bracket", -> - editSession.insertText '()x' - editSession.setCursorBufferPosition([0, 1]) - editSession.insertText ')' - expect(buffer.lineForRow(0)).toBe "())x" - expect(editSession.getCursorBufferPosition().column).toBe 2 - - describe "when the closing bracket was automatically inserted from inserting an opening bracket", -> - it "only moves cursor over the closing bracket one time", -> - editSession.insertText '(' - expect(buffer.lineForRow(0)).toBe "()" - editSession.setCursorBufferPosition([0, 1]) - editSession.insertText ')' - expect(buffer.lineForRow(0)).toBe "()" - expect(editSession.getCursorBufferPosition()).toEqual [0, 2] - - editSession.setCursorBufferPosition([0, 1]) - editSession.insertText ')' - expect(buffer.lineForRow(0)).toBe "())" - expect(editSession.getCursorBufferPosition()).toEqual [0, 2] - - it "moves cursor over the closing bracket after other text is inserted", -> - editSession.insertText '(' - editSession.insertText 'ok cool' - expect(buffer.lineForRow(0)).toBe "(ok cool)" - editSession.setCursorBufferPosition([0, 8]) - editSession.insertText ')' - expect(buffer.lineForRow(0)).toBe "(ok cool)" - expect(editSession.getCursorBufferPosition()).toEqual [0, 9] - - it "works with nested brackets", -> - editSession.insertText '(' - editSession.insertText '1' - editSession.insertText '(' - editSession.insertText '2' - expect(buffer.lineForRow(0)).toBe "(1(2))" - editSession.setCursorBufferPosition([0, 4]) - editSession.insertText ')' - expect(buffer.lineForRow(0)).toBe "(1(2))" - expect(editSession.getCursorBufferPosition()).toEqual [0, 5] - editSession.insertText ')' - expect(buffer.lineForRow(0)).toBe "(1(2))" - expect(editSession.getCursorBufferPosition()).toEqual [0, 6] - - it "works with mixed brackets", -> - editSession.insertText '(' - editSession.insertText '}' - expect(buffer.lineForRow(0)).toBe "(})" - editSession.insertText ')' - expect(buffer.lineForRow(0)).toBe "(})" - expect(editSession.getCursorBufferPosition()).toEqual [0, 3] - - it "closes brackets with the same begin/end character correctly", -> - editSession.insertText '"' - editSession.insertText 'ok' - expect(buffer.lineForRow(0)).toBe '"ok"' - expect(editSession.getCursorBufferPosition()).toEqual [0, 3] - editSession.insertText '"' - expect(buffer.lineForRow(0)).toBe '"ok"' - expect(editSession.getCursorBufferPosition()).toEqual [0, 4] - - describe "when there is text selected on a single line", -> - it "wraps the selection with brackets", -> - editSession.insertText 'text' - editSession.moveCursorToBottom() - editSession.selectToTop() - editSession.selectAll() - editSession.insertText '(' - expect('(text)').toBe buffer.getText() - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [0, 5]] - expect(editSession.getSelection().isReversed()).toBeTruthy() - - describe "when there is text selected on multiple lines", -> - it "wraps the selection with brackets", -> - editSession.insertText 'text\nabcd' - editSession.moveCursorToBottom() - editSession.selectToTop() - editSession.selectAll() - editSession.insertText '(' - expect('(text\nabcd)').toBe buffer.getText() - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [1, 4]] - expect(editSession.getSelection().isReversed()).toBeTruthy() - - describe "when inserting a quote", -> - describe "when a word character is before the cursor", -> - it "does not automatically insert closing quote", -> - editSession.buffer.setText("abc") - editSession.setCursorBufferPosition([0, 3]) - editSession.insertText '"' - expect(buffer.lineForRow(0)).toBe "abc\"" - - editSession.buffer.setText("abc") - editSession.setCursorBufferPosition([0, 3]) - editSession.insertText '\'' - expect(buffer.lineForRow(0)).toBe "abc\'" - - describe "when a non word character is before the cursor", -> - it "automatically insert closing quote", -> - editSession.buffer.setText("ab@") - editSession.setCursorBufferPosition([0, 3]) - editSession.insertText '"' - expect(buffer.lineForRow(0)).toBe "ab@\"\"" - expect(editSession.getCursorBufferPosition()).toEqual [0, 4] - - describe "when the cursor is on an empty line", -> - it "automatically insert closing quote", -> - editSession.buffer.setText("") - editSession.setCursorBufferPosition([0, 0]) - editSession.insertText '"' - expect(buffer.lineForRow(0)).toBe "\"\"" - expect(editSession.getCursorBufferPosition()).toEqual [0, 1] - - describe "bracket deletion", -> - it "deletes the end bracket when it directly proceeds a begin bracket that is being backspaced", -> - buffer.setText("") - editSession.setCursorBufferPosition([0, 0]) - editSession.insertText '{' - expect(buffer.lineForRow(0)).toBe "{}" - editSession.backspace() - expect(buffer.lineForRow(0)).toBe "" - describe "javascript", -> beforeEach -> editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index b046bef30..cc3516b9d 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -50,6 +50,14 @@ describe "RootView", -> expect(rootView.getEditors()[0].getText()).toEqual "" expect(rootView.getTitle()).toBe 'untitled' + describe ".deactivate()", -> + it "deactivates all packages", -> + pack = atom.loadPackage("package-with-module") + atom.activateAtomPackage(pack) + spyOn(pack.packageMain, "deactivate").andCallThrough() + rootView.deactivate() + expect(pack.packageMain.deactivate).toHaveBeenCalled() + describe "@deserialize()", -> viewState = null @@ -151,25 +159,6 @@ describe "RootView", -> expect(rootView.find('.pane').length).toBe 1 expect(rootView.find('.pane').children().length).toBe 0 - describe ".serialize()", -> - it "absorbs exceptions that are thrown by the package module's serialize methods", -> - spyOn(console, 'error') - - rootView.activatePackage("bad-egg", - activate: -> - serialize: -> throw new Error("I'm broken") - ) - - rootView.activatePackage("good-egg" - activate: -> - serialize: -> "I still get called" - ) - - data = rootView.serialize() - expect(data.packageStates['good-egg']).toBe "I still get called" - expect(data.packageStates['bad-egg']).toBeUndefined() - expect(console.error).toHaveBeenCalled() - describe "focus", -> describe "when there is an active editor", -> it "hands off focus to the active editor", -> @@ -418,54 +407,6 @@ describe "RootView", -> rootView.focusNextPane() expect(view1.focus).toHaveBeenCalled() - describe "packages", -> - packageModule = null - - beforeEach -> - packageModule = - configDefaults: foo: { bar: 2, baz: 3 } - activate: jasmine.createSpy("activate") - deactivate: -> - serialize: -> "it worked" - - describe ".activatePackage(name, packageModule)", -> - it "calls activate on the package module", -> - rootView.activatePackage('package', packageModule) - expect(packageModule.activate).toHaveBeenCalledWith(rootView, undefined) - - it "calls activate on the package module with its previous state", -> - rootView.activatePackage('package', packageModule) - packageModule.activate.reset() - - newRootView = RootView.deserialize(rootView.serialize()) - newRootView.activatePackage('package', packageModule) - expect(packageModule.activate).toHaveBeenCalledWith(newRootView, "it worked") - newRootView.remove() - - it "loads config defaults based on the `configDefaults` key", -> - expect(config.get('foo.bar')).toBeUndefined() - rootView.activatePackage('package', packageModule) - config.set("package.foo.bar", 1) - expect(config.get('package.foo.bar')).toBe 1 - expect(config.get('package.foo.baz')).toBe 3 - - describe ".deactivatePackage(packageName)", -> - it "deactivates and removes the package module from the package module map", -> - rootView.activatePackage('package', packageModule) - expect(rootView.packageModules['package']).toBeTruthy() - spyOn(packageModule, "deactivate").andCallThrough() - rootView.deactivatePackage('package') - expect(packageModule.deactivate).toHaveBeenCalled() - expect(rootView.packageModules['package']).toBeFalsy() - - it "is called when the rootView is deactivated to deactivate all packages", -> - rootView.activatePackage('package', packageModule) - spyOn(rootView, "deactivatePackage").andCallThrough() - spyOn(packageModule, "deactivate").andCallThrough() - rootView.deactivate() - expect(rootView.deactivatePackage).toHaveBeenCalled() - expect(packageModule.deactivate).toHaveBeenCalled() - describe "keymap wiring", -> commandHandler = null beforeEach -> diff --git a/spec/app/selection-spec.coffee b/spec/app/selection-spec.coffee index 6b59900dc..9a56e6118 100644 --- a/spec/app/selection-spec.coffee +++ b/spec/app/selection-spec.coffee @@ -40,7 +40,7 @@ describe "Selection", -> expect(buffer.lineForRow(0)).toBe "v;" expect(selection.isEmpty()).toBeTruthy() - describe "when the cursor precedes the anchor", -> + describe "when the cursor precedes the tail", -> it "deletes selected text and clears the selection", -> selection.cursor.setScreenPosition [0,13] selection.selectToScreenPosition [0,4] @@ -50,7 +50,7 @@ describe "Selection", -> expect(selection.isEmpty()).toBeTruthy() describe ".isReversed()", -> - it "returns true if the cursor precedes the anchor", -> + it "returns true if the cursor precedes the tail", -> selection.cursor.setScreenPosition([0, 20]) selection.selectToScreenPosition([0, 10]) expect(selection.isReversed()).toBeTruthy() @@ -58,7 +58,7 @@ describe "Selection", -> selection.selectToScreenPosition([0, 25]) expect(selection.isReversed()).toBeFalsy() - describe "when only the selection's anchor is moved (regression)", -> + describe "when only the selection's tail is moved (regression)", -> it "emits the 'screen-range-changed' event", -> selection.setBufferRange([[2, 0], [2, 10]], reverse: true) changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') @@ -68,10 +68,7 @@ describe "Selection", -> expect(changeScreenRangeHandler).toHaveBeenCalled() describe "when the selection is destroyed", -> - it "destroys its cursor and its anchor's cursor", -> + it "destroys its marker", -> selection.setBufferRange([[2, 0], [2, 10]]) - selection.destroy() - - expect(editSession.getAnchors().indexOf(selection.anchor)).toBe -1 - expect(editSession.getAnchors().indexOf(selection.cursor.anchor)).toBe -1 + expect(editSession.getMarkerBufferRange(selection.marker)).toBeUndefined() diff --git a/spec/app/text-mate-grammar-spec.coffee b/spec/app/text-mate-grammar-spec.coffee index 99613d9b9..c1a2fbc62 100644 --- a/spec/app/text-mate-grammar-spec.coffee +++ b/spec/app/text-mate-grammar-spec.coffee @@ -1,4 +1,5 @@ TextMateGrammar = require 'text-mate-grammar' +TextMatePackage = require 'text-mate-package' plist = require 'plist' fs = require 'fs' _ = require 'underscore' @@ -257,3 +258,14 @@ describe "TextMateGrammar", -> {tokens, ruleStack} = grammar.tokenizeLine("if(1){if(1){m()}}") expect(tokens[5]).toEqual value: "if", scopes: ["source.c", "meta.block.c", "keyword.control.c"] expect(tokens[10]).toEqual value: "m", scopes: ["source.c", "meta.block.c", "meta.block.c", "meta.function-call.c", "support.function.any-method.c"] + + describe "when the grammar is CSON", -> + it "loads the grammar and correctly parses a keyword", -> + spyOn(syntax, 'addGrammar') + pack = new TextMatePackage(fixturesProject.resolve("packages/package-with-a-cson-grammar.tmbundle")) + pack.load() + grammar = pack.grammars[0] + expect(grammar).toBeTruthy() + expect(grammar.scopeName).toBe "source.alot" + {tokens} = grammar.tokenizeLine("this is alot of code") + expect(tokens[1]).toEqual value: "alot", scopes: ["source.alot", "keyword.alot"] diff --git a/spec/app/undo-manager-spec.coffee b/spec/app/undo-manager-spec.coffee index 69d1c72df..6134a4da2 100644 --- a/spec/app/undo-manager-spec.coffee +++ b/spec/app/undo-manager-spec.coffee @@ -63,161 +63,145 @@ describe "UndoManager", -> undoManager.redo() expect(buffer.getText()).toContain 'qsport' - describe "transact([fn])", -> - describe "when called with a function", -> - it "causes changes performed within the function's dynamic extent to be undone simultaneously", -> - buffer.insert([0, 0], "foo") + describe "transaction methods", -> + describe "transact([fn])", -> + describe "when called with a function", -> + it "causes changes performed within the function's dynamic extent to be undone simultaneously", -> + buffer.insert([0, 0], "foo") - undoManager.transact -> undoManager.transact -> - buffer.insert([1, 2], "111") - buffer.insert([1, 9], "222") + undoManager.transact -> + buffer.insert([1, 2], "111") + buffer.insert([1, 9], "222") - expect(buffer.lineForRow(1)).toBe ' 111var 222sort = function(items) {' + expect(buffer.lineForRow(1)).toBe ' 111var 222sort = function(items) {' - undoManager.undo() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' - expect(buffer.lineForRow(0)).toContain 'foo' + undoManager.undo() + expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' + expect(buffer.lineForRow(0)).toContain 'foo' - undoManager.undo() + undoManager.undo() - expect(buffer.lineForRow(0)).not.toContain 'foo' + expect(buffer.lineForRow(0)).not.toContain 'foo' - undoManager.redo() - expect(buffer.lineForRow(0)).toContain 'foo' + undoManager.redo() + expect(buffer.lineForRow(0)).toContain 'foo' - undoManager.redo() - expect(buffer.lineForRow(1)).toBe ' 111var 222sort = function(items) {' + undoManager.redo() + expect(buffer.lineForRow(1)).toBe ' 111var 222sort = function(items) {' - undoManager.undo() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' + undoManager.undo() + expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' - it "does not record empty transactions", -> - buffer.insert([0,0], "foo") - undoManager.transact -> - - undoManager.undo() - expect(buffer.lineForRow(0)).not.toContain("foo") - - it "undoes operations that occured prior to an exception when the transaction is undone", -> - buffer.setText("jumpstreet") - - expect(-> + it "does not record empty transactions", -> + buffer.insert([0,0], "foo") undoManager.transact -> - buffer.insert([0,0], "3") - buffer.insert([0,0], "2") - throw new Error("problem") - buffer.insert([0,0], "2") - ).toThrow('problem') - expect(buffer.lineForRow(0)).toBe "23jumpstreet" - undoManager.undo() - expect(buffer.lineForRow(0)).toBe "jumpstreet" + undoManager.undo() + expect(buffer.lineForRow(0)).not.toContain("foo") - describe "when called without a function", -> - beforeEach -> + it "undoes operations that occured prior to an exception when the transaction is undone", -> + buffer.setText("jumpstreet") + + expect(-> + undoManager.transact -> + buffer.insert([0,0], "3") + buffer.insert([0,0], "2") + throw new Error("problem") + buffer.insert([0,0], "2") + ).toThrow('problem') + + expect(buffer.lineForRow(0)).toBe "23jumpstreet" + undoManager.undo() + expect(buffer.lineForRow(0)).toBe "jumpstreet" + + describe "when called without a function", -> + beforeEach -> + buffer.setText('') + + it "returns a transaction object that can be committed later", -> + buffer.append('1') + undoManager.transact() + buffer.append('2') + buffer.append('3') + undoManager.commit() + buffer.append('4') + + expect(buffer.getText()).toBe '1234' + undoManager.undo() + expect(buffer.getText()).toBe '123' + undoManager.undo() + expect(buffer.getText()).toBe '1' + undoManager.redo() + expect(buffer.getText()).toBe '123' + + it "returns a transaction object that can be aborted later", -> + buffer.append('1') + buffer.append('2') + + undoManager.transact() + + buffer.append('3') + buffer.append('4') + expect(buffer.getText()).toBe '1234' + + undoManager.abort() + expect(buffer.getText()).toBe '12' + + undoManager.undo() + expect(buffer.getText()).toBe '1' + + undoManager.redo() + expect(buffer.getText()).toBe '12' + + undoManager.redo() + expect(buffer.getText()).toBe '12' + + describe "commit", -> + it "throws an exception if there is no current transaction", -> + expect(-> buffer.commit()).toThrow() + + describe "abort", -> + it "does not affect the undo stack when the current transaction is empty", -> buffer.setText('') - - it "returns a transaction object that can be committed later", -> buffer.append('1') - undoManager.transact() - buffer.append('2') - buffer.append('3') - undoManager.commit() - buffer.append('4') - - expect(buffer.getText()).toBe '1234' - undoManager.undo() - expect(buffer.getText()).toBe '123' - undoManager.undo() - expect(buffer.getText()).toBe '1' - undoManager.redo() - expect(buffer.getText()).toBe '123' - - it "returns a transaction object that can be aborted later", -> - buffer.append('1') - buffer.append('2') - - undoManager.transact() - - buffer.append('3') - buffer.append('4') - expect(buffer.getText()).toBe '1234' - - undoManager.abort() - expect(buffer.getText()).toBe '12' - - undoManager.undo() - expect(buffer.getText()).toBe '1' - - undoManager.redo() - expect(buffer.getText()).toBe '12' - - undoManager.redo() - expect(buffer.getText()).toBe '12' - - describe "commit", -> - it "throws an exception if there is no current transaction", -> - expect(-> - buffer.commit() - ).toThrow() - - describe "abort", -> - it "does not affect the undo stack when the current transaction is empty", -> - buffer.setText('') - buffer.append('1') - buffer.transact() - buffer.abort() - expect(buffer.getText()).toBe '1' - buffer.undo() - expect(buffer.getText()).toBe '' - - it "throws an exception if there is no current transaction", -> - expect(-> + buffer.transact() buffer.abort() - ).toThrow() + expect(buffer.getText()).toBe '1' + buffer.undo() + expect(buffer.getText()).toBe '' - describe "when a `do` operation throws an exception", -> - it "clears the stack", -> - spyOn(console, 'error') - buffer.setText("word") - class FailingOperation - do: -> throw new Error("I'm a bad do operation") + it "throws an exception if there is no current transaction", -> + expect(-> buffer.abort()).toThrow() - buffer.insert([0,0], "1") + describe "exception handling", -> + describe "when a `do` operation throws an exception", -> + it "clears the stack", -> + spyOn(console, 'error') + buffer.setText("word") + buffer.insert([0,0], "1") + expect(-> + undoManager.pushOperation(do: -> throw new Error("I'm a bad do operation")) + ).toThrow("I'm a bad do operation") - expect(-> - undoManager.pushOperation(new FailingOperation()) - ).toThrow("I'm a bad do operation") - - undoManager.undo() - expect(buffer.lineForRow(0)).toBe "1word" - - describe "when an `undo` operation throws an exception", -> - it "clears the stack", -> - spyOn(console, 'error') - buffer.setText("word") - class FailingOperation - undo: -> throw new Error("I'm a bad undo operation") - - buffer.insert([0,0], "1") - undoManager.pushOperation(new FailingOperation()) - expect(-> undoManager.undo() - ).toThrow("I'm a bad undo operation") - expect(buffer.lineForRow(0)).toBe "1word" + expect(buffer.lineForRow(0)).toBe "1word" - describe "when an `redo` operation throws an exception", -> - it "clears the stack", -> - spyOn(console, 'error') - buffer.setText("word") - class FailingOperation - redo: -> throw new Error("I'm a bad redo operation") + describe "when an `undo` operation throws an exception", -> + it "clears the stack", -> + spyOn(console, 'error') + buffer.setText("word") + buffer.insert([0,0], "1") + undoManager.pushOperation(undo: -> throw new Error("I'm a bad undo operation")) + expect(-> undoManager.undo()).toThrow("I'm a bad undo operation") + expect(buffer.lineForRow(0)).toBe "1word" - buffer.insert([0,0], "1") - undoManager.pushOperation(new FailingOperation()) - undoManager.undo() - expect(-> - undoManager.redo() - ).toThrow("I'm a bad redo operation") - expect(buffer.lineForRow(0)).toBe "1word" + describe "when an `redo` operation throws an exception", -> + it "clears the stack", -> + spyOn(console, 'error') + buffer.setText("word") + buffer.insert([0,0], "1") + undoManager.pushOperation(redo: -> throw new Error("I'm a bad redo operation")) + undoManager.undo() + expect(-> undoManager.redo()).toThrow("I'm a bad redo operation") + expect(buffer.lineForRow(0)).toBe "1word" diff --git a/spec/fixtures/packages/package-with-a-cson-grammar.tmbundle/Snippets/test.cson b/spec/fixtures/packages/package-with-a-cson-grammar.tmbundle/Snippets/test.cson new file mode 100644 index 000000000..a82d8447a --- /dev/null +++ b/spec/fixtures/packages/package-with-a-cson-grammar.tmbundle/Snippets/test.cson @@ -0,0 +1,4 @@ +'name': 'Really' +'scope': 'source.alot' +'tabTrigger': 'really' +'content': 'I really like $1 alot$0' diff --git a/spec/fixtures/packages/package-with-a-cson-grammar.tmbundle/Syntaxes/alot.cson b/spec/fixtures/packages/package-with-a-cson-grammar.tmbundle/Syntaxes/alot.cson new file mode 100644 index 000000000..d30d33f56 --- /dev/null +++ b/spec/fixtures/packages/package-with-a-cson-grammar.tmbundle/Syntaxes/alot.cson @@ -0,0 +1,11 @@ +'fileTypes': ['alot'] +'name': 'Alot' +'scopeName': 'source.alot' +'patterns': [ + { + 'captures': + '0': + 'name': 'keyword.alot' + 'match': 'alot' + } +] diff --git a/spec/fixtures/packages/package-with-activation-events/main.coffee b/spec/fixtures/packages/package-with-activation-events/main.coffee new file mode 100644 index 000000000..fc2588ef9 --- /dev/null +++ b/spec/fixtures/packages/package-with-activation-events/main.coffee @@ -0,0 +1,6 @@ +module.exports = + activationEventCallCount: 0 + + activate: -> + rootView.getActiveEditor()?.command 'activation-event', => + @activationEventCallCount++ diff --git a/spec/fixtures/packages/package-with-activation-events/package.cson b/spec/fixtures/packages/package-with-activation-events/package.cson new file mode 100644 index 000000000..80903d6f4 --- /dev/null +++ b/spec/fixtures/packages/package-with-activation-events/package.cson @@ -0,0 +1,2 @@ +'activationEvents': ['activation-event'] +'main': 'main' diff --git a/spec/fixtures/packages/package-with-module/index.coffee b/spec/fixtures/packages/package-with-module/index.coffee index 6b54f633a..49287b94d 100644 --- a/spec/fixtures/packages/package-with-module/index.coffee +++ b/spec/fixtures/packages/package-with-module/index.coffee @@ -1,6 +1,8 @@ -AtomPackage = require 'atom-package' - module.exports = -class MyPackage extends AtomPackage + configDefaults: + numbers: { one: 1, two: 2 } + activate: -> @activateCalled = true + + deactivate: -> \ No newline at end of file diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 1a4657488..fb7c36cc2 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -17,12 +17,20 @@ fixturePackagesPath = require.resolve('fixtures/packages') require.paths.unshift(fixturePackagesPath) [bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = [] -# Load TextMate bundles, which specs rely on (but not other packages) -atom.loadTextMatePackages() +# Specs rely on TextMate bundles (but not atom packages) +window.loadTextMatePackages = -> + TextMatePackage = require 'text-mate-package' + config.packageDirPaths.unshift(fixturePackagesPath) + window.textMatePackages = [] + for path in atom.getPackagePaths() when TextMatePackage.testName(path) + window.textMatePackages.push atom.loadPackage(fs.base(path)) + +window.loadTextMatePackages() beforeEach -> window.fixturesProject = new Project(require.resolve('fixtures')) window.resetTimeouts() + atom.atomPackageStates = {} # used to reset keymap after each spec bindingSetsToRestore = _.clone(keymap.bindingSets) @@ -30,7 +38,6 @@ beforeEach -> # reset config before each spec; don't load or save from/to `config.json` window.config = new Config() - config.packageDirPaths.unshift(fixturePackagesPath) spyOn(config, 'load') spyOn(config, 'save') config.set "editor.fontSize", 16 @@ -63,17 +70,19 @@ afterEach -> window.keymap.bindKeys '*', 'meta-w': 'close' $(document).on 'close', -> window.close() +$(document).on 'toggle-dev-tools', (e) -> + atom.toggleDevTools() if $('#root-view').length is 0 $('html,body').css('overflow', 'auto') +jasmine.getEnv().addEqualityTester(_.isEqual) # Use underscore's definition of equality for toEqual assertions +jasmine.getEnv().defaultTimeoutInterval = 1000 + ensureNoPathSubscriptions = -> watchedPaths = $native.getWatchedPaths() $native.unwatchAllPaths() if watchedPaths.length > 0 throw new Error("Leaking subscriptions for paths: " + watchedPaths.join(", ")) -# Use underscore's definition of equality for toEqual assertions -jasmine.Env.prototype.equals_ = _.isEqual - emitObject = jasmine.StringPrettyPrinter.prototype.emitObject jasmine.StringPrettyPrinter.prototype.emitObject = (obj) -> if obj.inspect @@ -85,8 +94,6 @@ jasmine.unspy = (object, methodName) -> throw new Error("Not a spy") unless object[methodName].originalValue? object[methodName] = object[methodName].originalValue -jasmine.getEnv().defaultTimeoutInterval = 1000 - window.keyIdentifierForKey = (key) -> if key.length > 1 # named key key diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee index 15b9be963..e0e002f00 100644 --- a/spec/stdlib/child-process-spec.coffee +++ b/spec/stdlib/child-process-spec.coffee @@ -127,3 +127,18 @@ describe 'Child Processes', -> runs -> expect(output.length).toBeGreaterThan 1 + + describe "when the cwd option is set", -> + it "runs the task from the specified current working directory", -> + output = [] + + waitsForPromise -> + options = + cwd: fixturesProject.getPath() + stdout: (data) -> output.push(data) + stderr: (data) -> + + ChildProcess.exec("pwd", options) + + runs -> + expect(output.join('')).toBe "#{fixturesProject.getPath()}\n" diff --git a/src/app/anchor-range.coffee b/src/app/anchor-range.coffee deleted file mode 100644 index 1f1ad5151..000000000 --- a/src/app/anchor-range.coffee +++ /dev/null @@ -1,41 +0,0 @@ -Range = require 'range' -EventEmitter = require 'event-emitter' -Subscriber = require 'subscriber' -_ = require 'underscore' - -module.exports = -class AnchorRange - start: null - end: null - buffer: null - editSession: null # optional - destroyed: false - - constructor: (bufferRange, @buffer, @editSession) -> - bufferRange = Range.fromObject(bufferRange) - @startAnchor = @buffer.addAnchorAtPosition(bufferRange.start, ignoreChangesStartingOnAnchor: true) - @endAnchor = @buffer.addAnchorAtPosition(bufferRange.end) - @subscribe @startAnchor, 'destroyed', => @destroy() - @subscribe @endAnchor, 'destroyed', => @destroy() - - getBufferRange: -> - new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition()) - - getScreenRange: -> - new Range(@startAnchor.getScreenPosition(), @endAnchor.getScreenPosition()) - - containsBufferPosition: (bufferPosition) -> - @getBufferRange().containsPoint(bufferPosition) - - destroy: -> - return if @destroyed - @unsubscribe() - @startAnchor.destroy() - @endAnchor.destroy() - @buffer.removeAnchorRange(this) - @editSession?.removeAnchorRange(this) - @destroyed = true - @trigger 'destroyed' - -_.extend(AnchorRange.prototype, EventEmitter) -_.extend(AnchorRange.prototype, Subscriber) diff --git a/src/app/anchor.coffee b/src/app/anchor.coffee deleted file mode 100644 index f0b26b10b..000000000 --- a/src/app/anchor.coffee +++ /dev/null @@ -1,91 +0,0 @@ -Point = require 'point' -EventEmitter = require 'event-emitter' -_ = require 'underscore' - -module.exports = -class Anchor - buffer: null - editSession: null # optional - bufferPosition: null - screenPosition: null - ignoreChangesStartingOnAnchor: false - strong: false - destroyed: false - - constructor: (@buffer, options = {}) -> - { @editSession, @ignoreChangesStartingOnAnchor, @strong } = options - - handleBufferChange: (e) -> - { oldRange, newRange } = e - position = @getBufferPosition() - - if oldRange.containsPoint(position, exclusive: true) - if @strong - @setBufferPosition(oldRange.start) - else - @destroy() - return - - return if @ignoreChangesStartingOnAnchor and position.isEqual(oldRange.start) - return if position.isLessThan(oldRange.end) - - newRow = newRange.end.row - newColumn = newRange.end.column - if position.row == oldRange.end.row - newColumn += position.column - oldRange.end.column - else - newColumn = position.column - newRow += position.row - oldRange.end.row - - @setBufferPosition([newRow, newColumn], bufferChange: true) - - getBufferPosition: -> - @bufferPosition - - setBufferPosition: (position, options={}) -> - @bufferPosition = Point.fromObject(position) - clip = options.clip ? true - @bufferPosition = @buffer.clipPosition(@bufferPosition) if clip - @refreshScreenPosition(options) - - getScreenPosition: -> - @screenPosition - - getScreenRow: -> - @screenPosition.row - - setScreenPosition: (position, options={}) -> - oldScreenPosition = @screenPosition - oldBufferPosition = @bufferPosition - @screenPosition = Point.fromObject(position) - clip = options.clip ? true - assignBufferPosition = options.assignBufferPosition ? true - - @screenPosition = @editSession.clipScreenPosition(@screenPosition, options) if clip - @bufferPosition = @editSession.bufferPositionForScreenPosition(@screenPosition, options) if assignBufferPosition - - Object.freeze @screenPosition - Object.freeze @bufferPosition - - unless @screenPosition.isEqual(oldScreenPosition) - @trigger 'moved', - oldScreenPosition: oldScreenPosition - newScreenPosition: @screenPosition - oldBufferPosition: oldBufferPosition - newBufferPosition: @bufferPosition - bufferChange: options.bufferChange - autoscroll: options.autoscroll - - refreshScreenPosition: (options={}) -> - return unless @editSession - screenPosition = @editSession.screenPositionForBufferPosition(@bufferPosition, options) - @setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false, autoscroll: options.autoscroll) - - destroy: -> - return if @destroyed - @buffer.removeAnchor(this) - @editSession?.removeAnchor(this) - @destroyed = true - @trigger 'destroyed' - -_.extend(Anchor.prototype, EventEmitter) diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee index 15a4e465a..d9971e9c5 100644 --- a/src/app/atom-package.coffee +++ b/src/app/atom-package.coffee @@ -1,42 +1,88 @@ Package = require 'package' fs = require 'fs' +_ = require 'underscore' +$ = require 'jquery' module.exports = class AtomPackage extends Package metadata: null - keymapsDirPath: null - autoloadStylesheets: true + packageMain: null - constructor: (@name) -> - super - @keymapsDirPath = fs.join(@path, 'keymaps') - - load: -> + load: ({activateImmediately}={}) -> try @loadMetadata() @loadKeymaps() - @loadStylesheets() if @autoloadStylesheets - rootView?.activatePackage(@name, this) unless @isDirectory + @loadStylesheets() + if @metadata.activationEvents and not activateImmediately + @subscribeToActivationEvents() + else + @activatePackageMain() catch e console.warn "Failed to load package named '#{@name}'", e.stack this + disableEventHandlersOnBubblePath: (event) -> + bubblePathEventHandlers = [] + disabledHandler = -> + element = $(event.target) + while element.length + if eventHandlers = element.data('events')?[event.type] + for eventHandler in eventHandlers + eventHandler.disabledHandler = eventHandler.handler + eventHandler.handler = disabledHandler + bubblePathEventHandlers.push(eventHandler) + element = element.parent() + bubblePathEventHandlers + + restoreEventHandlersOnBubblePath: (eventHandlers) -> + for eventHandler in eventHandlers + eventHandler.handler = eventHandler.disabledHandler + delete eventHandler.disabledHandler + + unsubscribeFromActivationEvents: (activateHandler) -> + if _.isArray(@metadata.activationEvents) + rootView.off(event, activateHandler) for event in @metadata.activationEvents + else + rootView.off(event, selector, activateHandler) for event, selector of @metadata.activationEvents + + subscribeToActivationEvents: () -> + activateHandler = (event) => + bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event) + @activatePackageMain() + $(event.target).trigger(event) + @restoreEventHandlersOnBubblePath(bubblePathEventHandlers) + @unsubscribeFromActivationEvents(activateHandler) + + if _.isArray(@metadata.activationEvents) + rootView.command(event, activateHandler) for event in @metadata.activationEvents + else + rootView.command(event, selector, activateHandler) for event, selector of @metadata.activationEvents + + activatePackageMain: -> + mainPath = @path + mainPath = fs.join(mainPath, @metadata.main) if @metadata.main + mainPath = require.resolve(mainPath) + if fs.isFile(mainPath) + @packageMain = require(mainPath) + config.setDefaults(@name, @packageMain.configDefaults) + atom.activateAtomPackage(this) + loadMetadata: -> - if metadataPath = fs.resolveExtension(fs.join(@path, "package"), ['cson', 'json']) + if metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json']) @metadata = fs.readObject(metadataPath) + @metadata ?= {} loadKeymaps: -> - if keymaps = @metadata?.keymaps - keymaps = keymaps.map (relativePath) => - fs.resolve(@keymapsDirPath, relativePath, ['cson', 'json', '']) - keymap.load(keymapPath) for keymapPath in keymaps + keymapsDirPath = fs.join(@path, 'keymaps') + + if @metadata.keymaps + for path in @metadata.keymaps + keymapPath = fs.resolve(keymapsDirPath, path, ['cson', 'json', '']) + keymap.load(keymapPath) else - keymap.loadDirectory(@keymapsDirPath) + keymap.loadDirectory(keymapsDirPath) loadStylesheets: -> - for stylesheetPath in @getStylesheetPaths() - requireStylesheet(stylesheetPath) - - getStylesheetPaths: -> stylesheetDirPath = fs.join(@path, 'stylesheets') - fs.list(stylesheetDirPath) + for stylesheetPath in fs.list(stylesheetDirPath) + requireStylesheet(stylesheetPath) \ No newline at end of file diff --git a/src/app/atom-theme.coffee b/src/app/atom-theme.coffee index b52ed3111..1d0224acf 100644 --- a/src/app/atom-theme.coffee +++ b/src/app/atom-theme.coffee @@ -8,11 +8,10 @@ class AtomTheme extends Theme @stylesheets[stylesheetPath] = fs.read(stylesheetPath) load: -> - if /\.css$/.test(@path) - @loadStylesheet @path + if fs.extension(@path) is '.css' + @loadStylesheet(@path) else - json = fs.read(fs.join(@path, "package.json")) - for stylesheetName in JSON.parse(json).stylesheets - stylesheetPath = fs.join(@path, stylesheetName) - @loadStylesheet stylesheetPath + metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json']) + stylesheetNames = fs.readObject(metadataPath).stylesheets + @loadStylesheet(fs.join(@path, name)) for name in stylesheetNames super diff --git a/src/app/atom.coffee b/src/app/atom.coffee index b080e03bb..ec9246539 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -12,42 +12,58 @@ _.extend atom, exitWhenDone: window.location.params.exitWhenDone loadedThemes: [] pendingBrowserProcessCallbacks: {} + loadedPackages: [] + activatedAtomPackages: [] + atomPackageStates: {} + + activateAtomPackage: (pack) -> + @activatedAtomPackages.push(pack) + pack.packageMain.activate(@atomPackageStates[pack.name]) + + deactivateAtomPackages: -> + pack.packageMain.deactivate?() for pack in @activatedAtomPackages + @activatedAtomPackages = [] + + serializeAtomPackages: -> + packageStates = {} + for pack in @activatedAtomPackages + try + packageStates[pack.name] = pack.packageMain.serialize?() + catch e + console?.error("Exception serializing '#{pack.name}' package's module\n", e.stack) + packageStates + + loadPackage: (name, options) -> + packagePath = _.find @getPackagePaths(), (packagePath) -> fs.base(packagePath) == name + pack = Package.build(packagePath) + pack?.load(options) loadPackages: -> - {packages, asyncTextMatePackages} = _.groupBy @getPackages(), (pack) -> - if pack instanceof TextMatePackage and pack.name isnt 'text.tmbundle' - 'asyncTextMatePackages' + textMatePackages = [] + for path in @getPackagePaths() + pack = Package.build(path) + @loadedPackages.push(pack) + if pack instanceof TextMatePackage and fs.base(pack.path) isnt 'text.tmbundle' + textMatePackages.push(pack) else - 'packages' + pack.load() - pack.load() for pack in packages - if asyncTextMatePackages.length - new LoadTextMatePackagesTask(asyncTextMatePackages).start() + new LoadTextMatePackagesTask(textMatePackages).start() if textMatePackages.length > 0 - getPackages: -> - @packages ?= @getPackageNames().map((name) -> Package.build(name)) - .filter((pack) -> pack?) - new Array(@packages...) + getLoadedPackages: -> + _.clone(@loadedPackages) - loadTextMatePackages: -> - pack.load() for pack in @getTextMatePackages() - - getTextMatePackages: -> - @getPackages().filter (pack) -> pack instanceof TextMatePackage - - loadPackage: (name) -> - Package.build(name)?.load() - - getPackageNames: -> + getPackagePaths: -> disabledPackages = config.get("core.disabledPackages") ? [] - allPackageNames = [] + packagePaths = [] for packageDirPath in config.packageDirPaths - packageNames = fs.list(packageDirPath) - .filter((packagePath) -> fs.isDirectory(packagePath)) - .map((packagePath) -> fs.base(packagePath)) - allPackageNames.push(packageNames...) - _.unique(allPackageNames) - .filter (name) -> not _.contains(disabledPackages, name) + for packagePath in fs.list(packageDirPath) + continue if not fs.isDirectory(packagePath) + continue if fs.base(packagePath) in disabledPackages + continue if packagePath in packagePaths + packagePaths.push(packagePath) + + packagePaths loadThemes: -> themeNames = config.get("core.themes") ? ['atom-dark-ui', 'atom-dark-syntax'] diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index 06b241c75..06a5dc7c1 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -8,6 +8,8 @@ class BufferChangeOperation oldText: null newRange: null newText: null + markersToRestoreOnUndo: null + markersToRestoreOnRedo: null constructor: ({@buffer, @oldRange, @newText, @options}) -> @options ?= {} @@ -15,18 +17,24 @@ class BufferChangeOperation do: -> @oldText = @buffer.getTextInRange(@oldRange) @newRange = @calculateNewRange(@oldRange, @newText) + @markersToRestoreOnUndo = @invalidateMarkers(@oldRange) @changeBuffer oldRange: @oldRange newRange: @newRange oldText: @oldText newText: @newText + redo: -> + @restoreMarkers(@markersToRestoreOnRedo) + undo: -> + @markersToRestoreOnRedo = @invalidateMarkers(@newRange) @changeBuffer oldRange: @newRange newRange: @oldRange oldText: @newText newText: @oldText + @restoreMarkers(@markersToRestoreOnUndo) splitLines: (text) -> lines = text.split('\n') @@ -41,7 +49,6 @@ class BufferChangeOperation changeBuffer: ({ oldRange, newRange, newText, oldText }) -> { prefix, suffix } = @buffer.prefixAndSuffixForRange(oldRange) - {lines, lineEndings} = @splitLines(newText) lastLineIndex = lines.length - 1 @@ -65,7 +72,7 @@ class BufferChangeOperation event = { oldRange, newRange, oldText, newText } @buffer.trigger 'changed', event @buffer.scheduleStoppedChangingEvent() - @buffer.updateAnchors(event) + @updateMarkers(event) newRange calculateNewRange: (oldRange, newText) -> @@ -78,3 +85,18 @@ class BufferChangeOperation newRange.end.row += lastLineIndex newRange.end.column = lines[lastLineIndex].length newRange + + invalidateMarkers: (oldRange) -> + _.compact(@buffer.getMarkers().map (marker) -> marker.tryToInvalidate(oldRange)) + + updateMarkers: (bufferChange) -> + marker.handleBufferChange(bufferChange) for marker in @buffer.getMarkers() + @buffer.trigger 'markers-updated' + + restoreMarkers: (markersToRestore) -> + for [id, previousRange] in markersToRestore + if validMarker = @buffer.validMarkers[id] + validMarker.setRange(previousRange) + else if invalidMarker = @buffer.invalidMarkers[id] + @buffer.validMarkers[id] = invalidMarker + diff --git a/src/app/buffer-marker.coffee b/src/app/buffer-marker.coffee new file mode 100644 index 000000000..13957a949 --- /dev/null +++ b/src/app/buffer-marker.coffee @@ -0,0 +1,152 @@ +_ = require 'underscore' +Point = require 'point' +Range = require 'range' + +module.exports = +class BufferMarker + headPosition: null + tailPosition: null + observers: null + suppressObserverNotification: false + stayValid: false + + constructor: ({@id, @buffer, range, @stayValid, noTail, reverse}) -> + @headPositionObservers = [] + @observers = [] + @setRange(range, {noTail, reverse}) + + setRange: (range, options={}) -> + @consolidateObserverNotifications false, => + range = Range.fromObject(range) + if options.reverse + @setTailPosition(range.end) unless options.noTail + @setHeadPosition(range.start) + else + @setTailPosition(range.start) unless options.noTail + @setHeadPosition(range.end) + + isReversed: -> + @tailPosition? and @headPosition.isLessThan(@tailPosition) + + getRange: -> + if @tailPosition + new Range(@tailPosition, @headPosition) + else + new Range(@headPosition, @headPosition) + + getHeadPosition: -> @headPosition + + getTailPosition: -> @tailPosition ? @getHeadPosition() + + setHeadPosition: (newHeadPosition, options={}) -> + oldHeadPosition = @getHeadPosition() + newHeadPosition = Point.fromObject(newHeadPosition) + newHeadPosition = @buffer.clipPosition(newHeadPosition) if options.clip ? true + return if newHeadPosition.isEqual(@headPosition) + @headPosition = newHeadPosition + bufferChanged = !!options.bufferChanged + @notifyObservers({oldHeadPosition, newHeadPosition, bufferChanged}) + @headPosition + + setTailPosition: (newTailPosition, options={}) -> + oldTailPosition = @getTailPosition() + newTailPosition = Point.fromObject(newTailPosition) + newTailPosition = @buffer.clipPosition(newTailPosition) if options.clip ? true + return if newTailPosition.isEqual(@tailPosition) + @tailPosition = newTailPosition + bufferChanged = !!options.bufferChanged + @notifyObservers({oldTailPosition, newTailPosition, bufferChanged}) + @tailPosition + + getStartPosition: -> + @getRange().start + + getEndPosition: -> + @getRange().end + + placeTail: -> + @setTailPosition(@getHeadPosition()) unless @tailPosition + + clearTail: -> + oldTailPosition = @getTailPosition() + @tailPosition = null + newTailPosition = @getTailPosition() + @notifyObservers({oldTailPosition, newTailPosition, bufferChanged: false}) + + tryToInvalidate: (oldRange) -> + containsStart = oldRange.containsPoint(@getStartPosition(), exclusive: true) + containsEnd = oldRange.containsPoint(@getEndPosition(), exclusive: true) + return unless containsEnd or containsStart + + if @stayValid + previousRange = @getRange() + if containsStart and containsEnd + @setRange([oldRange.end, oldRange.end]) + else if containsStart + @setRange([oldRange.end, @getEndPosition()]) + else + @setRange([@getStartPosition(), oldRange.start]) + [@id, previousRange] + else + @invalidate() + [@id] + + handleBufferChange: (bufferChange) -> + @consolidateObserverNotifications true, => + @setHeadPosition(@updatePosition(@headPosition, bufferChange, false), clip: false, bufferChanged: true) + @setTailPosition(@updatePosition(@tailPosition, bufferChange, true), clip: false, bufferChanged: true) if @tailPosition + + updatePosition: (position, bufferChange, isFirstPoint) -> + { oldRange, newRange } = bufferChange + + return position if oldRange.containsPoint(position, exclusive: true) + return position if isFirstPoint and oldRange.start.isEqual(position) + return position if position.isLessThan(oldRange.end) + + newRow = newRange.end.row + newColumn = newRange.end.column + + if position.row == oldRange.end.row + newColumn += position.column - oldRange.end.column + else + newColumn = position.column + newRow += position.row - oldRange.end.row + + [newRow, newColumn] + + observe: (callback) -> + @observers.push(callback) + cancel: => @unobserve(callback) + + unobserve: (callback) -> + _.remove(@observers, callback) + + containsPoint: (point) -> + @getRange().containsPoint(point) + + notifyObservers: ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged}) -> + return if @suppressObserverNotification + return if _.isEqual(newHeadPosition, oldHeadPosition) and _.isEqual(newTailPosition, oldTailPosition) + oldHeadPosition ?= @getHeadPosition() + newHeadPosition ?= @getHeadPosition() + oldTailPosition ?= @getTailPosition() + newTailPosition ?= @getTailPosition() + for observer in @getObservers() + observer({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged}) + + getObservers: -> + new Array(@observers...) + + consolidateObserverNotifications: (bufferChanged, fn) -> + @suppressObserverNotification = true + oldHeadPosition = @getHeadPosition() + oldTailPosition = @getTailPosition() + fn() + newHeadPosition = @getHeadPosition() + newTailPosition = @getTailPosition() + @suppressObserverNotification = false + @notifyObservers({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged}) + + invalidate: (preserve) -> + delete @buffer.validMarkers[@id] + @buffer.invalidMarkers[@id] = this diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 62511d2f9..0200bddeb 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -6,8 +6,7 @@ Range = require 'range' EventEmitter = require 'event-emitter' UndoManager = require 'undo-manager' BufferChangeOperation = require 'buffer-change-operation' -Anchor = require 'anchor' -AnchorRange = require 'anchor-range' +BufferMarker = require 'buffer-marker' module.exports = class Buffer @@ -21,14 +20,15 @@ class Buffer lines: null lineEndings: null file: null - anchors: null - anchorRanges: null + validMarkers: null + invalidMarkers: null refcount: 0 constructor: (path, @project) -> @id = @constructor.idCounter++ - @anchors = [] - @anchorRanges = [] + @nextMarkerId = 1 + @validMarkers = {} + @invalidMarkers = {} @lines = [''] @lineEndings = [] @@ -264,38 +264,70 @@ class Buffer isEmpty: -> @lines.length is 1 and @lines[0].length is 0 - getAnchors: -> new Array(@anchors...) + getMarkers: -> + _.values(@validMarkers) - addAnchor: (options) -> - anchor = new Anchor(this, options) - @anchors.push(anchor) - anchor + getMarkerCount: -> + _.size(@validMarkers) - addAnchorAtPosition: (position, options) -> - anchor = @addAnchor(options) - anchor.setBufferPosition(position) - anchor + markRange: (range, options={}) -> + marker = new BufferMarker(_.defaults({ + id: (@nextMarkerId++).toString() + buffer: this + range + }, options)) + @validMarkers[marker.id] = marker + marker.id - addAnchorRange: (range, editSession) -> - anchorRange = new AnchorRange(range, this, editSession) - @anchorRanges.push(anchorRange) - anchorRange + markPosition: (position, options) -> + @markRange([position, position], _.defaults({noTail: true}, options)) - removeAnchor: (anchor) -> - _.remove(@anchors, anchor) + destroyMarker: (id) -> + delete @validMarkers[id] + delete @invalidMarkers[id] - removeAnchorRange: (anchorRange) -> - _.remove(@anchorRanges, anchorRange) + getMarkerPosition: (args...) -> + @getMarkerHeadPosition(args...) - anchorRangesForPosition: (position) -> - _.filter @anchorRanges, (anchorRange) -> anchorRange.containsBufferPosition(position) + setMarkerPosition: (args...) -> + @setMarkerHeadPosition(args...) - updateAnchors: (change) -> - anchors = @getAnchors() - anchor.pauseEvents() for anchor in anchors - anchor.handleBufferChange(change) for anchor in anchors - anchor.resumeEvents() for anchor in anchors - @trigger 'anchors-updated' + getMarkerHeadPosition: (id) -> + @validMarkers[id]?.getHeadPosition() + + setMarkerHeadPosition: (id, position, options) -> + @validMarkers[id]?.setHeadPosition(position) + + getMarkerTailPosition: (id) -> + @validMarkers[id]?.getTailPosition() + + setMarkerTailPosition: (id, position, options) -> + @validMarkers[id]?.setTailPosition(position) + + getMarkerRange: (id) -> + @validMarkers[id]?.getRange() + + setMarkerRange: (id, range, options) -> + @validMarkers[id]?.setRange(range, options) + + placeMarkerTail: (id) -> + @validMarkers[id]?.placeTail() + + clearMarkerTail: (id) -> + @validMarkers[id]?.clearTail() + + isMarkerReversed: (id) -> + @validMarkers[id]?.isReversed() + + observeMarker: (id, callback) -> + @validMarkers[id]?.observe(callback) + + markersForPosition: (bufferPosition) -> + bufferPosition = Point.fromObject(bufferPosition) + ids = [] + for id, marker of @validMarkers + ids.push(id) if marker.containsPoint(bufferPosition) + ids matchesInCharacterRange: (regex, startIndex, endIndex) -> text = @getText() diff --git a/src/app/cursor-view.coffee b/src/app/cursor-view.coffee index 402d74748..03fae93de 100644 --- a/src/app/cursor-view.coffee +++ b/src/app/cursor-view.coffee @@ -1,5 +1,4 @@ {View} = require 'space-pen' -Anchor = require 'anchor' Point = require 'point' Range = require 'range' _ = require 'underscore' @@ -18,18 +17,18 @@ class CursorView extends View shouldPauseBlinking: false initialize: (@cursor, @editor) -> - @cursor.on 'moved.cursor-view', ({ autoscroll }) => + @cursor.on 'moved.cursor-view', => @needsUpdate = true @shouldPauseBlinking = true - @editor.requestDisplayUpdate() @cursor.on 'visibility-changed.cursor-view', (visible) => @needsUpdate = true + + @cursor.on 'autoscrolled.cursor-view', => @editor.requestDisplayUpdate() @cursor.on 'destroyed.cursor-view', => @needsRemoval = true - @editor.requestDisplayUpdate() remove: -> @editor.removeCursorView(this) @@ -55,8 +54,8 @@ class CursorView extends View needsAutoscroll: -> @cursor.needsAutoscroll - autoscrolled: -> - @cursor.autoscrolled() + clearAutoscroll: -> + @cursor.clearAutoscroll() getPixelPosition: -> @editor.pixelPositionForScreenPosition(@getScreenPosition()) diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index 8ac44739e..5708d4bf7 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -1,6 +1,5 @@ Point = require 'point' Range = require 'range' -Anchor = require 'anchor' EventEmitter = require 'event-emitter' _ = require 'underscore' @@ -10,47 +9,60 @@ class Cursor bufferPosition: null goalColumn: null visible: true - needsAutoscroll: false + needsAutoscroll: null - constructor: ({@editSession, screenPosition, bufferPosition}) -> - @anchor = @editSession.addAnchor(strong: true) - @anchor.on 'moved', (e) => - @needsAutoscroll = (e.autoscroll ? true) and @isLastCursor() - @trigger 'moved', e - @editSession.trigger 'cursor-moved', e + constructor: ({@editSession, @marker}) -> + @editSession.observeMarker @marker, (e) => + @setVisible(@selection.isEmpty()) - @setScreenPosition(screenPosition) if screenPosition - @setBufferPosition(bufferPosition) if bufferPosition + {oldHeadScreenPosition, newHeadScreenPosition} = e + {oldHeadBufferPosition, newHeadBufferPosition} = e + {bufferChanged} = e + return if oldHeadScreenPosition.isEqual(newHeadScreenPosition) + + @needsAutoscroll ?= @isLastCursor() and !bufferChanged + + movedEvent = + oldBufferPosition: oldHeadBufferPosition + oldScreenPosition: oldHeadScreenPosition + newBufferPosition: newHeadBufferPosition + newScreenPosition: newHeadScreenPosition + bufferChanged: bufferChanged + + @trigger 'moved', movedEvent + @editSession.trigger 'cursor-moved', movedEvent @needsAutoscroll = true destroy: -> - @anchor.destroy() + @editSession.destroyMarker(@marker) @editSession.removeCursor(this) @trigger 'destroyed' - setScreenPosition: (screenPosition, options) -> - @goalColumn = null - @clearSelection() - @anchor.setScreenPosition(screenPosition, options) + setScreenPosition: (screenPosition, options={}) -> + @changePosition options, => + @editSession.setMarkerHeadScreenPosition(@marker, screenPosition, options) getScreenPosition: -> - @anchor.getScreenPosition() + @editSession.getMarkerHeadScreenPosition(@marker) - getScreenRow: -> - @anchor.getScreenRow() - - setBufferPosition: (bufferPosition, options) -> - @goalColumn = null - @clearSelection() - @anchor.setBufferPosition(bufferPosition, options) + setBufferPosition: (bufferPosition, options={}) -> + @changePosition options, => + @editSession.setMarkerHeadBufferPosition(@marker, bufferPosition, options) getBufferPosition: -> - @anchor.getBufferPosition() + @editSession.getMarkerHeadBufferPosition(@marker) + + changePosition: (options, fn) -> + @goalColumn = null + @clearSelection() + @needsAutoscroll = options.autoscroll ? @isLastCursor() + unless fn() + @trigger 'autoscrolled' if @needsAutoscroll setVisible: (visible) -> if @visible != visible @visible = visible - @needsAutoscroll = @visible and @isLastCursor() + @needsAutoscroll ?= true if @visible and @isLastCursor() @trigger 'visibility-changed', @visible isVisible: -> @visible @@ -67,8 +79,8 @@ class Cursor range = [[row, Math.min(0, column - 1)], [row, Math.max(0, column + 1)]] /^\s+$/.test @editSession.getTextInBufferRange(range) - autoscrolled: -> - @needsAutoscroll = false + clearAutoscroll: -> + @needsAutoscroll = null clearSelection: -> if @selection @@ -89,9 +101,6 @@ class Cursor getCurrentBufferLine: -> @editSession.lineForBufferRow(@getBufferRow()) - refreshScreenPosition: -> - @anchor.refreshScreenPosition() - moveUp: (rowCount = 1) -> { row, column } = @getScreenPosition() column = @goalColumn if @goalColumn? diff --git a/src/app/deferred-atom-package.coffee b/src/app/deferred-atom-package.coffee deleted file mode 100644 index 1f328fc02..000000000 --- a/src/app/deferred-atom-package.coffee +++ /dev/null @@ -1,36 +0,0 @@ -AtomPackage = require 'atom-package' -_ = require 'underscore' - -module.exports = -class DeferredAtomPackage extends AtomPackage - - constructor: -> - super - - @autoloadStylesheets = false - - activate: (@rootView, @state) -> - @instance = null - onLoadEvent = (e) => @onLoadEvent(e, @getInstance()) - if _.isArray(@loadEvents) - for event in @loadEvents - @rootView.command(event, onLoadEvent) - else - for event, selector of @loadEvents - @rootView.command(event, selector, onLoadEvent) - this - - deactivate: -> @instance?.deactivate?() - - serialize: -> - if @instance - @instance.serialize?() - else - @state - - getInstance: -> - unless @instance - @loadStylesheets() - InstanceClass = require @instanceClass - @instance = InstanceClass.activate(@rootView, @state) - @instance diff --git a/src/app/display-buffer-marker.coffee b/src/app/display-buffer-marker.coffee new file mode 100644 index 000000000..78c9e8d2e --- /dev/null +++ b/src/app/display-buffer-marker.coffee @@ -0,0 +1,114 @@ +Range = require 'range' +_ = require 'underscore' + +module.exports = +class DisplayBufferMarker + observers: null + bufferMarkerSubscription: null + headScreenPosition: null + tailScreenPosition: null + + constructor: ({@id, @displayBuffer}) -> + @buffer = @displayBuffer.buffer + + getScreenRange: -> + @displayBuffer.screenRangeForBufferRange(@getBufferRange(), wrapAtSoftNewlines: true) + + setScreenRange: (screenRange, options) -> + @setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange, options), options) + + getBufferRange: -> + @buffer.getMarkerRange(@id) + + setBufferRange: (bufferRange, options) -> + @buffer.setMarkerRange(@id, bufferRange, options) + + getHeadScreenPosition: -> + @headScreenPosition ?= @displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true) + + setHeadScreenPosition: (screenPosition, options) -> + screenPosition = @displayBuffer.clipScreenPosition(screenPosition, options) + @setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options)) + + getHeadBufferPosition: -> + @buffer.getMarkerHeadPosition(@id) + + setHeadBufferPosition: (bufferPosition) -> + @buffer.setMarkerHeadPosition(@id, bufferPosition) + + getTailScreenPosition: -> + @tailScreenPosition ?= @displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true) + + setTailScreenPosition: (screenPosition, options) -> + screenPosition = @displayBuffer.clipScreenPosition(screenPosition, options) + @setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options)) + + getTailBufferPosition: -> + @buffer.getMarkerTailPosition(@id) + + setTailBufferPosition: (bufferPosition) -> + @buffer.setMarkerTailPosition(@id, bufferPosition) + + placeTail: -> + @buffer.placeMarkerTail(@id) + + clearTail: -> + @buffer.clearMarkerTail(@id) + + observe: (callback) -> + @observeBufferMarkerIfNeeded() + @observers.push(callback) + cancel: => @unobserve(callback) + + unobserve: (callback) -> + _.remove(@observers, callback) + @unobserveBufferMarkerIfNeeded() + + observeBufferMarkerIfNeeded: -> + return if @observers + @observers = [] + @getHeadScreenPosition() # memoize current value + @getTailScreenPosition() # memoize current value + @bufferMarkerSubscription = + @buffer.observeMarker @id, ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged}) => + @notifyObservers + oldHeadBufferPosition: oldHeadPosition + newHeadBufferPosition: newHeadPosition + oldTailBufferPosition: oldTailPosition + newTailBufferPosition: newTailPosition + bufferChanged: bufferChanged + @displayBuffer.markers[@id] = this + + unobserveBufferMarkerIfNeeded: -> + return if @observers.length + @observers = null + @bufferMarkerSubscription.cancel() + delete @displayBuffer.markers[@id] + + notifyObservers: ({oldHeadBufferPosition, oldTailBufferPosition, bufferChanged}) -> + oldHeadScreenPosition = @getHeadScreenPosition() + @headScreenPosition = null + newHeadScreenPosition = @getHeadScreenPosition() + + oldTailScreenPosition = @getTailScreenPosition() + @tailScreenPosition = null + newTailScreenPosition = @getTailScreenPosition() + + return if _.isEqual(newHeadScreenPosition, oldHeadScreenPosition) and _.isEqual(newTailScreenPosition, oldTailScreenPosition) + + oldHeadBufferPosition ?= @getHeadBufferPosition() + newHeadBufferPosition = @getHeadBufferPosition() + oldTailBufferPosition ?= @getTailBufferPosition() + newTailBufferPosition = @getTailBufferPosition() + + for observer in @getObservers() + observer({ + oldHeadScreenPosition, newHeadScreenPosition, + oldTailScreenPosition, newTailScreenPosition, + oldHeadBufferPosition, newHeadBufferPosition, + oldTailBufferPosition, newTailBufferPosition, + bufferChanged + }) + + getObservers: -> + new Array(@observers...) diff --git a/src/app/display-buffer.coffee b/src/app/display-buffer.coffee index 91e8af4b3..84090f052 100644 --- a/src/app/display-buffer.coffee +++ b/src/app/display-buffer.coffee @@ -7,6 +7,7 @@ Range = require 'range' Fold = require 'fold' ScreenLine = require 'screen-line' Token = require 'token' +DisplayBufferMarker = require 'display-buffer-marker' module.exports = class DisplayBuffer @@ -16,6 +17,7 @@ class DisplayBuffer tokenizedBuffer: null activeFolds: null foldsById: null + markers: null constructor: (@buffer, options={}) -> @id = @constructor.idCounter++ @@ -24,6 +26,7 @@ class DisplayBuffer @softWrapColumn = options.softWrapColumn ? Infinity @activeFolds = {} @foldsById = {} + @markers = {} @buildLineMap() @tokenizedBuffer.on 'changed', (e) => @handleTokenizedBufferChange(e) @@ -33,13 +36,17 @@ class DisplayBuffer @lineMap = new LineMap @lineMap.insertAtScreenRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow()) + triggerChanged: (eventProperties, refreshMarkers=true) -> + @refreshMarkerScreenPositions() if refreshMarkers + @trigger 'changed', eventProperties + setSoftWrapColumn: (@softWrapColumn) -> start = 0 end = @getLastRow() @buildLineMap() screenDelta = @getLastRow() - end bufferDelta = 0 - @trigger 'changed', { start, end, screenDelta, bufferDelta } + @triggerChanged({ start, end, screenDelta, bufferDelta }) lineForRow: (row) -> @lineMap.lineForScreenRow(row) @@ -95,7 +102,7 @@ class DisplayBuffer end = oldScreenRange.end.row screenDelta = newScreenRange.end.row - oldScreenRange.end.row bufferDelta = 0 - @trigger 'changed', { start, end, screenDelta, bufferDelta } + @triggerChanged({ start, end, screenDelta, bufferDelta }) fold @@ -124,7 +131,8 @@ class DisplayBuffer screenDelta = newScreenRange.end.row - oldScreenRange.end.row bufferDelta = 0 - @trigger 'changed', { start, end, screenDelta, bufferDelta } + @refreshMarkerScreenPositions() + @triggerChanged({ start, end, screenDelta, bufferDelta }) destroyFoldsContainingBufferRow: (bufferRow) -> for row, folds of @activeFolds @@ -213,19 +221,17 @@ class DisplayBuffer @handleBufferChange(bufferChange) bufferDelta = bufferChange.newRange.end.row - bufferChange.oldRange.end.row - tokenizedBufferStart = @bufferRowForScreenRow(@screenRowForBufferRow(tokenizedBufferChange.start)) tokenizedBufferEnd = tokenizedBufferChange.end tokenizedBufferDelta = tokenizedBufferChange.delta - start = @screenRowForBufferRow(tokenizedBufferStart) end = @lastScreenRowForBufferRow(tokenizedBufferEnd) newScreenLines = @buildLinesForBufferRows(tokenizedBufferStart, tokenizedBufferEnd + tokenizedBufferDelta) @lineMap.replaceScreenRows(start, end, newScreenLines) screenDelta = @lastScreenRowForBufferRow(tokenizedBufferEnd + tokenizedBufferDelta) - end - @trigger 'changed', { start, end, screenDelta, bufferDelta } + @triggerChanged({ start, end, screenDelta, bufferDelta }, false) buildLineForBufferRow: (bufferRow) -> @buildLinesForBufferRows(bufferRow, bufferRow) @@ -290,6 +296,86 @@ class DisplayBuffer rangeForAllLines: -> new Range([0, 0], @clipScreenPosition([Infinity, Infinity])) + getMarker: (id) -> + @markers[id] ? new DisplayBufferMarker({id, displayBuffer: this}) + + getMarkers: -> + _.values(@markers) + + markScreenRange: (screenRange) -> + @markBufferRange(@bufferRangeForScreenRange(screenRange)) + + markBufferRange: (args...) -> + @buffer.markRange(args...) + + markScreenPosition: (screenPosition, options) -> + @markBufferPosition(@bufferPositionForScreenPosition(screenPosition), options) + + markBufferPosition: (bufferPosition, options) -> + @buffer.markPosition(bufferPosition, options) + + destroyMarker: (id) -> + @buffer.destroyMarker(id) + delete @markers[id] + + getMarkerScreenRange: (id) -> + @getMarker(id).getScreenRange() + + setMarkerScreenRange: (id, screenRange, options) -> + @getMarker(id).setScreenRange(screenRange, options) + + getMarkerBufferRange: (id) -> + @getMarker(id).getBufferRange() + + setMarkerBufferRange: (id, bufferRange, options) -> + @getMarker(id).setBufferRange(bufferRange, options) + + getMarkerScreenPosition: (id) -> + @getMarkerHeadScreenPosition(id) + + getMarkerBufferPosition: (id) -> + @getMarkerHeadBufferPosition(id) + + getMarkerHeadScreenPosition: (id) -> + @getMarker(id).getHeadScreenPosition() + + setMarkerHeadScreenPosition: (id, screenPosition, options) -> + @getMarker(id).setHeadScreenPosition(screenPosition, options) + + getMarkerHeadBufferPosition: (id) -> + @getMarker(id).getHeadBufferPosition() + + setMarkerHeadBufferPosition: (id, bufferPosition) -> + @getMarker(id).setHeadBufferPosition(bufferPosition) + + getMarkerTailScreenPosition: (id) -> + @getMarker(id).getTailScreenPosition() + + setMarkerTailScreenPosition: (id, screenPosition, options) -> + @getMarker(id).setTailScreenPosition(screenPosition, options) + + getMarkerTailBufferPosition: (id) -> + @getMarker(id).getTailBufferPosition() + + setMarkerTailBufferPosition: (id, bufferPosition) -> + @getMarker(id).setTailBufferPosition(bufferPosition) + + placeMarkerTail: (id) -> + @getMarker(id).placeTail() + + clearMarkerTail: (id) -> + @getMarker(id).clearTail() + + isMarkerReversed: (id) -> + @buffer.isMarkerReversed(id) + + observeMarker: (id, callback) -> + @getMarker(id).observe(callback) + + refreshMarkerScreenPositions: -> + for marker in @getMarkers() + marker.notifyObservers(bufferChanged: false) + destroy: -> @tokenizedBuffer.destroy() diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index c0df1d6f0..d0a87ff26 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -1,6 +1,5 @@ Point = require 'point' Buffer = require 'buffer' -Anchor = require 'anchor' LanguageMode = require 'language-mode' DisplayBuffer = require 'display-buffer' Cursor = require 'cursor' @@ -8,7 +7,6 @@ Selection = require 'selection' EventEmitter = require 'event-emitter' Subscriber = require 'subscriber' Range = require 'range' -AnchorRange = require 'anchor-range' _ = require 'underscore' fs = require 'fs' @@ -29,8 +27,6 @@ class EditSession scrollLeft: 0 languageMode: null displayBuffer: null - anchors: null - anchorRanges: null cursors: null selections: null softTabs: true @@ -40,8 +36,6 @@ class EditSession @softTabs = @buffer.usesSoftTabs() ? softTabs ? true @languageMode = new LanguageMode(this, @buffer.getExtension()) @displayBuffer = new DisplayBuffer(@buffer, { @languageMode, tabLength }) - @anchors = [] - @anchorRanges = [] @cursors = [] @selections = [] @addCursorAtScreenPosition([0, 0]) @@ -49,12 +43,11 @@ class EditSession @buffer.retain() @subscribe @buffer, "path-changed", => @trigger "path-changed" @subscribe @buffer, "contents-conflicted", => @trigger "contents-conflicted" - @subscribe @buffer, "anchors-updated", => @mergeCursors() + @subscribe @buffer, "markers-updated", => @mergeCursors() @preserveCursorPositionOnBufferReload() @subscribe @displayBuffer, "changed", (e) => - @refreshAnchorScreenPositions() unless e.bufferDelta @trigger 'screen-lines-changed', e destroy: -> @@ -62,10 +55,11 @@ class EditSession @destroyed = true @unsubscribe() @buffer.release() + selection.destroy() for selection in @getSelections() @displayBuffer.destroy() - @project.removeEditSession(this) - anchor.destroy() for anchor in @getAnchors() - anchorRange.destroy() for anchorRange in @getAnchorRanges() + @project?.removeEditSession(this) + @trigger 'destroyed' + @off() serialize: -> buffer: @buffer.getPath() @@ -451,38 +445,77 @@ class EditSession pushOperation: (operation) -> @buffer.pushOperation(operation, this) - getAnchors: -> - new Array(@anchors...) + markScreenRange: (args...) -> + @displayBuffer.markScreenRange(args...) - getAnchorRanges: -> - new Array(@anchorRanges...) + markBufferRange: (args...) -> + @displayBuffer.markBufferRange(args...) - addAnchor: (options={}) -> - anchor = @buffer.addAnchor(_.extend({editSession: this}, options)) - @anchors.push(anchor) - anchor + markScreenPosition: (args...) -> + @displayBuffer.markScreenPosition(args...) - addAnchorAtBufferPosition: (bufferPosition, options) -> - anchor = @addAnchor(options) - anchor.setBufferPosition(bufferPosition) - anchor + markBufferPosition: (args...) -> + @displayBuffer.markBufferPosition(args...) - addAnchorRange: (range) -> - anchorRange = @buffer.addAnchorRange(range, this) - @anchorRanges.push(anchorRange) - anchorRange + destroyMarker: (args...) -> + @displayBuffer.destroyMarker(args...) - removeAnchor: (anchor) -> - _.remove(@anchors, anchor) + getMarkerCount: -> + @buffer.getMarkerCount() - refreshAnchorScreenPositions: -> - anchor.refreshScreenPosition() for anchor in @getAnchors() + getMarkerScreenRange: (args...) -> + @displayBuffer.getMarkerScreenRange(args...) - removeAnchorRange: (anchorRange) -> - _.remove(@anchorRanges, anchorRange) + setMarkerScreenRange: (args...) -> + @displayBuffer.setMarkerScreenRange(args...) - anchorRangesForBufferPosition: (bufferPosition) -> - _.intersect(@anchorRanges, @buffer.anchorRangesForPosition(bufferPosition)) + getMarkerBufferRange: (args...) -> + @displayBuffer.getMarkerBufferRange(args...) + + setMarkerBufferRange: (args...) -> + @displayBuffer.setMarkerBufferRange(args...) + + getMarkerScreenPosition: (args...) -> + @displayBuffer.getMarkerScreenPosition(args...) + + getMarkerBufferPosition: (args...) -> + @displayBuffer.getMarkerBufferPosition(args...) + + getMarkerHeadScreenPosition: (args...) -> + @displayBuffer.getMarkerHeadScreenPosition(args...) + + setMarkerHeadScreenPosition: (args...) -> + @displayBuffer.setMarkerHeadScreenPosition(args...) + + getMarkerHeadBufferPosition: (args...) -> + @displayBuffer.getMarkerHeadBufferPosition(args...) + + setMarkerHeadBufferPosition: (args...) -> + @displayBuffer.setMarkerHeadBufferPosition(args...) + + getMarkerTailScreenPosition: (args...) -> + @displayBuffer.getMarkerTailScreenPosition(args...) + + setMarkerTailScreenPosition: (args...) -> + @displayBuffer.setMarkerTailScreenPosition(args...) + + getMarkerTailBufferPosition: (args...) -> + @displayBuffer.getMarkerTailBufferPosition(args...) + + setMarkerTailBufferPosition: (args...) -> + @displayBuffer.setMarkerTailBufferPosition(args...) + + observeMarker: (args...) -> + @displayBuffer.observeMarker(args...) + + placeMarkerTail: (args...) -> + @displayBuffer.placeMarkerTail(args...) + + clearMarkerTail: (args...) -> + @displayBuffer.clearMarkerTail(args...) + + isMarkerReversed: (args...) -> + @displayBuffer.isMarkerReversed(args...) hasMultipleCursors: -> @getCursors().length > 1 @@ -493,31 +526,36 @@ class EditSession _.last(@cursors) addCursorAtScreenPosition: (screenPosition) -> - @addCursor(new Cursor(editSession: this, screenPosition: screenPosition)) + marker = @markScreenPosition(screenPosition, stayValid: true) + @addSelection(marker).cursor addCursorAtBufferPosition: (bufferPosition) -> - @addCursor(new Cursor(editSession: this, bufferPosition: bufferPosition)) + marker = @markBufferPosition(bufferPosition, stayValid: true) + @addSelection(marker).cursor - addCursor: (cursor=new Cursor(editSession: this, screenPosition: [0,0])) -> + addCursor: (marker) -> + cursor = new Cursor(editSession: this, marker: marker) @cursors.push(cursor) @trigger 'cursor-added', cursor - @addSelectionForCursor(cursor) cursor removeCursor: (cursor) -> _.remove(@cursors, cursor) - addSelectionForCursor: (cursor) -> - selection = new Selection(editSession: this, cursor: cursor) + addSelection: (marker, options={}) -> + unless options.preserveFolds + @destroyFoldsIntersectingBufferRange(@getMarkerBufferRange(marker)) + cursor = @addCursor(marker) + selection = new Selection({editSession: this, marker, cursor}) @selections.push(selection) + @mergeIntersectingSelections() @trigger 'selection-added', selection selection addSelectionForBufferRange: (bufferRange, options={}) -> - bufferRange = Range.fromObject(bufferRange) - @destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds - @addCursor().selection.setBufferRange(bufferRange, options) - @mergeIntersectingSelections() + options = _.defaults({stayValid: true}, options) + marker = @markBufferRange(bufferRange, options) + @addSelection(marker) setSelectedBufferRange: (bufferRange, options) -> @setSelectedBufferRanges([bufferRange], options) @@ -570,8 +608,8 @@ class EditSession _.any @getSelections(), (selection) -> selection.intersectsBufferRange(bufferRange) - setCursorScreenPosition: (position) -> - @moveCursors (cursor) -> cursor.setScreenPosition(position) + setCursorScreenPosition: (position, options) -> + @moveCursors (cursor) -> cursor.setScreenPosition(position, options) getCursorScreenPosition: -> @getCursor().getScreenPosition() @@ -710,9 +748,19 @@ class EditSession expandLastSelectionOverWord: -> @getLastSelection().expandOverWord() + selectMarker: (id) -> + if bufferRange = @getMarkerBufferRange(id) + @setSelectedBufferRange(bufferRange) + true + else + false + + markersForBufferPosition: (bufferPosition) -> + @buffer.markersForPosition(bufferPosition) + mergeCursors: -> positions = [] - for cursor in new Array(@getCursors()...) + for cursor in @getCursors() position = cursor.getBufferPosition().toString() if position in positions cursor.destroy() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index a11807bb0..36634b647 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -6,7 +6,6 @@ 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' @@ -21,6 +20,8 @@ class Editor extends View autoIndentOnPaste: false nonWordCharacters: "./\\()\"'-_:,.;<>~!@#$%^&*|+=[]{}`~?" + @nextEditorId: 1 + @content: (params) -> @div class: @classes(params), tabindex: -1, => @subview 'gutter', new Gutter @@ -55,15 +56,17 @@ class Editor extends View newSelections: null @deserialize: (state, rootView) -> + editor = new Editor(mini: state.mini, deserializing: true) editSessions = state.editSessions.map (state) -> EditSession.deserialize(state, rootView.project) - editor = new Editor(editSession: editSessions[state.activeEditSessionIndex], mini: state.mini) - editor.editSessions = editSessions + editor.pushEditSession(editSession) for editSession in editSessions + editor.setActiveEditSessionIndex(state.activeEditSessionIndex) editor.isFocused = state.isFocused editor - initialize: ({editSession, @mini} = {}) -> + initialize: ({editSession, @mini, deserializing} = {}) -> requireStylesheet 'editor.css' + @id = Editor.nextEditorId++ @lineCache = [] @configure() @bindKeys() @@ -76,19 +79,16 @@ class Editor extends View @newSelections = [] if editSession? - @editSessions.push editSession - @setActiveEditSessionIndex(0) + @edit(editSession) else if @mini - editSession = new EditSession + @edit(new EditSession buffer: new Buffer() softWrap: false tabLength: 2 softTabs: true - - @editSessions.push editSession - @setActiveEditSessionIndex(0) - else - throw new Error("Editor initialization requires an editSession") + ) + else if not deserializing + throw new Error("Editor must be constructed with an 'editSession' or 'mini: true' param") serialize: -> @saveScrollPositionForActiveEditSession() @@ -210,8 +210,8 @@ class Editor extends View moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine() moveLineUp: -> @activeEditSession.moveLineUp() moveLineDown: -> @activeEditSession.moveLineDown() + setCursorScreenPosition: (position, options) -> @activeEditSession.setCursorScreenPosition(position, options) duplicateLine: -> @activeEditSession.duplicateLine() - setCursorScreenPosition: (position) -> @activeEditSession.setCursorScreenPosition(position) getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition() getCursorScreenRow: -> @activeEditSession.getCursorScreenRow() setCursorBufferPosition: (position, options) -> @activeEditSession.setCursorBufferPosition(position, options) @@ -432,10 +432,10 @@ class Editor extends View @selectToScreenPosition(@screenPositionFromMouseEvent(event)) lastMoveEvent = event - $(document).on 'mousemove', moveHandler + $(document).on "mousemove.editor-#{@id}", moveHandler interval = setInterval(moveHandler, 20) - $(document).one 'mouseup', => + $(document).one "mouseup.editor-#{@id}", => clearInterval(interval) $(document).off 'mousemove', moveHandler reverse = @activeEditSession.getLastSelection().isReversed() @@ -449,7 +449,7 @@ class Editor extends View @calculateDimensions() @hiddenInput.width(@charWidth) @setSoftWrapColumn() if @activeEditSession.getSoftWrap() - @subscribe $(window), "resize", => @requestDisplayUpdate() + @subscribe $(window), "resize.editor-#{@id}", => @requestDisplayUpdate() @focus() if @isFocused @resetDisplay() @@ -458,14 +458,16 @@ class Editor extends View edit: (editSession) -> index = @editSessions.indexOf(editSession) - - if index == -1 - index = @editSessions.length - @editSessions.push(editSession) - @trigger 'editor:edit-session-added', [editSession, index] - + index = @pushEditSession(editSession) if index == -1 @setActiveEditSessionIndex(index) + pushEditSession: (editSession) -> + index = @editSessions.length + @editSessions.push(editSession) + editSession.on 'destroyed', => @editSessionDestroyed(editSession) + @trigger 'editor:edit-session-added', [editSession, index] + index + getBuffer: -> @activeEditSession.buffer destroyActiveEditSession: -> @@ -475,23 +477,18 @@ class Editor extends View return if @mini editSession = @editSessions[index] - destroySession = => - if index is @getActiveEditSessionIndex() and @editSessions.length > 1 - @loadPreviousEditSession() - _.remove(@editSessions, editSession) + destroySession = -> editSession.destroy() - @trigger 'editor:edit-session-removed', [editSession, index] - @remove() if @editSessions.length is 0 - callback(index) if callback + callback?(index) if editSession.isModified() and not editSession.hasEditors() @promptToSaveDirtySession(editSession, destroySession) else - destroySession(editSession) + destroySession() destroyInactiveEditSessions: -> destroyIndex = (index) => - index++ if @activeEditSession is @editSessions[index] + index++ if index is @getActiveEditSessionIndex() @destroyEditSessionIndex(index, destroyIndex) if @editSessions[index] destroyIndex(0) @@ -500,6 +497,13 @@ class Editor extends View @destroyEditSessionIndex(index, destroyIndex) if @editSessions[index] destroyIndex(0) + editSessionDestroyed: (editSession) -> + index = @editSessions.indexOf(editSession) + @loadPreviousEditSession() if index is @getActiveEditSessionIndex() and @editSessions.length > 1 + _.remove(@editSessions, editSession) + @trigger 'editor:edit-session-removed', [editSession, index] + @remove() if @editSessions.length is 0 + loadNextEditSession: -> nextIndex = (@getActiveEditSessionIndex() + 1) % @editSessions.length @setActiveEditSessionIndex(nextIndex) @@ -664,7 +668,7 @@ class Editor extends View if @activeEditSession.getSoftWrap() @addClass 'soft-wrap' @_setSoftWrapColumn = => @setSoftWrapColumn() - $(window).on "resize", @_setSoftWrapColumn + $(window).on "resize.editor-#{@id}", @_setSoftWrapColumn else @removeClass 'soft-wrap' $(window).off 'resize', @_setSoftWrapColumn @@ -751,15 +755,17 @@ class Editor extends View ) remove: (selector, keepData) -> - return super if keepData - + return super if keepData or @removed @trigger 'editor:will-be-removed' - - @destroyEditSessions() - if @pane() then @pane().remove() else super rootView?.focus() + afterRemove: -> + @removed = true + @destroyEditSessions() + $(window).off(".editor-#{@id}") + $(document).off(".editor-#{@id}") + getEditSessions: -> new Array(@editSessions...) @@ -883,6 +889,7 @@ class Editor extends View @updateCursorViews() @updateSelectionViews() @autoscroll(options) + @trigger 'editor:display-updated' updateCursorViews: -> if @newCursors.length > 0 @@ -912,13 +919,16 @@ class Editor extends View do (cursorView) -> cursorView.resetBlinking() autoscroll: (options={}) -> - for cursorView in @getCursorViews() when cursorView.needsAutoscroll() - @scrollToPixelPosition(cursorView.getPixelPosition()) unless options.suppressAutoScroll - cursorView.autoscrolled() + for cursorView in @getCursorViews() + if !options.suppressAutoScroll and cursorView.needsAutoscroll() + @scrollToPixelPosition(cursorView.getPixelPosition()) + cursorView.clearAutoscroll() - for selectionView in @getSelectionViews() when selectionView.needsAutoscroll() - @scrollToPixelPosition(selectionView.getCenterPixelPosition(), center: true) - selectionView.autoscrolled() + for selectionView in @getSelectionViews() + if !options.suppressAutoScroll and selectionView.needsAutoscroll() + @scrollToPixelPosition(selectionView.getCenterPixelPosition(), center: true) + selectionView.highlight() + selectionView.clearAutoscroll() updateRenderedLines: -> firstVisibleScreenRow = @getFirstVisibleScreenRow() diff --git a/src/app/grammar-view.coffee b/src/app/grammar-view.coffee index 3465c45a7..1319f979a 100644 --- a/src/app/grammar-view.coffee +++ b/src/app/grammar-view.coffee @@ -4,7 +4,7 @@ SelectList = require 'select-list' module.exports = class GrammarView extends SelectList - @viewClass: -> "#{super} grammar-view from-top overlay" + @viewClass: -> "#{super} grammar-view from-top overlay mini" filterKey: 'name' diff --git a/src/app/language-mode.coffee b/src/app/language-mode.coffee index bfc6cb108..3e1c1512c 100644 --- a/src/app/language-mode.coffee +++ b/src/app/language-mode.coffee @@ -8,78 +8,10 @@ class LanguageMode buffer = null grammar = null editSession = null - pairedCharacters: - '(': ')' - '[': ']' - '{': '}' - '"': '"' - "'": "'" constructor: (@editSession) -> @buffer = @editSession.buffer @reloadGrammar() - @bracketAnchorRanges = [] - - _.adviseBefore @editSession, 'insertText', (text) => - return true if @editSession.hasMultipleCursors() - - cursorBufferPosition = @editSession.getCursorBufferPosition() - previousCharacter = @editSession.getTextInBufferRange([cursorBufferPosition.add([0, -1]), cursorBufferPosition]) - nextCharacter = @editSession.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.add([0,1])]) - - if @isOpeningBracket(text) and not @editSession.getSelection().isEmpty() - @wrapSelectionInBrackets(text) - return false - - hasWordAfterCursor = /\w/.test(nextCharacter) - hasWordBeforeCursor = /\w/.test(previousCharacter) - - autoCompleteOpeningBracket = @isOpeningBracket(text) and not hasWordAfterCursor and not (@isQuote(text) and hasWordBeforeCursor) - skipOverExistingClosingBracket = false - if @isClosingBracket(text) and nextCharacter == text - if bracketAnchorRange = @bracketAnchorRanges.filter((anchorRange) -> anchorRange.getBufferRange().end.isEqual(cursorBufferPosition))[0] - skipOverExistingClosingBracket = true - - if skipOverExistingClosingBracket - bracketAnchorRange.destroy() - _.remove(@bracketAnchorRanges, bracketAnchorRange) - @editSession.moveCursorRight() - false - else if autoCompleteOpeningBracket - @editSession.insertText(text + @pairedCharacters[text]) - @editSession.moveCursorLeft() - range = [cursorBufferPosition, cursorBufferPosition.add([0, text.length])] - @bracketAnchorRanges.push @editSession.addAnchorRange(range) - false - - _.adviseBefore @editSession, 'backspace', => - return if @editSession.hasMultipleCursors() - return unless @editSession.getSelection().isEmpty() - - cursorBufferPosition = @editSession.getCursorBufferPosition() - previousCharacter = @editSession.getTextInBufferRange([cursorBufferPosition.add([0, -1]), cursorBufferPosition]) - nextCharacter = @editSession.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.add([0,1])]) - if @pairedCharacters[previousCharacter] is nextCharacter - @editSession.transact => - @editSession.moveCursorLeft() - @editSession.delete() - @editSession.delete() - false - - wrapSelectionInBrackets: (bracket) -> - pair = @pairedCharacters[bracket] - @editSession.mutateSelectedText (selection) => - return if selection.isEmpty() - - range = selection.getBufferRange() - options = reverse: selection.isReversed() - selection.insertText("#{bracket}#{selection.getText()}#{pair}") - selectionStart = range.start.add([0, 1]) - if range.start.row is range.end.row - selectionEnd = range.end.add([0, 1]) - else - selectionEnd = range.end - selection.setBufferRange([selectionStart, selectionEnd], options) reloadGrammar: -> path = @buffer.getPath() @@ -92,23 +24,6 @@ class LanguageMode throw new Error("No grammar found for path: #{path}") unless @grammar previousGrammar isnt @grammar - isQuote: (string) -> - /'|"/.test(string) - - isOpeningBracket: (string) -> - @pairedCharacters[string]? - - isClosingBracket: (string) -> - @getInvertedPairedCharacters()[string]? - - getInvertedPairedCharacters: -> - return @invertedPairedCharacters if @invertedPairedCharacters - - @invertedPairedCharacters = {} - for open, close of @pairedCharacters - @invertedPairedCharacters[close] = open - @invertedPairedCharacters - toggleLineCommentsForBufferRows: (start, end) -> scopes = @editSession.scopesForBufferPosition([start, 0]) return unless commentStartString = syntax.getProperty(scopes, "editor.commentStart") diff --git a/src/app/line-map.coffee b/src/app/line-map.coffee index 1a5a74147..f58333835 100644 --- a/src/app/line-map.coffee +++ b/src/app/line-map.coffee @@ -111,7 +111,7 @@ class LineMap [screenRow, screenLines] bufferPositionForScreenPosition: (screenPosition, options) -> - { row, column } = Point.fromObject(screenPosition) + { row, column } = @clipScreenPosition(Point.fromObject(screenPosition)) [bufferRow, screenLine] = @bufferRowAndScreenLineForScreenRow(row) bufferColumn = screenLine.bufferColumnForScreenColumn(column) new Point(bufferRow, bufferColumn) diff --git a/src/app/load-text-mate-packages-handler.coffee b/src/app/load-text-mate-packages-handler.coffee index c2e675bd1..c6dd9a18a 100644 --- a/src/app/load-text-mate-packages-handler.coffee +++ b/src/app/load-text-mate-packages-handler.coffee @@ -1,5 +1,5 @@ TextMatePackage = require 'text-mate-package' module.exports = - loadPackage: (name) -> - callTaskMethod('packageLoaded', new TextMatePackage(name).readGrammars()) + loadPackage: (path) -> + callTaskMethod('packageLoaded', new TextMatePackage(path).readGrammars()) diff --git a/src/app/load-text-mate-packages-task.coffee b/src/app/load-text-mate-packages-task.coffee index 0bb0ee805..9340f2c38 100644 --- a/src/app/load-text-mate-packages-task.coffee +++ b/src/app/load-text-mate-packages-task.coffee @@ -16,10 +16,10 @@ class LoadTextMatePackagesTask extends Task return @package = @packages.shift() - @loadPackage(@package.name) + @loadPackage(@package.path) - loadPackage: (name) -> - @callWorkerMethod('loadPackage', name) + loadPackage: (path) -> + @callWorkerMethod('loadPackage', path) packageLoaded: (grammars) -> @package.loadGrammars(grammars) diff --git a/src/app/package.coffee b/src/app/package.coffee index 01bdde1fa..5a7897347 100644 --- a/src/app/package.coffee +++ b/src/app/package.coffee @@ -2,34 +2,17 @@ fs = require 'fs' module.exports = class Package - @resolve: (name) -> - path = require.resolve(name, verifyExistence: false) - return path if path - throw new Error("No package found named '#{name}'") - - @build: (name) -> + @build: (path) -> TextMatePackage = require 'text-mate-package' AtomPackage = require 'atom-package' - if TextMatePackage.testName(name) - new TextMatePackage(name) + + if TextMatePackage.testName(path) + new TextMatePackage(path) else - if fs.isDirectory(@resolve(name)) - new AtomPackage(name) - else - try - PackageClass = require name - new PackageClass(name) if typeof PackageClass is 'function' - catch e - console.warn "Failed to load package named '#{name}'", e.stack + new AtomPackage(path) name: null path: null - isDirectory: false - module: null - constructor: (@name) -> - @path = Package.resolve(@name) - @isDirectory = fs.isDirectory(@path) - @path = fs.directory(@path) unless @isDirectory - - activate: (rootView) -> + constructor: (@path) -> + @name = fs.base(@path) diff --git a/src/app/project.coffee b/src/app/project.coffee index 7a6c3a219..058262ede 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -118,6 +118,10 @@ class Project getEditSessions: -> new Array(@editSessions...) + eachEditSession: (callback) -> + callback(editSession) for editSession in @getEditSessions() + @on 'edit-session-created', (editSession) -> callback(editSession) + removeEditSession: (editSession) -> _.remove(@editSessions, editSession) @@ -125,9 +129,12 @@ class Project buffers = [] for editSession in @editSessions when not _.include(buffers, editSession.buffer) buffers.push editSession.buffer - buffers + eachBuffer: (callback) -> + callback(buffer) for buffer in @getBuffers() + @on 'buffer-created', (buffer) -> callback(buffer) + bufferForPath: (filePath) -> if filePath? filePath = @resolve(filePath) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 4063ee863..c5dee555a 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -29,19 +29,19 @@ class RootView extends View else projectOrPathToOpen = projectPath # This will migrate people over to the new project serialization scheme. It should be removed eventually. - rootView = new RootView(projectOrPathToOpen , packageStates: packageStates, suppressOpen: true) + atom.atomPackageStates = packageStates ? {} + + rootView = new RootView(projectOrPathToOpen , suppressOpen: true) rootView.setRootPane(rootView.deserializeView(panesViewState)) if panesViewState rootView - packageModules: null - packageStates: null + packages: null title: null pathToOpenIsFile: false - initialize: (projectOrPathToOpen, { @packageStates, suppressOpen } = {}) -> + initialize: (projectOrPathToOpen, { suppressOpen } = {}) -> window.rootView = this - @packageStates ?= {} - @packageModules = {} + @packages = [] @viewClasses = { "Pane": Pane, "PaneRow": PaneRow, @@ -68,7 +68,7 @@ class RootView extends View serialize: -> projectState: @project?.serialize() panesViewState: @panes.children().view()?.serialize() - packageStates: @serializePackages() + packageStates: atom.serializeAtomPackages() handleFocus: (e) -> if @getActiveEditor() @@ -118,33 +118,15 @@ class RootView extends View afterAttach: (onDom) -> @focus() if onDom - serializePackages: -> - packageStates = {} - for name, packageModule of @packageModules - try - packageStates[name] = packageModule.serialize?() - catch e - console?.error("Exception serializing '#{name}' package's module\n", e.stack) - packageStates - registerViewClass: (viewClass) -> @viewClasses[viewClass.name] = viewClass deserializeView: (viewState) -> @viewClasses[viewState.viewClass]?.deserialize(viewState, this) - activatePackage: (name, packageModule) -> - config.setDefaults(name, packageModule.configDefaults) if packageModule.configDefaults? - @packageModules[name] = packageModule - packageModule.activate(this, @packageStates[name]) - - deactivatePackage: (name) -> - @packageModules[name].deactivate?() - delete @packageModules[name] - deactivate: -> atom.setRootViewStateForPath(@project.getPath(), @serialize()) - @deactivatePackage(name) for name of @packageModules + atom.deactivateAtomPackages() @remove() open: (path, options = {}) -> @@ -274,6 +256,8 @@ class RootView extends View callback(editor) for editor in @getEditors() @on 'editor:attached', (e, editor) -> callback(editor) + eachEditSession: (callback) -> + @project.eachEditSession(callback) + eachBuffer: (callback) -> - callback(buffer) for buffer in @project.getBuffers() - @project.on 'buffer-created', (buffer) -> callback(buffer) + @project.eachBuffer(callback) diff --git a/src/app/selection-view.coffee b/src/app/selection-view.coffee index 567571fa6..1fb43c18a 100644 --- a/src/app/selection-view.coffee +++ b/src/app/selection-view.coffee @@ -1,4 +1,3 @@ -Anchor = require 'anchor' Point = require 'point' Range = require 'range' {View, $$} = require 'space-pen' @@ -69,9 +68,8 @@ class SelectionView extends View needsAutoscroll: -> @selection.needsAutoscroll - autoscrolled: -> - @selection.autoscrolled() - @highlight() + clearAutoscroll: -> + @selection.clearAutoscroll() highlight: -> @unhighlight() diff --git a/src/app/selection.coffee b/src/app/selection.coffee index abc702103..81102dc0f 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -1,31 +1,26 @@ Range = require 'range' -Anchor = require 'anchor' EventEmitter = require 'event-emitter' _ = require 'underscore' module.exports = class Selection - anchor: null wordwise: false initialScreenRange: null + needsAutoscroll: null - constructor: ({@cursor, @editSession}) -> + constructor: ({@cursor, @marker, @editSession}) -> @cursor.selection = this - - @cursor.on 'moved.selection', ({bufferChange}) => - @screenRangeChanged() unless bufferChange - + @editSession.observeMarker @marker, => @screenRangeChanged() @cursor.on 'destroyed.selection', => @cursor = null @destroy() destroy: -> - if @cursor - @cursor.off('.selection') - @cursor.destroy() - @anchor?.destroy() + return if @destroyed + @destroyed = true @editSession.removeSelection(this) - @trigger 'destroyed' + @trigger 'destroyed' unless @editSession.destroyed + @cursor?.destroy() finalize: -> @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange()) @@ -37,42 +32,31 @@ class Selection @getBufferRange().isEmpty() isReversed: -> - not @isEmpty() and @cursor.getBufferPosition().isLessThan(@anchor.getBufferPosition()) + @editSession.isMarkerReversed(@marker) isSingleScreenLine: -> @getScreenRange().isSingleLine() - autoscrolled: -> - @needsAutoscroll = false + clearAutoscroll: -> + @needsAutoscroll = null getScreenRange: -> - if @anchor - new Range(@anchor.getScreenPosition(), @cursor.getScreenPosition()) - else - new Range(@cursor.getScreenPosition(), @cursor.getScreenPosition()) + @editSession.getMarkerScreenRange(@marker) setScreenRange: (screenRange, options) -> - bufferRange = editSession.bufferRangeForScreenRange(screenRange) - @setBufferRange(bufferRange, options) + @setBufferRange(@editSession.bufferRangeForScreenRange(screenRange), options) getBufferRange: -> - if @anchor - new Range(@anchor.getBufferPosition(), @cursor.getBufferPosition()) - else - new Range(@cursor.getBufferPosition(), @cursor.getBufferPosition()) + @editSession.getMarkerBufferRange(@marker) setBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) - { start, end } = bufferRange - [start, end] = [end, start] if options.reverse ? @isReversed() - @needsAutoscroll = options.autoscroll - + options.reverse ?= @isReversed() @editSession.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds - @placeAnchor() unless @anchor @modifySelection => - @anchor.setBufferPosition(start, autoscroll: false) - @cursor.setBufferPosition(end, autoscroll: false) + @cursor.needsAutoscroll = false if options.autoscroll? + @editSession.setMarkerBufferRange(@marker, bufferRange, options) getBufferRowRange: -> range = @getBufferRange() @@ -84,16 +68,12 @@ class Selection screenRangeChanged: -> screenRange = @getScreenRange() @trigger 'screen-range-changed', screenRange - @cursor?.setVisible(screenRange.isEmpty()) getText: -> @editSession.buffer.getTextInRange(@getBufferRange()) clear: -> - return unless @anchor - @anchor.destroy() - @anchor = null - @screenRangeChanged() + @editSession.clearMarkerTail(@marker) selectWord: -> options = {} @@ -121,11 +101,9 @@ class Selection @modifySelection => if @initialScreenRange if position.isLessThan(@initialScreenRange.start) - @anchor.setScreenPosition(@initialScreenRange.end) - @cursor.setScreenPosition(position) + @editSession.setMarkerScreenRange(@marker, [position, @initialScreenRange.end], reverse: true) else - @anchor.setScreenPosition(@initialScreenRange.start) - @cursor.setScreenPosition(position) + @editSession.setMarkerScreenRange(@marker, [@initialScreenRange.start, position]) else @cursor.setScreenPosition(position) @@ -317,8 +295,7 @@ class Selection @editSession.autoIndentBufferRows(start, end) toggleLineComments: -> - @modifySelection => - @editSession.toggleLineCommentsForBufferRows(@getBufferRowRange()...) + @editSession.toggleLineCommentsForBufferRows(@getBufferRowRange()...) cutToEndOfLine: (maintainPasteboard) -> @selectToEndOfLine() if @isEmpty() @@ -352,18 +329,12 @@ class Selection modifySelection: (fn) -> @retainSelection = true - @placeAnchor() unless @anchor - @anchor.pauseEvents() - @cursor.pauseEvents() + @placeTail() fn() - @anchor.resumeEvents() - @cursor.resumeEvents() @retainSelection = false - placeAnchor: -> - @anchor = @editSession.addAnchor(strong: true) - @anchor.setScreenPosition(@cursor.getScreenPosition()) - @anchor.on 'moved.selection', => @screenRangeChanged() + placeTail: -> + @editSession.placeMarkerTail(@marker) intersectsBufferRange: (bufferRange) -> @getBufferRange().intersectsWith(bufferRange) diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index 64ca52e8f..5fb7d4c61 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -9,9 +9,12 @@ module.exports = class TextMateGrammar @readFromPath: (path) -> grammarContent = null - plist.parseString fs.read(path), (e, data) -> - throw new Error(e) if e - grammarContent = data[0] + if fs.extension(path) is '.cson' + grammarContent = fs.readObject(path) + else + plist.parseString fs.read(path), (e, data) -> + throw new Error(e) if e + grammarContent = data[0] throw new Error("Failed to load grammar at path `#{path}`") unless grammarContent grammarContent diff --git a/src/app/text-mate-package.coffee b/src/app/text-mate-package.coffee index 97eea6189..2a54d12fe 100644 --- a/src/app/text-mate-package.coffee +++ b/src/app/text-mate-package.coffee @@ -28,7 +28,7 @@ class TextMatePackage extends Package try @loadGrammars() catch e - console.warn "Failed to load package named '#{@name}'", e.stack + console.warn "Failed to load package at '#{@path}'", e.stack this getGrammars: -> @grammars diff --git a/src/app/window.coffee b/src/app/window.coffee index 15f7c319e..901c6dea8 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -1,7 +1,6 @@ # 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 'jquery' Config = require 'config' @@ -38,8 +37,8 @@ windowAdditions = # Note: RootView assigns itself on window on initialization so that # window.rootView is available when loading user configuration attachRootView: (pathToOpen) -> - if rootViewState = atom.getRootViewStateForPath(pathToOpen) - RootView.deserialize(rootViewState) + if pathState = atom.getRootViewStateForPath(pathToOpen) + RootView.deserialize(pathState) else new RootView(pathToOpen) @@ -96,11 +95,11 @@ windowAdditions = atom.confirm( "There are unsaved buffers, reload anyway?", "You will lose all unsaved changes if you reload", - "Reload", (-> Native.reload()), + "Reload", (-> $native.reload()), "Cancel" ) else - Native.reload() + $native.reload() onerror: -> atom.showDevTools() diff --git a/src/packages/autocomplete/index.coffee b/src/packages/autocomplete/index.coffee deleted file mode 100644 index 902c08e24..000000000 --- a/src/packages/autocomplete/index.coffee +++ /dev/null @@ -1,6 +0,0 @@ -AtomPackage = require 'atom-package' -AutocompleteView = require './src/autocomplete-view' - -module.exports = -class Autocomplete extends AtomPackage - activate: (rootView) -> AutocompleteView.activate(rootView) diff --git a/src/packages/autocomplete/src/autocomplete-view.coffee b/src/packages/autocomplete/lib/autocomplete-view.coffee similarity index 97% rename from src/packages/autocomplete/src/autocomplete-view.coffee rename to src/packages/autocomplete/lib/autocomplete-view.coffee index f2c200d44..a3b3cc93f 100644 --- a/src/packages/autocomplete/src/autocomplete-view.coffee +++ b/src/packages/autocomplete/lib/autocomplete-view.coffee @@ -4,10 +4,6 @@ SelectList = require 'select-list' module.exports = class AutocompleteView extends SelectList - @activate: (rootView) -> - rootView.eachEditor (editor) -> - new AutocompleteView(editor) if editor.attached and not editor.mini - @viewClass: -> "autocomplete #{super} popover-list" editor: null @@ -21,7 +17,6 @@ class AutocompleteView extends SelectList initialize: (@editor) -> super - @handleEvents() @setCurrentBuffer(@editor.getBuffer()) diff --git a/src/packages/autocomplete/lib/autocomplete.coffee b/src/packages/autocomplete/lib/autocomplete.coffee new file mode 100644 index 000000000..d98ed5a75 --- /dev/null +++ b/src/packages/autocomplete/lib/autocomplete.coffee @@ -0,0 +1,10 @@ +AutocompleteView = require './autocomplete-view' + +module.exports = + autoCompleteViews: [] + + activate: -> + rootView.eachEditor (editor) => + if editor.attached and not editor.mini + @autoCompleteViews.push new AutocompleteView(editor) + diff --git a/src/packages/autocomplete/package.cson b/src/packages/autocomplete/package.cson new file mode 100644 index 000000000..45380b0c4 --- /dev/null +++ b/src/packages/autocomplete/package.cson @@ -0,0 +1,3 @@ +'main': 'lib/autocomplete' +'activationEvents': + 'autocomplete:attach': '.editor' \ No newline at end of file diff --git a/src/packages/autocomplete/spec/autocomplete-spec.coffee b/src/packages/autocomplete/spec/autocomplete-spec.coffee index d9b2da85a..84b9d666e 100644 --- a/src/packages/autocomplete/spec/autocomplete-spec.coffee +++ b/src/packages/autocomplete/spec/autocomplete-spec.coffee @@ -1,37 +1,33 @@ $ = require 'jquery' -Autocomplete = require 'autocomplete/src/autocomplete-view' +AutocompleteView = require 'autocomplete/lib/autocomplete-view' +Autocomplete = require 'autocomplete/lib/autocomplete' Buffer = require 'buffer' Editor = require 'editor' RootView = require 'root-view' describe "Autocomplete", -> - autocomplete = null - editor = null - miniEditor = null - beforeEach -> - editor = new Editor(editSession: fixturesProject.buildEditSessionForPath('sample.js')) - atom.loadPackage('autocomplete') - autocomplete = new Autocomplete(editor) - miniEditor = autocomplete.miniEditor + rootView = new RootView(require.resolve('fixtures/sample.js')) + rootView.simulateDomAttachment() afterEach -> - editor?.remove() + rootView.deactivate() - describe "@activate(rootView)", -> + describe "@activate()", -> it "activates autocomplete on all existing and future editors (but not on autocomplete's own mini editor)", -> - rootView = new RootView(require.resolve('fixtures/sample.js')) - rootView.simulateDomAttachment() - Autocomplete.activate(rootView) + spyOn(AutocompleteView.prototype, 'initialize').andCallThrough() + autocompletePackage = atom.loadPackage("autocomplete") + expect(AutocompleteView.prototype.initialize).not.toHaveBeenCalled() + leftEditor = rootView.getActiveEditor() rightEditor = rootView.getActiveEditor().splitRight() - spyOn(Autocomplete.prototype, 'initialize') - leftEditor.trigger 'autocomplete:attach' expect(leftEditor.find('.autocomplete')).toExist() expect(rightEditor.find('.autocomplete')).not.toExist() + expect(AutocompleteView.prototype.initialize).toHaveBeenCalled() + autoCompleteView = leftEditor.find('.autocomplete').view() autoCompleteView.trigger 'core:cancel' expect(leftEditor.find('.autocomplete')).not.toExist() @@ -39,9 +35,20 @@ describe "Autocomplete", -> rightEditor.trigger 'autocomplete:attach' expect(rightEditor.find('.autocomplete')).toExist() - expect(Autocomplete.prototype.initialize).not.toHaveBeenCalled() +describe "AutocompleteView", -> + autocomplete = null + editor = null + miniEditor = null - rootView.deactivate() + beforeEach -> + new RootView + editor = new Editor(editSession: fixturesProject.buildEditSessionForPath('sample.js')) + atom.loadPackage('autocomplete') + autocomplete = new AutocompleteView(editor) + miniEditor = autocomplete.miniEditor + + afterEach -> + editor?.remove() describe 'autocomplete:attach event', -> it "shows autocomplete view and focuses its mini-editor", -> diff --git a/src/packages/autoflow/index.coffee b/src/packages/autoflow/autoflow.coffee similarity index 91% rename from src/packages/autoflow/index.coffee rename to src/packages/autoflow/autoflow.coffee index 73b52d8ed..058b9c3b5 100644 --- a/src/packages/autoflow/index.coffee +++ b/src/packages/autoflow/autoflow.coffee @@ -1,8 +1,5 @@ -AtomPackage = require 'atom-package' - module.exports = -class Autoflow extends AtomPackage - activate: (rootView) -> + activate: -> rootView.command 'autoflow:reflow-paragraph', '.editor', (e) => @reflowParagraph(e.currentTargetView()) diff --git a/src/packages/autoflow/package.cson b/src/packages/autoflow/package.cson new file mode 100644 index 000000000..b137bc94e --- /dev/null +++ b/src/packages/autoflow/package.cson @@ -0,0 +1 @@ +'main': 'autoflow' \ No newline at end of file diff --git a/src/packages/bracket-matcher/index.coffee b/src/packages/bracket-matcher/lib/bracket-matcher.coffee similarity index 54% rename from src/packages/bracket-matcher/index.coffee rename to src/packages/bracket-matcher/lib/bracket-matcher.coffee index 081ca9923..8cf47f879 100644 --- a/src/packages/bracket-matcher/index.coffee +++ b/src/packages/bracket-matcher/lib/bracket-matcher.coffee @@ -1,10 +1,15 @@ -AtomPackage = require 'atom-package' _ = require 'underscore' {$$} = require 'space-pen' Range = require 'range' module.exports = -class BracketMatcher extends AtomPackage + pairedCharacters: + '(': ')' + '[': ']' + '{': '}' + '"': '"' + "'": "'" + startPairMatches: '(': ')' '[': ']' @@ -17,8 +22,9 @@ class BracketMatcher extends AtomPackage pairHighlighted: false - activate: (rootView) -> + activate: -> rootView.eachEditor (editor) => @subscribeToEditor(editor) if editor.attached + rootView.eachEditSession (editSession) => @subscribeToEditSession(editSession) subscribeToEditor: (editor) -> editor.on 'cursor:moved.bracket-matcher', => @updateMatch(editor) @@ -49,7 +55,7 @@ class BracketMatcher extends AtomPackage view = $$ -> @div class: 'bracket-matcher' view.data('bufferPosition', bufferPosition) view.css('top', pixelPosition.top).css('left', pixelPosition.left) - view.width(editor.charWidth).height(editor.charHeight) + view.width(editor.charWidth).height(editor.lineHeight) findCurrentPair: (editor, buffer, matches) -> position = editor.getCursorBufferPosition() @@ -118,3 +124,84 @@ class BracketMatcher extends AtomPackage underlayer.append(@createView(editor, matchPosition)) underlayer.append(@createView(editor, position)) @pairHighlighted = true + + subscribeToEditSession: (editSession) -> + @bracketMarkers = [] + + _.adviseBefore editSession, 'insertText', (text) => + return true if editSession.hasMultipleCursors() + + cursorBufferPosition = editSession.getCursorBufferPosition() + previousCharacter = editSession.getTextInBufferRange([cursorBufferPosition.add([0, -1]), cursorBufferPosition]) + nextCharacter = editSession.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.add([0,1])]) + + if @isOpeningBracket(text) and not editSession.getSelection().isEmpty() + @wrapSelectionInBrackets(editSession, text) + return false + + hasWordAfterCursor = /\w/.test(nextCharacter) + hasWordBeforeCursor = /\w/.test(previousCharacter) + + autoCompleteOpeningBracket = @isOpeningBracket(text) and not hasWordAfterCursor and not (@isQuote(text) and hasWordBeforeCursor) + skipOverExistingClosingBracket = false + if @isClosingBracket(text) and nextCharacter == text + if bracketMarker = _.find(@bracketMarkers, (marker) => editSession.getMarkerBufferRange(marker)?.end.isEqual(cursorBufferPosition)) + skipOverExistingClosingBracket = true + + if skipOverExistingClosingBracket + editSession.destroyMarker(bracketMarker) + _.remove(@bracketMarkers, bracketMarker) + editSession.moveCursorRight() + false + else if autoCompleteOpeningBracket + editSession.insertText(text + @pairedCharacters[text]) + editSession.moveCursorLeft() + range = [cursorBufferPosition, cursorBufferPosition.add([0, text.length])] + @bracketMarkers.push editSession.markBufferRange(range) + false + + _.adviseBefore editSession, 'backspace', => + return if editSession.hasMultipleCursors() + return unless editSession.getSelection().isEmpty() + + cursorBufferPosition = editSession.getCursorBufferPosition() + previousCharacter = editSession.getTextInBufferRange([cursorBufferPosition.add([0, -1]), cursorBufferPosition]) + nextCharacter = editSession.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.add([0,1])]) + if @pairedCharacters[previousCharacter] is nextCharacter + editSession.transact => + editSession.moveCursorLeft() + editSession.delete() + editSession.delete() + false + + wrapSelectionInBrackets: (editSession, bracket) -> + pair = @pairedCharacters[bracket] + editSession.mutateSelectedText (selection) => + return if selection.isEmpty() + + range = selection.getBufferRange() + options = reverse: selection.isReversed() + selection.insertText("#{bracket}#{selection.getText()}#{pair}") + selectionStart = range.start.add([0, 1]) + if range.start.row is range.end.row + selectionEnd = range.end.add([0, 1]) + else + selectionEnd = range.end + selection.setBufferRange([selectionStart, selectionEnd], options) + + isQuote: (string) -> + /'|"/.test(string) + + getInvertedPairedCharacters: -> + return @invertedPairedCharacters if @invertedPairedCharacters + + @invertedPairedCharacters = {} + for open, close of @pairedCharacters + @invertedPairedCharacters[close] = open + @invertedPairedCharacters + + isOpeningBracket: (string) -> + @pairedCharacters[string]? + + isClosingBracket: (string) -> + @getInvertedPairedCharacters()[string]? diff --git a/src/packages/bracket-matcher/package.cson b/src/packages/bracket-matcher/package.cson new file mode 100644 index 000000000..59e060d88 --- /dev/null +++ b/src/packages/bracket-matcher/package.cson @@ -0,0 +1 @@ +'main': 'lib/bracket-matcher' diff --git a/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee b/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee index 69e882502..6b07071c4 100644 --- a/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee +++ b/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee @@ -1,63 +1,66 @@ RootView = require 'root-view' describe "bracket matching", -> - [rootView, editor] = [] + [editor, editSession, buffer] = [] beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) atom.loadPackage('bracket-matcher') rootView.attachToDom() editor = rootView.getActiveEditor() + editSession = editor.activeEditSession + buffer = editSession.buffer afterEach -> rootView.deactivate() - describe "when the cursor is before a starting pair", -> - it "highlights the starting pair and ending pair", -> - editor.moveCursorToEndOfLine() - editor.moveCursorLeft() - expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 - expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) - expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) - - describe "when the cursor is after a starting pair", -> - it "highlights the starting pair and ending pair", -> - editor.moveCursorToEndOfLine() - expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 - expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) - expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) - - describe "when the cursor is before an ending pair", -> - it "highlights the starting pair and ending pair", -> - editor.moveCursorToBottom() - editor.moveCursorLeft() - editor.moveCursorLeft() - expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 - expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) - expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) - - describe "when the cursor is after an ending pair", -> - it "highlights the starting pair and ending pair", -> - editor.moveCursorToBottom() - editor.moveCursorLeft() - expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 - expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) - expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) - - describe "when the cursor is moved off a pair", -> - it "removes the starting pair and ending pair highlights", -> - editor.moveCursorToEndOfLine() - expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 - editor.moveCursorToBeginningOfLine() - expect(editor.underlayer.find('.bracket-matcher').length).toBe 0 - - describe "pair balancing", -> - describe "when a second starting pair preceeds the first ending pair", -> - it "advances to the second ending pair", -> - editor.setCursorBufferPosition([8,42]) + describe "matching bracket highlighting", -> + describe "when the cursor is before a starting pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToEndOfLine() + editor.moveCursorLeft() expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 - expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([8,42]) - expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([8,54]) + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + + describe "when the cursor is after a starting pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToEndOfLine() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + + describe "when the cursor is before an ending pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToBottom() + editor.moveCursorLeft() + editor.moveCursorLeft() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + + describe "when the cursor is after an ending pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToBottom() + editor.moveCursorLeft() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + + describe "when the cursor is moved off a pair", -> + it "removes the starting pair and ending pair highlights", -> + editor.moveCursorToEndOfLine() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + editor.moveCursorToBeginningOfLine() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 0 + + describe "pair balancing", -> + describe "when a second starting pair preceeds the first ending pair", -> + it "advances to the second ending pair", -> + editor.setCursorBufferPosition([8,42]) + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([8,42]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([8,54]) describe "when editor:go-to-matching-bracket is triggered", -> describe "when the cursor is before the starting pair", -> @@ -84,3 +87,189 @@ describe "bracket matching", -> editor.setCursorBufferPosition([12, 1]) editor.trigger "editor:go-to-matching-bracket" expect(editor.getCursorBufferPosition()).toEqual [0, 28] + + describe "matching bracket insertion", -> + beforeEach -> + editSession.buffer.setText("") + + describe "when more than one character is inserted", -> + it "does not insert a matching bracket", -> + editSession.insertText("woah(") + expect(editSession.buffer.getText()).toBe "woah(" + + describe "when there is a word character after the cursor", -> + it "does not insert a matching bracket", -> + editSession.buffer.setText("ab") + editSession.setCursorBufferPosition([0, 1]) + editSession.insertText("(") + + expect(editSession.buffer.getText()).toBe "a(b" + + describe "when there are multiple cursors", -> + it "inserts ) at each cursor", -> + editSession.buffer.setText("()\nab\n[]\n12") + editSession.setCursorBufferPosition([3, 1]) + editSession.addCursorAtBufferPosition([2, 1]) + editSession.addCursorAtBufferPosition([1, 1]) + editSession.addCursorAtBufferPosition([0, 1]) + editSession.insertText ')' + + expect(editSession.buffer.getText()).toBe "())\na)b\n[)]\n1)2" + + describe "when there is a non-word character after the cursor", -> + it "inserts a closing bracket after an opening bracket is inserted", -> + editSession.buffer.setText("}") + editSession.setCursorBufferPosition([0, 0]) + editSession.insertText '{' + expect(buffer.lineForRow(0)).toBe "{}}" + expect(editSession.getCursorBufferPosition()).toEqual([0,1]) + + describe "when the cursor is at the end of the line", -> + it "inserts a closing bracket after an opening bracket is inserted", -> + editSession.buffer.setText("") + editSession.insertText '{' + expect(buffer.lineForRow(0)).toBe "{}" + expect(editSession.getCursorBufferPosition()).toEqual([0,1]) + + editSession.buffer.setText("") + editSession.insertText '(' + expect(buffer.lineForRow(0)).toBe "()" + expect(editSession.getCursorBufferPosition()).toEqual([0,1]) + + editSession.buffer.setText("") + editSession.insertText '[' + expect(buffer.lineForRow(0)).toBe "[]" + expect(editSession.getCursorBufferPosition()).toEqual([0,1]) + + editSession.buffer.setText("") + editSession.insertText '"' + expect(buffer.lineForRow(0)).toBe '""' + expect(editSession.getCursorBufferPosition()).toEqual([0,1]) + + editSession.buffer.setText("") + editSession.insertText "'" + expect(buffer.lineForRow(0)).toBe "''" + expect(editSession.getCursorBufferPosition()).toEqual([0,1]) + + describe "when the cursor is on a closing bracket and a closing bracket is inserted", -> + describe "when the closing bracket was there previously", -> + it "inserts a closing bracket", -> + editSession.insertText '()x' + editSession.setCursorBufferPosition([0, 1]) + editSession.insertText ')' + expect(buffer.lineForRow(0)).toBe "())x" + expect(editSession.getCursorBufferPosition().column).toBe 2 + + describe "when the closing bracket was automatically inserted from inserting an opening bracket", -> + it "only moves cursor over the closing bracket one time", -> + editSession.insertText '(' + expect(buffer.lineForRow(0)).toBe "()" + editSession.setCursorBufferPosition([0, 1]) + editSession.insertText ')' + expect(buffer.lineForRow(0)).toBe "()" + expect(editSession.getCursorBufferPosition()).toEqual [0, 2] + + editSession.setCursorBufferPosition([0, 1]) + editSession.insertText ')' + expect(buffer.lineForRow(0)).toBe "())" + expect(editSession.getCursorBufferPosition()).toEqual [0, 2] + + it "moves cursor over the closing bracket after other text is inserted", -> + editSession.insertText '(' + editSession.insertText 'ok cool' + expect(buffer.lineForRow(0)).toBe "(ok cool)" + editSession.setCursorBufferPosition([0, 8]) + editSession.insertText ')' + expect(buffer.lineForRow(0)).toBe "(ok cool)" + expect(editSession.getCursorBufferPosition()).toEqual [0, 9] + + it "works with nested brackets", -> + editSession.insertText '(' + editSession.insertText '1' + editSession.insertText '(' + editSession.insertText '2' + expect(buffer.lineForRow(0)).toBe "(1(2))" + editSession.setCursorBufferPosition([0, 4]) + editSession.insertText ')' + expect(buffer.lineForRow(0)).toBe "(1(2))" + expect(editSession.getCursorBufferPosition()).toEqual [0, 5] + editSession.insertText ')' + expect(buffer.lineForRow(0)).toBe "(1(2))" + expect(editSession.getCursorBufferPosition()).toEqual [0, 6] + + it "works with mixed brackets", -> + editSession.insertText '(' + editSession.insertText '}' + expect(buffer.lineForRow(0)).toBe "(})" + editSession.insertText ')' + expect(buffer.lineForRow(0)).toBe "(})" + expect(editSession.getCursorBufferPosition()).toEqual [0, 3] + + it "closes brackets with the same begin/end character correctly", -> + editSession.insertText '"' + editSession.insertText 'ok' + expect(buffer.lineForRow(0)).toBe '"ok"' + expect(editSession.getCursorBufferPosition()).toEqual [0, 3] + editSession.insertText '"' + expect(buffer.lineForRow(0)).toBe '"ok"' + expect(editSession.getCursorBufferPosition()).toEqual [0, 4] + + describe "when there is text selected on a single line", -> + it "wraps the selection with brackets", -> + editSession.insertText 'text' + editSession.moveCursorToBottom() + editSession.selectToTop() + editSession.selectAll() + editSession.insertText '(' + expect('(text)').toBe buffer.getText() + expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [0, 5]] + expect(editSession.getSelection().isReversed()).toBeTruthy() + + describe "when there is text selected on multiple lines", -> + it "wraps the selection with brackets", -> + editSession.insertText 'text\nabcd' + editSession.moveCursorToBottom() + editSession.selectToTop() + editSession.selectAll() + editSession.insertText '(' + expect('(text\nabcd)').toBe buffer.getText() + expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [1, 4]] + expect(editSession.getSelection().isReversed()).toBeTruthy() + + describe "when inserting a quote", -> + describe "when a word character is before the cursor", -> + it "does not automatically insert closing quote", -> + editSession.buffer.setText("abc") + editSession.setCursorBufferPosition([0, 3]) + editSession.insertText '"' + expect(buffer.lineForRow(0)).toBe "abc\"" + + editSession.buffer.setText("abc") + editSession.setCursorBufferPosition([0, 3]) + editSession.insertText '\'' + expect(buffer.lineForRow(0)).toBe "abc\'" + + describe "when a non word character is before the cursor", -> + it "automatically insert closing quote", -> + editSession.buffer.setText("ab@") + editSession.setCursorBufferPosition([0, 3]) + editSession.insertText '"' + expect(buffer.lineForRow(0)).toBe "ab@\"\"" + expect(editSession.getCursorBufferPosition()).toEqual [0, 4] + + describe "when the cursor is on an empty line", -> + it "automatically insert closing quote", -> + editSession.buffer.setText("") + editSession.setCursorBufferPosition([0, 0]) + editSession.insertText '"' + expect(buffer.lineForRow(0)).toBe "\"\"" + expect(editSession.getCursorBufferPosition()).toEqual [0, 1] + + describe "matching bracket deletion", -> + it "deletes the end bracket when it directly proceeds a begin bracket that is being backspaced", -> + buffer.setText("") + editSession.setCursorBufferPosition([0, 0]) + editSession.insertText '{' + expect(buffer.lineForRow(0)).toBe "{}" + editSession.backspace() + expect(buffer.lineForRow(0)).toBe "" diff --git a/src/packages/bracket-matcher/stylesheets/bracket-matcher.css b/src/packages/bracket-matcher/stylesheets/bracket-matcher.css index 8d77b10de..1eeb11bd4 100644 --- a/src/packages/bracket-matcher/stylesheets/bracket-matcher.css +++ b/src/packages/bracket-matcher/stylesheets/bracket-matcher.css @@ -1,3 +1,4 @@ .bracket-matcher { + border-bottom: 1px dotted lime; position: absolute; } diff --git a/src/packages/command-logger/src/command-logger-view.coffee b/src/packages/command-logger/lib/command-logger-view.coffee similarity index 95% rename from src/packages/command-logger/src/command-logger-view.coffee rename to src/packages/command-logger/lib/command-logger-view.coffee index 9dfa03360..584c01d5a 100644 --- a/src/packages/command-logger/src/command-logger-view.coffee +++ b/src/packages/command-logger/lib/command-logger-view.coffee @@ -4,18 +4,12 @@ _ = require 'underscore' module.exports = class CommandLoggerView extends ScrollView - @activate: (rootView, state) -> - @instance = new CommandLoggerView(rootView) - @content: (rootView) -> @div class: 'command-logger', tabindex: -1, => @h1 class: 'category-header', outlet: 'categoryHeader' @h1 class: 'category-summary', outlet: 'categorySummary' @div class: 'tree-map', outlet: 'treeMap' - @serialize: -> - @instance.serialize() - eventLog: null ignoredEvents: [ 'core:backspace' @@ -30,7 +24,7 @@ class CommandLoggerView extends ScrollView 'tree-view:directory-modified' ] - initialize: (@rootView) -> + initialize: -> super @command 'core:cancel', => @detach() @@ -176,7 +170,7 @@ class CommandLoggerView extends ScrollView d3.select('.command-logger').on('click', -> zoom(root)) attach: -> - @rootView.append(this) + rootView.append(this) @addTreeMap() @focus() @@ -184,8 +178,5 @@ class CommandLoggerView extends ScrollView return if @detaching @detaching = true super - @rootView.focus() + rootView.focus() @detaching = false - - serialize: -> - eventLog: @eventLog diff --git a/src/packages/command-logger/index.coffee b/src/packages/command-logger/lib/command-logger.coffee similarity index 50% rename from src/packages/command-logger/index.coffee rename to src/packages/command-logger/lib/command-logger.coffee index 027035980..074f2e015 100644 --- a/src/packages/command-logger/index.coffee +++ b/src/packages/command-logger/lib/command-logger.coffee @@ -1,18 +1,14 @@ -DeferredAtomPackage = require 'deferred-atom-package' $ = require 'jquery' module.exports = -class CommandLogger extends DeferredAtomPackage - - loadEvents: ['command-logger:toggle'] - - instanceClass: 'command-logger/src/command-logger-view' - - activate: (rootView, state={})-> - super + eventLog: {} + commandLoggerView: null + originalTrigger: null + activate: (state={})-> @eventLog = state.eventLog ? {} rootView.command 'command-logger:clear-data', => @eventLog = {} + rootView.command 'command-logger:toggle', => @createView().toggle(@eventLog) registerTriggeredEvent = (eventName) => eventNameLog = @eventLog[eventName] @@ -23,10 +19,22 @@ class CommandLogger extends DeferredAtomPackage @eventLog[eventName] = eventNameLog eventNameLog.count++ eventNameLog.lastRun = new Date().getTime() - originalTrigger = $.fn.trigger + trigger = $.fn.trigger + @originalTrigger = trigger $.fn.trigger = (eventName) -> eventName = eventName.type if eventName.type registerTriggeredEvent(eventName) if $(this).events()[eventName] - originalTrigger.apply(this, arguments) + trigger.apply(this, arguments) - onLoadEvent: (event, instance) -> instance.toggle(@eventLog) + deactivate: -> + $.fn.trigger = @originalTrigger if @originalTrigger? + @commandLoggerView = null + @eventLog = {} + + serialize: -> + {@eventLog} + + createView: -> + unless @commandLoggerView + CommandLoggerView = require 'command-logger/lib/command-logger-view' + @commandLoggerView = new CommandLoggerView diff --git a/src/packages/command-logger/package.cson b/src/packages/command-logger/package.cson new file mode 100644 index 000000000..ae57a4b8c --- /dev/null +++ b/src/packages/command-logger/package.cson @@ -0,0 +1 @@ +'main': 'lib/command-logger' diff --git a/src/packages/command-logger/spec/command-logger-spec.coffee b/src/packages/command-logger/spec/command-logger-spec.coffee index e2a06da2b..ab9d4e478 100644 --- a/src/packages/command-logger/spec/command-logger-spec.coffee +++ b/src/packages/command-logger/spec/command-logger-spec.coffee @@ -1,12 +1,13 @@ RootView = require 'root-view' -CommandLogger = require 'command-logger/src/command-logger-view' +CommandLogger = require 'command-logger/lib/command-logger-view' describe "CommandLogger", -> - [rootView, commandLogger, editor] = [] + [commandLogger, editor] = [] beforeEach -> - rootView = new RootView(require.resolve('fixtures/sample.js')) - commandLogger = atom.loadPackage('command-logger') + new RootView(require.resolve('fixtures/sample.js')) + commandLogger = atom.loadPackage('command-logger').packageMain + commandLogger.eventLog = {} editor = rootView.getActiveEditor() afterEach -> @@ -43,7 +44,7 @@ describe "CommandLogger", -> describe "when an event is ignored", -> it "does not create a node for that event", -> - commandLoggerView = commandLogger.getInstance() + commandLoggerView = commandLogger.createView() commandLoggerView.ignoredEvents.push 'editor:delete-line' editor.trigger 'editor:delete-line' commandLoggerView.eventLog = commandLogger.eventLog diff --git a/src/packages/command-palette/index.coffee b/src/packages/command-palette/index.coffee deleted file mode 100644 index 15d41efb5..000000000 --- a/src/packages/command-palette/index.coffee +++ /dev/null @@ -1,10 +0,0 @@ -DeferredAtomPackage = require 'deferred-atom-package' - -module.exports = -class CommandPalette extends DeferredAtomPackage - - loadEvents: ['command-palette:toggle'] - - instanceClass: 'command-palette/src/command-palette-view' - - onLoadEvent: (event, instance) -> instance.attach() diff --git a/src/packages/command-palette/src/command-palette-view.coffee b/src/packages/command-palette/lib/command-palette-view.coffee similarity index 85% rename from src/packages/command-palette/src/command-palette-view.coffee rename to src/packages/command-palette/lib/command-palette-view.coffee index 90c083d0d..2acb80753 100644 --- a/src/packages/command-palette/src/command-palette-view.coffee +++ b/src/packages/command-palette/lib/command-palette-view.coffee @@ -5,8 +5,8 @@ _ = require 'underscore' module.exports = class CommandPaletteView extends SelectList - @activate: (rootView) -> - @instance = new CommandPaletteView(rootView) + @activate: -> + new CommandPaletteView @viewClass: -> "#{super} command-palette overlay from-top" @@ -16,12 +16,17 @@ class CommandPaletteView extends SelectList previouslyFocusedElement: null keyBindings: null - initialize: (@rootView) -> - @command 'command-palette:toggle', => - @cancel() - false + initialize: -> super + rootView.command 'command-palette:toggle', => @toggle() + + toggle: -> + if @hasParent() + @cancel() + else + @attach() + attach: -> super @@ -34,7 +39,7 @@ class CommandPaletteView extends SelectList events = _.sortBy events, (e) -> e.eventDescription @setArray(events) - @appendTo(@rootView) + @appendTo(rootView) @miniEditor.focus() itemForElement: ({eventName, eventDescription}) -> diff --git a/src/packages/command-palette/package.cson b/src/packages/command-palette/package.cson new file mode 100644 index 000000000..e76ea2d38 --- /dev/null +++ b/src/packages/command-palette/package.cson @@ -0,0 +1,2 @@ +'main': 'lib/command-palette-view' +'activationEvents': ['command-palette:toggle'] diff --git a/src/packages/command-palette/spec/command-palette-spec.coffee b/src/packages/command-palette/spec/command-palette-spec.coffee index 1f56f1ab5..a57533dd9 100644 --- a/src/packages/command-palette/spec/command-palette-spec.coffee +++ b/src/packages/command-palette/spec/command-palette-spec.coffee @@ -1,17 +1,17 @@ RootView = require 'root-view' -CommandPalette = require 'command-palette/src/command-palette-view' +CommandPalette = require 'command-palette/lib/command-palette-view' $ = require 'jquery' _ = require 'underscore' describe "CommandPalette", -> - [rootView, palette] = [] + [palette] = [] beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) - atom.loadPackage("command-palette").getInstance() - palette = CommandPalette.instance + atom.loadPackage("command-palette") rootView.attachToDom().focus() rootView.trigger 'command-palette:toggle' + palette = rootView.find('.command-palette').view() afterEach -> rootView.remove() diff --git a/src/packages/command-panel/index.coffee b/src/packages/command-panel/index.coffee deleted file mode 100644 index 4f1a26c8a..000000000 --- a/src/packages/command-panel/index.coffee +++ /dev/null @@ -1,33 +0,0 @@ -DeferredAtomPackage = require 'deferred-atom-package' - -module.exports = -class CommandPanel extends DeferredAtomPackage - - loadEvents: [ - 'command-panel:toggle' - 'command-panel:toggle-preview' - 'command-panel:find-in-file' - 'command-panel:find-in-project' - 'command-panel:repeat-relative-address' - 'command-panel:repeat-relative-address-in-reverse' - 'command-panel:set-selection-as-regex-address' - ] - - instanceClass: 'command-panel/src/command-panel-view' - - onLoadEvent: (event, instance) -> - switch event.type - when 'command-panel:toggle' - instance.toggle() - when 'command-panel:toggle-preview' - instance.togglePreview() - when 'command-panel:find-in-file' - instance.attach("/") - when 'command-panel:find-in-project' - instance.attach("Xx/") - when 'command-panel:repeat-relative-address' - instance.repeatRelativeAddress() - when 'command-panel:repeat-relative-address-in-reverse' - instance.repeatRelativeAddressInReverse() - when 'command-panel:set-selection-as-regex-address' - instance.setSelectionAsLastRelativeAddress() diff --git a/src/packages/command-panel/src/command-interpreter.coffee b/src/packages/command-panel/lib/command-interpreter.coffee similarity index 95% rename from src/packages/command-panel/src/command-interpreter.coffee rename to src/packages/command-panel/lib/command-interpreter.coffee index 0c8a08d8a..c2328ceed 100644 --- a/src/packages/command-panel/src/command-interpreter.coffee +++ b/src/packages/command-panel/lib/command-interpreter.coffee @@ -6,7 +6,7 @@ class CommandInterpreter constructor: (@project) -> eval: (string, activeEditSession) -> - @parser ?= PEG.buildParser(fs.read(require.resolve 'command-panel/commands.pegjs')) + @parser ?= PEG.buildParser(fs.read(require.resolve 'command-panel/lib/commands.pegjs')) compositeCommand = @parser.parse(string) @lastRelativeAddress = compositeCommand if compositeCommand.isRelativeAddress() compositeCommand.execute(@project, activeEditSession) diff --git a/src/packages/command-panel/src/command-panel-view.coffee b/src/packages/command-panel/lib/command-panel-view.coffee similarity index 69% rename from src/packages/command-panel/src/command-panel-view.coffee rename to src/packages/command-panel/lib/command-panel-view.coffee index d27711a70..0cb7c1e1a 100644 --- a/src/packages/command-panel/src/command-panel-view.coffee +++ b/src/packages/command-panel/lib/command-panel-view.coffee @@ -1,28 +1,15 @@ {View, $$, $$$} = require 'space-pen' -CommandInterpreter = require 'command-panel/src/command-interpreter' -RegexAddress = require 'command-panel/src/commands/regex-address' -CompositeCommand = require 'command-panel/src/commands/composite-command' -PreviewList = require 'command-panel/src/preview-list' +CommandInterpreter = require './command-interpreter' +RegexAddress = require './commands/regex-address' +CompositeCommand = require './commands/composite-command' +PreviewList = require './preview-list' Editor = require 'editor' {SyntaxError} = require('pegjs').parser - _ = require 'underscore' module.exports = class CommandPanelView extends View - @activate: (rootView, state) -> - if state? - @instance = @deserialize(state, rootView) - else - @instance = new CommandPanelView(rootView) - - @deserialize: (state, rootView) -> - commandPanel = new CommandPanelView(rootView, state.history) - commandPanel.attach(state.text, focus: false) if state.visible - commandPanel.miniEditor.focus() if state.miniEditorFocused - commandPanel - - @content: (rootView) -> + @content: -> @div class: 'command-panel tool-panel', => @div outlet: 'previewCount', class: 'preview-count' @subview 'previewList', new PreviewList(rootView) @@ -36,38 +23,42 @@ class CommandPanelView extends View historyIndex: 0 maxSerializedHistorySize: 100 - initialize: (@rootView, @history) -> - @commandInterpreter = new CommandInterpreter(@rootView.project) + initialize: (state={}) -> + @commandInterpreter = new CommandInterpreter(rootView.project) - @history ?= [] - @historyIndex = @history.length - - @command 'tool-panel:unfocus', => @rootView.focus() + @command 'tool-panel:unfocus', => rootView.focus() @command 'core:close', => @detach(); false @command 'core:confirm', => @execute() @command 'core:move-up', => @navigateBackwardInHistory() @command 'core:move-down', => @navigateForwardInHistory() + rootView.command 'command-panel:toggle', => @toggle() + rootView.command 'command-panel:toggle-preview', => @togglePreview() + rootView.command 'command-panel:find-in-file', => @attach('/') + rootView.command 'command-panel:find-in-project', => @attach('Xx/') + rootView.command 'command-panel:repeat-relative-address', => @repeatRelativeAddress() + rootView.command 'command-panel:repeat-relative-address-in-reverse', => @repeatRelativeAddressInReverse() + rootView.command 'command-panel:set-selection-as-regex-address', => @setSelectionAsLastRelativeAddress() + @previewList.hide() @previewCount.hide() @errorMessages.hide() @prompt.iconSize(@miniEditor.getFontSize()) + @history = state.history ? [] + @historyIndex = @history.length + serialize: -> text: @miniEditor.getText() - visible: @hasParent() - miniEditorFocused: @miniEditor.isFocused history: @history[-@maxSerializedHistorySize..] - deactivate: -> @destroy() - destroy: -> @previewList.destroy() toggle: -> if @miniEditor.isFocused @detach() - @rootView.focus() + rootView.focus() else @attach() unless @hasParent() @miniEditor.focus() @@ -77,7 +68,7 @@ class CommandPanelView extends View @previewList.hide() @previewCount.hide() @detach() - @rootView.focus() + rootView.focus() else @attach() unless @hasParent() if @previewList.hasOperations() @@ -90,13 +81,13 @@ class CommandPanelView extends View @errorMessages.hide() focus = options.focus ? true - @rootView.vertical.append(this) + rootView.vertical.append(this) @miniEditor.focus() if focus @miniEditor.setText(text) @miniEditor.setCursorBufferPosition([0, Infinity]) detach: -> - @rootView.focus() + rootView.focus() @previewList.hide() @previewCount.hide() super @@ -108,7 +99,7 @@ class CommandPanelView extends View @errorMessages.empty() try - @commandInterpreter.eval(command, @rootView.getActiveEditSession()).done ({operationsToPreview, errorMessages}) => + @commandInterpreter.eval(command, rootView.getActiveEditSession()).done ({operationsToPreview, errorMessages}) => @history.push(command) @historyIndex = @history.length @@ -141,12 +132,12 @@ class CommandPanelView extends View @miniEditor.setText(@history[@historyIndex] or '') repeatRelativeAddress: -> - @commandInterpreter.repeatRelativeAddress(@rootView.getActiveEditSession()) + @commandInterpreter.repeatRelativeAddress(rootView.getActiveEditSession()) repeatRelativeAddressInReverse: -> - @commandInterpreter.repeatRelativeAddressInReverse(@rootView.getActiveEditSession()) + @commandInterpreter.repeatRelativeAddressInReverse(rootView.getActiveEditSession()) setSelectionAsLastRelativeAddress: -> - selection = @rootView.getActiveEditor().getSelectedText() + selection = rootView.getActiveEditor().getSelectedText() regex = _.escapeRegExp(selection) @commandInterpreter.lastRelativeAddress = new CompositeCommand([new RegexAddress(regex)]) diff --git a/src/packages/command-panel/lib/command-panel.coffee b/src/packages/command-panel/lib/command-panel.coffee new file mode 100644 index 000000000..1159e9ce4 --- /dev/null +++ b/src/packages/command-panel/lib/command-panel.coffee @@ -0,0 +1,17 @@ +CommandPanelView = require './command-panel-view' + +module.exports = + commandPanelView: null + + activate: (@state) -> + @commandPanelView = new CommandPanelView(@state) + + deactivate: -> + @commandPanelView?.destroy() + @commandPanelView = null + + serialize: -> + if @commandPanelView? + @commandPanelView.serialize() + else + @state diff --git a/src/packages/command-panel/commands.pegjs b/src/packages/command-panel/lib/commands.pegjs similarity index 70% rename from src/packages/command-panel/commands.pegjs rename to src/packages/command-panel/lib/commands.pegjs index 7536319dc..81f1b9c8e 100644 --- a/src/packages/command-panel/commands.pegjs +++ b/src/packages/command-panel/lib/commands.pegjs @@ -1,15 +1,15 @@ { - var CompositeCommand = require('command-panel/src/commands/composite-command') - var Substitution = require('command-panel/src/commands/substitution'); - var ZeroAddress = require('command-panel/src/commands/zero-address'); - var EofAddress = require('command-panel/src/commands/eof-address'); - var LineAddress = require('command-panel/src/commands/line-address'); - var AddressRange = require('command-panel/src/commands/address-range'); - var DefaultAddressRange = require('command-panel/src/commands/default-address-range'); - var CurrentSelectionAddress = require('command-panel/src/commands/current-selection-address') - var RegexAddress = require('command-panel/src/commands/regex-address') - var SelectAllMatches = require('command-panel/src/commands/select-all-matches') - var SelectAllMatchesInProject = require('command-panel/src/commands/select-all-matches-in-project') + var CompositeCommand = require('command-panel/lib/commands/composite-command') + var Substitution = require('command-panel/lib/commands/substitution'); + var ZeroAddress = require('command-panel/lib/commands/zero-address'); + var EofAddress = require('command-panel/lib/commands/eof-address'); + var LineAddress = require('command-panel/lib/commands/line-address'); + var AddressRange = require('command-panel/lib/commands/address-range'); + var DefaultAddressRange = require('command-panel/lib/commands/default-address-range'); + var CurrentSelectionAddress = require('command-panel/lib/commands/current-selection-address') + var RegexAddress = require('command-panel/lib/commands/regex-address') + var SelectAllMatches = require('command-panel/lib/commands/select-all-matches') + var SelectAllMatchesInProject = require('command-panel/lib/commands/select-all-matches-in-project') } start = _ commands:( selectAllMatchesInProject / textCommand ) { diff --git a/src/packages/command-panel/src/commands/address-range.coffee b/src/packages/command-panel/lib/commands/address-range.coffee similarity index 85% rename from src/packages/command-panel/src/commands/address-range.coffee rename to src/packages/command-panel/lib/commands/address-range.coffee index 3e4291011..8c4c976f2 100644 --- a/src/packages/command-panel/src/commands/address-range.coffee +++ b/src/packages/command-panel/lib/commands/address-range.coffee @@ -1,4 +1,4 @@ -Address = require 'command-panel/src/commands/address' +Address = require 'command-panel/lib/commands/address' Range = require 'range' module.exports = diff --git a/src/packages/command-panel/src/commands/address.coffee b/src/packages/command-panel/lib/commands/address.coffee similarity index 79% rename from src/packages/command-panel/src/commands/address.coffee rename to src/packages/command-panel/lib/commands/address.coffee index e5e4d66a9..7d0f88214 100644 --- a/src/packages/command-panel/src/commands/address.coffee +++ b/src/packages/command-panel/lib/commands/address.coffee @@ -1,5 +1,5 @@ -Command = require 'command-panel/src/commands/command' -Operation = require 'command-panel/src/operation' +Command = require './command' +Operation = require 'command-panel/lib/operation' $ = require 'jquery' module.exports = diff --git a/src/packages/command-panel/src/commands/command.coffee b/src/packages/command-panel/lib/commands/command.coffee similarity index 83% rename from src/packages/command-panel/src/commands/command.coffee rename to src/packages/command-panel/lib/commands/command.coffee index b07dd1a83..16a4e5a07 100644 --- a/src/packages/command-panel/src/commands/command.coffee +++ b/src/packages/command-panel/lib/commands/command.coffee @@ -1,5 +1,3 @@ -_ = require 'underscore' - module.exports = class Command isAddress: -> false diff --git a/src/packages/command-panel/src/commands/composite-command.coffee b/src/packages/command-panel/lib/commands/composite-command.coffee similarity index 100% rename from src/packages/command-panel/src/commands/composite-command.coffee rename to src/packages/command-panel/lib/commands/composite-command.coffee diff --git a/src/packages/command-panel/src/commands/current-selection-address.coffee b/src/packages/command-panel/lib/commands/current-selection-address.coffee similarity index 61% rename from src/packages/command-panel/src/commands/current-selection-address.coffee rename to src/packages/command-panel/lib/commands/current-selection-address.coffee index 6fd873123..2c01eb76e 100644 --- a/src/packages/command-panel/src/commands/current-selection-address.coffee +++ b/src/packages/command-panel/lib/commands/current-selection-address.coffee @@ -1,5 +1,4 @@ -Address = require 'command-panel/src/commands/address' -Range = require 'range' +Address = require './address' module.exports = class CurrentSelectionAddress extends Address diff --git a/src/packages/command-panel/src/commands/default-address-range.coffee b/src/packages/command-panel/lib/commands/default-address-range.coffee similarity index 69% rename from src/packages/command-panel/src/commands/default-address-range.coffee rename to src/packages/command-panel/lib/commands/default-address-range.coffee index 1af9c6a88..60400b321 100644 --- a/src/packages/command-panel/src/commands/default-address-range.coffee +++ b/src/packages/command-panel/lib/commands/default-address-range.coffee @@ -1,5 +1,4 @@ -Address = require 'command-panel/src/commands/address' -Range = require 'range' +Address = require './address' module.exports = class DefaultAddressRange extends Address diff --git a/src/packages/command-panel/src/commands/eof-address.coffee b/src/packages/command-panel/lib/commands/eof-address.coffee similarity index 77% rename from src/packages/command-panel/src/commands/eof-address.coffee rename to src/packages/command-panel/lib/commands/eof-address.coffee index c98dff4b3..aa13d6041 100644 --- a/src/packages/command-panel/src/commands/eof-address.coffee +++ b/src/packages/command-panel/lib/commands/eof-address.coffee @@ -1,4 +1,4 @@ -Address = require 'command-panel/src/commands/address' +Address = require './address' Range = require 'range' module.exports = diff --git a/src/packages/command-panel/src/commands/line-address.coffee b/src/packages/command-panel/lib/commands/line-address.coffee similarity index 79% rename from src/packages/command-panel/src/commands/line-address.coffee rename to src/packages/command-panel/lib/commands/line-address.coffee index 2c2e87b48..ab0f2ad92 100644 --- a/src/packages/command-panel/src/commands/line-address.coffee +++ b/src/packages/command-panel/lib/commands/line-address.coffee @@ -1,4 +1,4 @@ -Address = require 'command-panel/src/commands/address' +Address = require './address' Range = require 'range' module.exports = diff --git a/src/packages/command-panel/src/commands/regex-address.coffee b/src/packages/command-panel/lib/commands/regex-address.coffee similarity index 96% rename from src/packages/command-panel/src/commands/regex-address.coffee rename to src/packages/command-panel/lib/commands/regex-address.coffee index 58a46f749..d2828a6be 100644 --- a/src/packages/command-panel/src/commands/regex-address.coffee +++ b/src/packages/command-panel/lib/commands/regex-address.coffee @@ -1,4 +1,4 @@ -Address = require 'command-panel/src/commands/address' +Address = require './address' Range = require 'range' module.exports = diff --git a/src/packages/command-panel/src/commands/select-all-matches-in-project.coffee b/src/packages/command-panel/lib/commands/select-all-matches-in-project.coffee similarity index 83% rename from src/packages/command-panel/src/commands/select-all-matches-in-project.coffee rename to src/packages/command-panel/lib/commands/select-all-matches-in-project.coffee index 7ea56a70e..25acc5fbb 100644 --- a/src/packages/command-panel/src/commands/select-all-matches-in-project.coffee +++ b/src/packages/command-panel/lib/commands/select-all-matches-in-project.coffee @@ -1,5 +1,5 @@ -Command = require 'command-panel/src/commands/command' -Operation = require 'command-panel/src/operation' +Command = require './command' +Operation = require 'command-panel/lib/operation' $ = require 'jquery' module.exports = diff --git a/src/packages/command-panel/src/commands/select-all-matches.coffee b/src/packages/command-panel/lib/commands/select-all-matches.coffee similarity index 83% rename from src/packages/command-panel/src/commands/select-all-matches.coffee rename to src/packages/command-panel/lib/commands/select-all-matches.coffee index 78ebe9b8e..ad1b0bca9 100644 --- a/src/packages/command-panel/src/commands/select-all-matches.coffee +++ b/src/packages/command-panel/lib/commands/select-all-matches.coffee @@ -1,5 +1,5 @@ -Command = require 'command-panel/src/commands/command' -Operation = require 'command-panel/src/operation' +Command = require './command' +Operation = require 'command-panel/lib/operation' $ = require 'jquery' module.exports = diff --git a/src/packages/command-panel/src/commands/substitution.coffee b/src/packages/command-panel/lib/commands/substitution.coffee similarity index 87% rename from src/packages/command-panel/src/commands/substitution.coffee rename to src/packages/command-panel/lib/commands/substitution.coffee index 26231bd01..66a621bc2 100644 --- a/src/packages/command-panel/src/commands/substitution.coffee +++ b/src/packages/command-panel/lib/commands/substitution.coffee @@ -1,5 +1,5 @@ -Command = require 'command-panel/src/commands/command' -Operation = require 'command-panel/src/operation' +Command = require './command' +Operation = require 'command-panel/lib/operation' $ = require 'jquery' module.exports = diff --git a/src/packages/command-panel/src/commands/zero-address.coffee b/src/packages/command-panel/lib/commands/zero-address.coffee similarity index 72% rename from src/packages/command-panel/src/commands/zero-address.coffee rename to src/packages/command-panel/lib/commands/zero-address.coffee index 313f1cd5b..80928b29b 100644 --- a/src/packages/command-panel/src/commands/zero-address.coffee +++ b/src/packages/command-panel/lib/commands/zero-address.coffee @@ -1,4 +1,4 @@ -Address = require 'command-panel/src/commands/address' +Address = require './address' Range = require 'range' module.exports = diff --git a/src/packages/command-panel/src/operation.coffee b/src/packages/command-panel/lib/operation.coffee similarity index 77% rename from src/packages/command-panel/src/operation.coffee rename to src/packages/command-panel/lib/operation.coffee index f3fe79a98..52e45d544 100644 --- a/src/packages/command-panel/src/operation.coffee +++ b/src/packages/command-panel/lib/operation.coffee @@ -1,23 +1,21 @@ -{$$$} = require 'space-pen' - module.exports = class Operation constructor: ({@project, @buffer, bufferRange, @newText, @preserveSelection, @errorMessage}) -> @buffer.retain() - @anchorRange = @buffer.addAnchorRange(bufferRange) + @marker = @buffer.markRange(bufferRange) getPath: -> @project.relativize(@buffer.getPath()) getBufferRange: -> - @anchorRange.getBufferRange() + @buffer.getMarkerRange(@marker) execute: (editSession) -> @buffer.change(@getBufferRange(), @newText) if @newText @getBufferRange() unless @preserveSelection preview: -> - range = @anchorRange.getBufferRange() + range = @buffer.getMarkerRange(@marker) line = @buffer.lineForRow(range.start.row) prefix = line[0...range.start.column] match = line[range.start.column...range.end.column] @@ -26,5 +24,5 @@ class Operation {prefix, suffix, match, range} destroy: -> + @buffer.destroyMarker(@marker) @buffer.release() - @anchorRange.destroy() diff --git a/src/packages/command-panel/src/preview-list.coffee b/src/packages/command-panel/lib/preview-list.coffee similarity index 100% rename from src/packages/command-panel/src/preview-list.coffee rename to src/packages/command-panel/lib/preview-list.coffee diff --git a/src/packages/command-panel/package.cson b/src/packages/command-panel/package.cson new file mode 100644 index 000000000..e5d8cf032 --- /dev/null +++ b/src/packages/command-panel/package.cson @@ -0,0 +1,10 @@ +'main': 'lib/command-panel' +'activationEvents': [ + 'command-panel:toggle' + 'command-panel:toggle-preview' + 'command-panel:find-in-file' + 'command-panel:find-in-project' + 'command-panel:repeat-relative-address' + 'command-panel:repeat-relative-address-in-reverse' + 'command-panel:set-selection-as-regex-address' +] diff --git a/src/packages/command-panel/spec/command-interpreter-spec.coffee b/src/packages/command-panel/spec/command-interpreter-spec.coffee index 923541e23..289d87709 100644 --- a/src/packages/command-panel/spec/command-interpreter-spec.coffee +++ b/src/packages/command-panel/spec/command-interpreter-spec.coffee @@ -1,10 +1,12 @@ -CommandInterpreter = require 'command-panel/src/command-interpreter' +CommandInterpreter = require 'command-panel/lib/command-interpreter' Project = require 'project' Buffer = require 'buffer' EditSession = require 'edit-session' +_ = require 'underscore' + describe "CommandInterpreter", -> - [project, interpreter, editSession, buffer, anchorCountBefore] = [] + [project, interpreter, editSession, buffer] = [] beforeEach -> project = new Project(fixturesProject.resolve('dir/')) @@ -14,7 +16,7 @@ describe "CommandInterpreter", -> afterEach -> editSession?.destroy() - expect(buffer.getAnchors().length).toBe 0 + expect(buffer.getMarkerCount()).toBe 0 describe "addresses", -> beforeEach -> diff --git a/src/packages/command-panel/spec/command-panel-spec.coffee b/src/packages/command-panel/spec/command-panel-spec.coffee index c0d47fe45..99f886b03 100644 --- a/src/packages/command-panel/spec/command-panel-spec.coffee +++ b/src/packages/command-panel/spec/command-panel-spec.coffee @@ -1,19 +1,19 @@ RootView = require 'root-view' -CommandPanelView = require 'command-panel/src/command-panel-view' +CommandPanelView = require 'command-panel/lib/command-panel-view' _ = require 'underscore' describe "CommandPanel", -> - [rootView, editor, buffer, commandPanel, project, CommandPanel] = [] + [editor, buffer, commandPanel, project, CommandPanel] = [] beforeEach -> - rootView = new RootView + new RootView rootView.open(require.resolve 'fixtures/sample.js') rootView.enableKeymap() project = rootView.project editor = rootView.getActiveEditor() buffer = editor.activeEditSession.buffer - CommandPanel = atom.loadPackage('command-panel') - commandPanel = CommandPanel.getInstance() + commandPanelMain = atom.loadPackage('command-panel', activateImmediately: true).packageMain + commandPanel = commandPanelMain.commandPanelView commandPanel.history = [] commandPanel.historyIndex = 0 @@ -21,7 +21,7 @@ describe "CommandPanel", -> rootView.deactivate() describe "serialization", -> - it "preserves the command panel's mini-editor text, visibility, focus, and history across reloads", -> + it "preserves the command panel's history across reloads", -> rootView.attachToDom() rootView.trigger 'command-panel:toggle' expect(commandPanel.miniEditor.isFocused).toBeTruthy() @@ -31,29 +31,20 @@ describe "CommandPanel", -> expect(commandPanel.historyIndex).toBe(1) rootView.trigger 'command-panel:toggle' expect(commandPanel.miniEditor.isFocused).toBeTruthy() - commandPanel.miniEditor.insertText 'abc' - rootView2 = RootView.deserialize(rootView.serialize()) - rootView.deactivate() - rootView2.attachToDom() - commandPanel = rootView2.activatePackage('command-panel', CommandPanel).getInstance() - expect(rootView2.find('.command-panel')).toExist() - expect(commandPanel.miniEditor.getText()).toBe 'abc' - expect(commandPanel.miniEditor.isFocused).toBeTruthy() + rootViewState = rootView.serialize() + rootView.deactivate() + RootView.deserialize(rootViewState).attachToDom() + atom.loadPackage('command-panel') + + expect(rootView.find('.command-panel')).not.toExist() + rootView.trigger 'command-panel:toggle' + expect(rootView.find('.command-panel')).toExist() + commandPanel = rootView.find('.command-panel').view() expect(commandPanel.history.length).toBe(1) expect(commandPanel.history[0]).toBe('/.') expect(commandPanel.historyIndex).toBe(1) - rootView2.focus() - expect(commandPanel.miniEditor.isFocused).toBeFalsy() - rootView3 = RootView.deserialize(rootView2.serialize()) - rootView2.deactivate() - rootView3.attachToDom() - commandPanel = rootView3.activatePackage('command-panel', CommandPanel).getInstance() - - expect(commandPanel.miniEditor.isFocused).toBeFalsy() - rootView3.deactivate() - it "only retains the configured max serialized history size", -> rootView.attachToDom() @@ -67,18 +58,18 @@ describe "CommandPanel", -> expect(commandPanel.history[2]).toBe('/test3') expect(commandPanel.historyIndex).toBe(3) - rootView2 = RootView.deserialize(rootView.serialize()) + rootViewState = rootView.serialize() rootView.deactivate() - rootView2.attachToDom() + RootView.deserialize(rootViewState).attachToDom() + atom.loadPackage('command-panel') + rootView.trigger 'command-panel:toggle' - commandPanel = rootView2.activatePackage('command-panel', CommandPanel).getInstance() + commandPanel = rootView.find('.command-panel').view() expect(commandPanel.history.length).toBe(2) expect(commandPanel.history[0]).toBe('/test2') expect(commandPanel.history[1]).toBe('/test3') expect(commandPanel.historyIndex).toBe(2) - rootView2.deactivate() - describe "when core:close is triggered on the command panel", -> it "detaches the command panel, focuses the RootView and does not bubble the core:close event", -> commandPanel.attach() diff --git a/src/packages/editor-stats/index.coffee b/src/packages/editor-stats/index.coffee deleted file mode 100644 index e743c0f18..000000000 --- a/src/packages/editor-stats/index.coffee +++ /dev/null @@ -1,16 +0,0 @@ -DeferredAtomPackage = require 'deferred-atom-package' -Stats = require './src/stats' - -module.exports = -class EditorStats extends DeferredAtomPackage - loadEvents: ['editor-stats:toggle'] - instanceClass: 'editor-stats/src/editor-stats-view' - stats: new Stats - - activate: (rootView) -> - super - - rootView.on 'keydown', => @stats.track() - rootView.on 'mouseup', => @stats.track() - - onLoadEvent: (event, instance) -> instance.toggle(@stats) diff --git a/src/packages/editor-stats/src/editor-stats-view.coffee b/src/packages/editor-stats/lib/editor-stats-view.coffee similarity index 91% rename from src/packages/editor-stats/src/editor-stats-view.coffee rename to src/packages/editor-stats/lib/editor-stats-view.coffee index aea6ca303..c86ab18d8 100644 --- a/src/packages/editor-stats/src/editor-stats-view.coffee +++ b/src/packages/editor-stats/lib/editor-stats-view.coffee @@ -5,10 +5,10 @@ $ = require 'jquery' module.exports = class EditorStatsView extends ScrollView - @activate: (rootView, state) -> - @instance = new EditorStatsView(rootView) + @activate: -> + new EditorStatsView - @content: (rootView) -> + @content: -> @div class: 'editor-stats-wrapper', tabindex: -1, => @div class: 'editor-stats', outlet: 'editorStats' @@ -17,7 +17,7 @@ class EditorStatsView extends ScrollView pb: 3 pr: 25 - initialize: (@rootView) -> + initialize: -> super resizer = => @@ -30,7 +30,7 @@ class EditorStatsView extends ScrollView @editorStats.empty() @x ?= d3.scale.ordinal().domain d3.range(@stats.hours * 60) @y ?= d3.scale.linear() - w = @rootView.vertical.width() + w = rootView.vertical.width() h = @height() data = d3.entries @stats.eventLog max = d3.max data, (d) -> d.value @@ -94,10 +94,10 @@ class EditorStatsView extends ScrollView @attach() attach: -> - @rootView.vertical.append(@) + rootView.vertical.append(this) @draw() detach: -> - super() + super clearInterval(@updateInterval) - @rootView.focus() + rootView.focus() diff --git a/src/packages/editor-stats/lib/editor-stats.coffee b/src/packages/editor-stats/lib/editor-stats.coffee new file mode 100644 index 000000000..bdfdaa6b1 --- /dev/null +++ b/src/packages/editor-stats/lib/editor-stats.coffee @@ -0,0 +1,19 @@ +StatsTracker = require './stats-tracker' + +module.exports = + stats: null + editorStatsView: null + + activate: -> + @stats = new StatsTracker() + rootView.command 'editor-stats:toggle', => @createView().toggle(@stats) + + deactivate: -> + @editorStatsView = null + @stats = null + + createView: -> + unless @editorStatsView + EditorStatsView = require 'editor-stats/lib/editor-stats-view' + @editorStatsView = new EditorStatsView() + @editorStatsView diff --git a/src/packages/editor-stats/src/stats.coffee b/src/packages/editor-stats/lib/stats-tracker.coffee similarity index 85% rename from src/packages/editor-stats/src/stats.coffee rename to src/packages/editor-stats/lib/stats-tracker.coffee index 5e9c6700d..d7cc8d1d8 100644 --- a/src/packages/editor-stats/src/stats.coffee +++ b/src/packages/editor-stats/lib/stats-tracker.coffee @@ -1,5 +1,5 @@ module.exports = -class Stats +class StatsTracker startDate: new Date hours: 6 eventLog: [] @@ -12,6 +12,9 @@ class Stats while date < future @eventLog[@time(date)] = 0 + rootView.on 'keydown', => @track() + rootView.on 'mouseup', => @track() + clear: -> @eventLog = [] diff --git a/src/packages/editor-stats/package.cson b/src/packages/editor-stats/package.cson new file mode 100644 index 000000000..258a141f9 --- /dev/null +++ b/src/packages/editor-stats/package.cson @@ -0,0 +1 @@ +'main': 'lib/editor-stats' diff --git a/src/packages/editor-stats/spec/editor-stats-spec.coffee b/src/packages/editor-stats/spec/editor-stats-spec.coffee index ca274a252..0ba0b2032 100644 --- a/src/packages/editor-stats/spec/editor-stats-spec.coffee +++ b/src/packages/editor-stats/spec/editor-stats-spec.coffee @@ -1,9 +1,9 @@ $ = require 'jquery' RootView = require 'root-view' -EditorStats = require 'editor-stats/src/editor-stats-view' +EditorStats = require 'editor-stats/lib/editor-stats-view' describe "EditorStats", -> - [rootView, editorStats, time] = [] + [editorStats, time] = [] simulateKeyUp = (key) -> e = $.Event "keydown", keyCode: key.charCodeAt(0) @@ -23,10 +23,7 @@ describe "EditorStats", -> mins = if mins == 60 then '01' else mins + 1 time = "#{hours}:#{mins}" - editorStatsPackage = atom.loadPackage('editor-stats') - editorStatsPackage.getInstance() - editorStats = editorStatsPackage.stats - editorStats.clear() + editorStats = atom.loadPackage('editor-stats').packageMain.stats afterEach -> rootView.deactivate() diff --git a/src/packages/editor-stats/src/stats-tracker.coffee b/src/packages/editor-stats/src/stats-tracker.coffee new file mode 100644 index 000000000..d7cc8d1d8 --- /dev/null +++ b/src/packages/editor-stats/src/stats-tracker.coffee @@ -0,0 +1,32 @@ +module.exports = +class StatsTracker + startDate: new Date + hours: 6 + eventLog: [] + + constructor: -> + date = new Date(@startDate) + future = new Date(date.getTime() + (36e5 * @hours)) + @eventLog[@time(date)] = 0 + + while date < future + @eventLog[@time(date)] = 0 + + rootView.on 'keydown', => @track() + rootView.on 'mouseup', => @track() + + clear: -> + @eventLog = [] + + track: -> + date = new Date + times = @time date + @eventLog[times] ?= 0 + @eventLog[times] += 1 + @eventLog.shift() if @eventLog.length > (@hours * 60) + + time: (date) -> + date.setTime(date.getTime() + 6e4) + hour = date.getHours() + minute = date.getMinutes() + "#{hour}:#{minute}" diff --git a/src/packages/fuzzy-finder/index.coffee b/src/packages/fuzzy-finder/index.coffee deleted file mode 100644 index 2f8cf6783..000000000 --- a/src/packages/fuzzy-finder/index.coffee +++ /dev/null @@ -1,32 +0,0 @@ -DeferredAtomPackage = require 'deferred-atom-package' -LoadPathsTask = require './src/load-paths-task' - -module.exports = -class FuzzyFinder extends DeferredAtomPackage - loadEvents: [ - 'fuzzy-finder:toggle-file-finder' - 'fuzzy-finder:toggle-buffer-finder' - 'fuzzy-finder:find-under-cursor' - ] - - instanceClass: 'fuzzy-finder/src/fuzzy-finder-view' - - activate: (rootView) -> - super - - if rootView.project.getPath()? - callback = (paths) => @projectPaths = paths - new LoadPathsTask(rootView, callback).start() - - onLoadEvent: (event, instance) -> - if @projectPaths? and not instance.projectPaths? - instance.projectPaths = @projectPaths - instance.reloadProjectPaths = false - - switch event.type - when 'fuzzy-finder:toggle-file-finder' - instance.toggleFileFinder() - when 'fuzzy-finder:toggle-buffer-finder' - instance.toggleBufferFinder() - when 'fuzzy-finder:find-under-cursor' - instance.findUnderCursor() diff --git a/src/packages/fuzzy-finder/src/fuzzy-finder-view.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee similarity index 84% rename from src/packages/fuzzy-finder/src/fuzzy-finder-view.coffee rename to src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee index 17165a8c1..ad1249ffb 100644 --- a/src/packages/fuzzy-finder/src/fuzzy-finder-view.coffee +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee @@ -3,15 +3,12 @@ SelectList = require 'select-list' _ = require 'underscore' $ = require 'jquery' fs = require 'fs' -LoadPathsTask = require 'fuzzy-finder/src/load-paths-task' +LoadPathsTask = require './load-paths-task' module.exports = class FuzzyFinderView extends SelectList filenameRegex: /[\w\.\-\/\\]+/ - @activate: (rootView) -> - @instance = new FuzzyFinderView(rootView) - @viewClass: -> [super, 'fuzzy-finder', 'overlay', 'from-top'].join(' ') @@ -20,7 +17,7 @@ class FuzzyFinderView extends SelectList projectPaths: null reloadProjectPaths: true - initialize: (@rootView) -> + initialize: -> super @subscribe $(window), 'focus', => @reloadProjectPaths = true @@ -54,15 +51,15 @@ class FuzzyFinderView extends SelectList @span " - #{folder}/", class: 'directory' openPath: (path) -> - @rootView.open(path, {@allowActiveEditorChange}) if path + rootView.open(path, {@allowActiveEditorChange}) if path splitOpenPath: (fn) -> path = @getSelectedElement() return unless path - editor = @rootView.getActiveEditor() + editor = rootView.getActiveEditor() if editor - fn(editor, @rootView.project.buildEditSessionForPath(path)) + fn(editor, rootView.project.buildEditSessionForPath(path)) else @openPath(path) @@ -79,7 +76,7 @@ class FuzzyFinderView extends SelectList if @hasParent() @cancel() else - return unless @rootView.project.getPath()? + return unless rootView.project.getPath()? @allowActiveEditorChange = false @populateProjectPaths() @attach() @@ -96,9 +93,9 @@ class FuzzyFinderView extends SelectList if @hasParent() @cancel() else - return unless @rootView.project.getPath()? + return unless rootView.project.getPath()? @allowActiveEditorChange = false - editor = @rootView.getActiveEditor() + editor = rootView.getActiveEditor() currentWord = editor.getWordUnderCursor(wordRegex: @filenameRegex) if currentWord.length == 0 @@ -110,7 +107,7 @@ class FuzzyFinderView extends SelectList @attach() @setError("No files match '#{currentWord}'") else if paths.length == 1 - @rootView.open(paths[0]) + rootView.open(paths[0]) else @attach() @miniEditor.setText(currentWord) @@ -142,16 +139,21 @@ class FuzzyFinderView extends SelectList @setArray(listedItems) options.done(listedItems) if options.done? - @loadPathsTask = new LoadPathsTask(@rootView, callback) + @loadPathsTask = new LoadPathsTask(callback) @loadPathsTask.start() populateOpenBufferPaths: -> - @paths = @rootView.getOpenBufferPaths().map (path) => - @rootView.project.relativize(path) + @paths = rootView.getOpenBufferPaths().map (path) => + rootView.project.relativize(path) @setArray(@paths) + detach: -> + super + + @loadPathsTask?.terminate() + attach: -> super - @rootView.append(this) + rootView.append(this) @miniEditor.focus() diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee new file mode 100644 index 000000000..2bdd340a1 --- /dev/null +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee @@ -0,0 +1,32 @@ +module.exports = + projectPaths: null + fuzzyFinderView: null + + activate: -> + rootView.command 'fuzzy-finder:toggle-file-finder', => + @createView().toggleFileFinder() + rootView.command 'fuzzy-finder:toggle-buffer-finder', => + @createView().toggleBufferFinder() + rootView.command 'fuzzy-finder:find-under-cursor', => + @createView().findUnderCursor() + + if rootView.project.getPath()? + LoadPathsTask = require 'fuzzy-finder/lib/load-paths-task' + @loadPathsTask = new LoadPathsTask((paths) => @projectPaths = paths) + @loadPathsTask.start() + + deactivate: -> + @loadPathsTask?.terminate() + @fuzzyFinderView?.cancel() + @fuzzyFinderView = null + @projectPaths = null + @fuzzyFinderView = null + + createView: -> + unless @fuzzyFinderView + FuzzyFinderView = require 'fuzzy-finder/lib/fuzzy-finder-view' + @fuzzyFinderView = new FuzzyFinderView() + if @projectPaths? and not @fuzzyFinderView.projectPaths? + @fuzzyFinderView.projectPaths = @projectPaths + @fuzzyFinderView.reloadProjectPaths = false + @fuzzyFinderView diff --git a/src/packages/fuzzy-finder/src/load-paths-handler.coffee b/src/packages/fuzzy-finder/lib/load-paths-handler.coffee similarity index 82% rename from src/packages/fuzzy-finder/src/load-paths-handler.coffee rename to src/packages/fuzzy-finder/lib/load-paths-handler.coffee index 98c439d3d..723f302d1 100644 --- a/src/packages/fuzzy-finder/src/load-paths-handler.coffee +++ b/src/packages/fuzzy-finder/lib/load-paths-handler.coffee @@ -1,11 +1,13 @@ fs = require 'fs' _ = require 'underscore' -Git = require 'git' module.exports = loadPaths: (rootPath, ignoredNames, excludeGitIgnoredPaths) -> + if excludeGitIgnoredPaths + Git = require 'git' + repo = Git.open(rootPath, refreshIndexOnFocus: false) + paths = [] - repo = Git.open(rootPath, refreshIndexOnFocus: false) if excludeGitIgnoredPaths isIgnored = (path) -> for segment in path.split('/') return true if _.contains(ignoredNames, segment) @@ -15,5 +17,7 @@ module.exports = onDirectory = (path) -> not isIgnored(path) fs.traverseTree(rootPath, onFile, onDirectory) + repo?.destroy() + callTaskMethod('pathsLoaded', paths) diff --git a/src/packages/fuzzy-finder/src/load-paths-task.coffee b/src/packages/fuzzy-finder/lib/load-paths-task.coffee similarity index 77% rename from src/packages/fuzzy-finder/src/load-paths-task.coffee rename to src/packages/fuzzy-finder/lib/load-paths-task.coffee index 64bf095b5..254c3673e 100644 --- a/src/packages/fuzzy-finder/src/load-paths-task.coffee +++ b/src/packages/fuzzy-finder/lib/load-paths-task.coffee @@ -2,14 +2,14 @@ Task = require 'task' module.exports = class LoadPathsTask extends Task - constructor: (@rootView, @callback)-> - super('fuzzy-finder/src/load-paths-handler') + constructor: (@callback) -> + super('fuzzy-finder/lib/load-paths-handler') started: -> ignoredNames = config.get('fuzzyFinder.ignoredNames') ? [] ignoredNames = ignoredNames.concat(config.get('core.ignoredNames') ? []) excludeGitIgnoredPaths = config.get('core.hideGitIgnoredFiles') - rootPath = @rootView.project.getPath() + rootPath = rootView.project.getPath() @callWorkerMethod('loadPaths', rootPath, ignoredNames, excludeGitIgnoredPaths) pathsLoaded: (paths) -> diff --git a/src/packages/fuzzy-finder/package.cson b/src/packages/fuzzy-finder/package.cson new file mode 100644 index 000000000..d259d625b --- /dev/null +++ b/src/packages/fuzzy-finder/package.cson @@ -0,0 +1 @@ +'main': 'lib/fuzzy-finder' diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index 76f8bd476..1a4dcd742 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -1,21 +1,20 @@ RootView = require 'root-view' -FuzzyFinder = require 'fuzzy-finder/src/fuzzy-finder-view' -LoadPathsTask = require 'fuzzy-finder/src/load-paths-task' +FuzzyFinder = require 'fuzzy-finder/lib/fuzzy-finder-view' +LoadPathsTask = require 'fuzzy-finder/lib/load-paths-task' $ = require 'jquery' {$$} = require 'space-pen' fs = require 'fs' describe 'FuzzyFinder', -> - [rootView, finder] = [] + [finderView] = [] beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) rootView.enableKeymap() - atom.loadPackage("fuzzy-finder").getInstance() - finder = FuzzyFinder.instance + finderView = atom.loadPackage("fuzzy-finder").packageMain.createView() afterEach -> - rootView.remove() + rootView.deactivate() describe "file-finder behavior", -> describe "toggling", -> @@ -26,12 +25,13 @@ describe 'FuzzyFinder', -> rootView.find('.editor').trigger 'editor:split-right' [editor1, editor2] = rootView.find('.editor').map -> $(this).view() + expect(rootView.find('.fuzzy-finder')).not.toExist() rootView.trigger 'fuzzy-finder:toggle-file-finder' expect(rootView.find('.fuzzy-finder')).toExist() - expect(finder.miniEditor.isFocused).toBeTruthy() + expect(finderView.miniEditor.isFocused).toBeTruthy() expect(editor1.isFocused).toBeFalsy() expect(editor2.isFocused).toBeFalsy() - finder.miniEditor.insertText('this should not show up next time we toggle') + finderView.miniEditor.insertText('this should not show up next time we toggle') rootView.trigger 'fuzzy-finder:toggle-file-finder' expect(editor1.isFocused).toBeFalsy() @@ -39,27 +39,27 @@ describe 'FuzzyFinder', -> expect(rootView.find('.fuzzy-finder')).not.toExist() rootView.trigger 'fuzzy-finder:toggle-file-finder' - expect(finder.miniEditor.getText()).toBe '' + expect(finderView.miniEditor.getText()).toBe '' it "shows all relative file paths for the current project and selects the first", -> rootView.attachToDom() - finder.maxItems = Infinity + finderView.maxItems = Infinity rootView.trigger 'fuzzy-finder:toggle-file-finder' paths = null - expect(finder.find(".loading")).toBeVisible() - expect(finder.find(".loading")).toHaveText "Indexing..." + expect(finderView.find(".loading")).toBeVisible() + expect(finderView.find(".loading")).toHaveText "Indexing..." waitsFor "all project paths to load", 5000, -> - if finder.projectPaths?.length > 0 - paths = finder.projectPaths + if finderView.projectPaths?.length > 0 + paths = finderView.projectPaths true runs -> - expect(finder.list.children('li').length).toBe paths.length + expect(finderView.list.children('li').length).toBe paths.length for path in paths - expect(finder.list.find("li:contains(#{fs.base(path)})")).toExist() - expect(finder.list.children().first()).toHaveClass 'selected' - expect(finder.find(".loading")).not.toBeVisible() + expect(finderView.list.find("li:contains(#{fs.base(path)})")).toExist() + expect(finderView.list.children().first()).toHaveClass 'selected' + expect(finderView.find(".loading")).not.toBeVisible() describe "when root view's project has no path", -> beforeEach -> @@ -78,10 +78,10 @@ describe 'FuzzyFinder', -> expect(rootView.getActiveEditor()).toBe editor2 rootView.trigger 'fuzzy-finder:toggle-file-finder' - finder.confirmed('dir/a') + finderView.confirmed('dir/a') expectedPath = fixturesProject.resolve('dir/a') - expect(finder.hasParent()).toBeFalsy() + expect(finderView.hasParent()).toBeFalsy() expect(editor1.getPath()).not.toBe expectedPath expect(editor2.getPath()).toBe expectedPath expect(editor2.isFocused).toBeTruthy() @@ -91,12 +91,12 @@ describe 'FuzzyFinder', -> rootView.attachToDom() path = rootView.getActiveEditor().getPath() rootView.trigger 'fuzzy-finder:toggle-file-finder' - finder.confirmed('dir/this/is/not/a/file.txt') - expect(finder.hasParent()).toBeTruthy() + finderView.confirmed('dir/this/is/not/a/file.txt') + expect(finderView.hasParent()).toBeTruthy() expect(rootView.getActiveEditor().getPath()).toBe path - expect(finder.find('.error').text().length).toBeGreaterThan 0 + expect(finderView.find('.error').text().length).toBeGreaterThan 0 advanceClock(2000) - expect(finder.find('.error').text().length).toBe 0 + expect(finderView.find('.error').text().length).toBe 0 describe "buffer-finder behavior", -> describe "toggling", -> @@ -113,7 +113,7 @@ describe 'FuzzyFinder', -> rootView.trigger 'fuzzy-finder:toggle-buffer-finder' expect(rootView.find('.fuzzy-finder')).toExist() expect(rootView.find('.fuzzy-finder input:focus')).toExist() - finder.miniEditor.insertText('this should not show up next time we toggle') + finderView.miniEditor.insertText('this should not show up next time we toggle') rootView.trigger 'fuzzy-finder:toggle-buffer-finder' expect(editor1.isFocused).toBeFalsy() @@ -121,14 +121,14 @@ describe 'FuzzyFinder', -> expect(rootView.find('.fuzzy-finder')).not.toExist() rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - expect(finder.miniEditor.getText()).toBe '' + expect(finderView.miniEditor.getText()).toBe '' it "lists the paths of the current open buffers", -> rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - expect(finder.list.children('li').length).toBe 2 - expect(finder.list.find("li:contains(sample.js)")).toExist() - expect(finder.list.find("li:contains(sample.txt)")).toExist() - expect(finder.list.children().first()).toHaveClass 'selected' + expect(finderView.list.children('li').length).toBe 2 + expect(finderView.list.find("li:contains(sample.js)")).toExist() + expect(finderView.list.find("li:contains(sample.txt)")).toExist() + expect(finderView.list.children().first()).toHaveClass 'selected' describe "when the active editor only contains edit sessions for anonymous buffers", -> it "does not open", -> @@ -162,9 +162,9 @@ describe 'FuzzyFinder', -> describe "when there is an edit session for the confirmed path in the active editor", -> it "switches the active editor to the edit session for the selected path", -> expectedPath = fixturesProject.resolve('sample.txt') - finder.confirmed('sample.txt') + finderView.confirmed('sample.txt') - expect(finder.hasParent()).toBeFalsy() + expect(finderView.hasParent()).toBeFalsy() expect(editor1.getPath()).not.toBe expectedPath expect(editor2.getPath()).toBe expectedPath expect(editor2.isFocused).toBeTruthy() @@ -178,9 +178,9 @@ describe 'FuzzyFinder', -> expect(rootView.getActiveEditor()).toBe editor1 expectedPath = fixturesProject.resolve('sample.txt') - finder.confirmed('sample.txt') + finderView.confirmed('sample.txt') - expect(finder.hasParent()).toBeFalsy() + expect(finderView.hasParent()).toBeFalsy() expect(editor1.getPath()).not.toBe expectedPath expect(editor2.getPath()).toBe expectedPath expect(editor2.isFocused).toBeTruthy() @@ -194,15 +194,15 @@ describe 'FuzzyFinder', -> activeEditor.focus() rootView.trigger 'fuzzy-finder:toggle-file-finder' - expect(finder.hasParent()).toBeTruthy() + expect(finderView.hasParent()).toBeTruthy() expect(activeEditor.isFocused).toBeFalsy() - expect(finder.miniEditor.isFocused).toBeTruthy() + expect(finderView.miniEditor.isFocused).toBeTruthy() - finder.cancel() + finderView.cancel() - expect(finder.hasParent()).toBeFalsy() + expect(finderView.hasParent()).toBeFalsy() expect(activeEditor.isFocused).toBeTruthy() - expect(finder.miniEditor.isFocused).toBeFalsy() + expect(finderView.miniEditor.isFocused).toBeFalsy() describe "when no editors are open", -> it "detaches the finder and focuses the previously focused element", -> @@ -210,15 +210,15 @@ describe 'FuzzyFinder', -> rootView.getActiveEditor().destroyActiveEditSession() rootView.trigger 'fuzzy-finder:toggle-file-finder' - expect(finder.hasParent()).toBeTruthy() + expect(finderView.hasParent()).toBeTruthy() expect(rootView.isFocused).toBeFalsy() - expect(finder.miniEditor.isFocused).toBeTruthy() + expect(finderView.miniEditor.isFocused).toBeTruthy() - finder.cancel() + finderView.cancel() - expect(finder.hasParent()).toBeFalsy() + expect(finderView.hasParent()).toBeFalsy() expect($(document.activeElement).view()).toBe rootView - expect(finder.miniEditor.isFocused).toBeFalsy() + expect(finderView.miniEditor.isFocused).toBeFalsy() describe "cached file paths", -> it "caches file paths after first time", -> @@ -226,26 +226,26 @@ describe 'FuzzyFinder', -> rootView.trigger 'fuzzy-finder:toggle-file-finder' waitsFor -> - finder.list.children('li').length > 0 + finderView.list.children('li').length > 0 runs -> - expect(finder.loadPathsTask.start).toHaveBeenCalled() - finder.loadPathsTask.start.reset() + expect(finderView.loadPathsTask.start).toHaveBeenCalled() + finderView.loadPathsTask.start.reset() rootView.trigger 'fuzzy-finder:toggle-file-finder' rootView.trigger 'fuzzy-finder:toggle-file-finder' waitsFor -> - finder.list.children('li').length > 0 + finderView.list.children('li').length > 0 runs -> - expect(finder.loadPathsTask.start).not.toHaveBeenCalled() + expect(finderView.loadPathsTask.start).not.toHaveBeenCalled() it "doesn't cache buffer paths", -> spyOn(rootView, "getOpenBufferPaths").andCallThrough() rootView.trigger 'fuzzy-finder:toggle-buffer-finder' waitsFor -> - finder.list.children('li').length > 0 + finderView.list.children('li').length > 0 runs -> expect(rootView.getOpenBufferPaths).toHaveBeenCalled() @@ -254,7 +254,7 @@ describe 'FuzzyFinder', -> rootView.trigger 'fuzzy-finder:toggle-buffer-finder' waitsFor -> - finder.list.children('li').length > 0 + finderView.list.children('li').length > 0 runs -> expect(rootView.getOpenBufferPaths).toHaveBeenCalled() @@ -264,27 +264,27 @@ describe 'FuzzyFinder', -> rootView.trigger 'fuzzy-finder:toggle-file-finder' waitsFor -> - finder.list.children('li').length > 0 + finderView.list.children('li').length > 0 runs -> - expect(finder.loadPathsTask.start).toHaveBeenCalled() - finder.loadPathsTask.start.reset() + expect(finderView.loadPathsTask.start).toHaveBeenCalled() + finderView.loadPathsTask.start.reset() $(window).trigger 'focus' rootView.trigger 'fuzzy-finder:toggle-file-finder' rootView.trigger 'fuzzy-finder:toggle-file-finder' - expect(finder.loadPathsTask.start).toHaveBeenCalled() + expect(finderView.loadPathsTask.start).toHaveBeenCalled() describe "path ignoring", -> it "ignores paths that match entries in config.fuzzyFinder.ignoredNames", -> config.set("fuzzyFinder.ignoredNames", ["tree-view.js"]) rootView.trigger 'fuzzy-finder:toggle-file-finder' - finder.maxItems = Infinity + finderView.maxItems = Infinity waitsFor -> - finder.list.children('li').length > 0 + finderView.list.children('li').length > 0 runs -> - expect(finder.list.find("li:contains(tree-view.js)")).not.toExist() + expect(finderView.list.find("li:contains(tree-view.js)")).not.toExist() describe "fuzzy find by content under cursor", -> editor = null @@ -298,10 +298,10 @@ describe 'FuzzyFinder', -> rootView.trigger 'fuzzy-finder:find-under-cursor' waitsFor -> - finder.list.children('li').length > 0 + finderView.list.children('li').length > 0 runs -> - expect(finder).toBeVisible() + expect(finderView).toBeVisible() expect(rootView.find('.fuzzy-finder input:focus')).toExist() it "opens a file directly when there is a single match", -> @@ -316,7 +316,7 @@ describe 'FuzzyFinder', -> openedPath != null runs -> - expect(finder).not.toBeVisible() + expect(finderView).not.toBeVisible() expect(openedPath).toBe "sample.txt" it "displays error when the word under the cursor doesn't match any files", -> @@ -326,10 +326,10 @@ describe 'FuzzyFinder', -> rootView.trigger 'fuzzy-finder:find-under-cursor' waitsFor -> - finder.is(':visible') + finderView.is(':visible') runs -> - expect(finder.find('.error').text().length).toBeGreaterThan 0 + expect(finderView.find('.error').text().length).toBeGreaterThan 0 it "displays error when there is no word under the cursor", -> editor.setText("&&&&&&&&&&&&&&& sample") @@ -338,10 +338,10 @@ describe 'FuzzyFinder', -> rootView.trigger 'fuzzy-finder:find-under-cursor' waitsFor -> - finder.is(':visible') + finderView.is(':visible') runs -> - expect(finder.find('.error').text().length).toBeGreaterThan 0 + expect(finderView.find('.error').text().length).toBeGreaterThan 0 describe "opening a path into a split", -> @@ -354,7 +354,7 @@ describe 'FuzzyFinder', -> spyOn(editor, "splitLeft").andCallThrough() expect(rootView.find('.editor').length).toBe 1 rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - finder.miniEditor.trigger 'editor:split-left' + finderView.miniEditor.trigger 'editor:split-left' expect(rootView.find('.editor').length).toBe 2 expect(editor.splitLeft).toHaveBeenCalled() expect(rootView.getActiveEditor()).not.toBe editor @@ -365,7 +365,7 @@ describe 'FuzzyFinder', -> spyOn(editor, "splitRight").andCallThrough() expect(rootView.find('.editor').length).toBe 1 rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - finder.miniEditor.trigger 'editor:split-right' + finderView.miniEditor.trigger 'editor:split-right' expect(rootView.find('.editor').length).toBe 2 expect(editor.splitRight).toHaveBeenCalled() expect(rootView.getActiveEditor()).not.toBe editor @@ -376,7 +376,7 @@ describe 'FuzzyFinder', -> spyOn(editor, "splitDown").andCallThrough() expect(rootView.find('.editor').length).toBe 1 rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - finder.miniEditor.trigger 'editor:split-down' + finderView.miniEditor.trigger 'editor:split-down' expect(rootView.find('.editor').length).toBe 2 expect(editor.splitDown).toHaveBeenCalled() expect(rootView.getActiveEditor()).not.toBe editor @@ -387,7 +387,7 @@ describe 'FuzzyFinder', -> spyOn(editor, "splitUp").andCallThrough() expect(rootView.find('.editor').length).toBe 1 rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - finder.miniEditor.trigger 'editor:split-up' + finderView.miniEditor.trigger 'editor:split-up' expect(rootView.find('.editor').length).toBe 2 expect(editor.splitUp).toHaveBeenCalled() expect(rootView.getActiveEditor()).not.toBe editor diff --git a/src/packages/gfm.tmbundle/Snippets/bold.cson b/src/packages/gfm.tmbundle/Snippets/bold.cson new file mode 100644 index 000000000..f42f3b0e6 --- /dev/null +++ b/src/packages/gfm.tmbundle/Snippets/bold.cson @@ -0,0 +1,4 @@ +'name': 'bold text' +'scope': 'source.gfm' +'tabTrigger': 'b' +'content': '**$1**$0' diff --git a/src/packages/gfm.tmbundle/Snippets/code.cson b/src/packages/gfm.tmbundle/Snippets/code.cson new file mode 100644 index 000000000..14002c848 --- /dev/null +++ b/src/packages/gfm.tmbundle/Snippets/code.cson @@ -0,0 +1,4 @@ +'name': 'code block' +'scope': 'source.gfm' +'tabTrigger': 'code' +'content': '```$1\n$2\n```$0' diff --git a/src/packages/gfm.tmbundle/Snippets/img.cson b/src/packages/gfm.tmbundle/Snippets/img.cson new file mode 100644 index 000000000..ea5474167 --- /dev/null +++ b/src/packages/gfm.tmbundle/Snippets/img.cson @@ -0,0 +1,4 @@ +'name': 'embedded image' +'scope': 'source.gfm' +'tabTrigger': 'img' +'content': '![$1]($2)$0' diff --git a/src/packages/gfm.tmbundle/Snippets/italic.cson b/src/packages/gfm.tmbundle/Snippets/italic.cson new file mode 100644 index 000000000..eccc243da --- /dev/null +++ b/src/packages/gfm.tmbundle/Snippets/italic.cson @@ -0,0 +1,4 @@ +'name': 'italic text' +'scope': 'source.gfm' +'tabTrigger': 'i' +'content': '*$1*$0' diff --git a/src/packages/gfm.tmbundle/Syntaxes/gfm.cson b/src/packages/gfm.tmbundle/Syntaxes/gfm.cson new file mode 100644 index 000000000..066a345cc --- /dev/null +++ b/src/packages/gfm.tmbundle/Syntaxes/gfm.cson @@ -0,0 +1,80 @@ +'name': 'GitHub Markdown' +'scopeName': 'source.gfm' +'fileTypes': [ + 'markdown' + 'md' + 'mkd' + 'mkdown' + 'ron' +] +'patterns': [ + { + 'match': '(?:^|\\s)(\\*\\*[^\\*]+\\*\\*)' + 'captures': + '1': 'name': 'markup.bold.gfm' + } + { + 'match': '(?:^|\\s)(__[^_]+__)' + 'captures': + '1': 'name': 'markup.bold.gfm' + } + { + 'match': '(?:^|\\s)(\\*[^\\*]+\\*)' + 'captures': + '1': 'name': 'markup.italic.gfm' + } + { + 'match': '(?:^|\\s)(_[^_]+_)' + 'captures': + '1': 'name': 'markup.italic.gfm' + } + { + 'match': '^#{1,6}\\s+.+$' + 'name': 'markup.heading.gfm' + } + { + 'match': '\\:[^\\:\\s]+\\:' + 'name': 'string.emoji.gfm' + } + { + 'match': '^\\s*[\\*]{3,}\\s*$' + 'name': 'comment.hr.gfm' + } + { + 'match': '^\\s*[-]{3,}\\s*$' + 'name': 'comment.hr.gfm' + } + { + 'begin': '^```.*$' + 'beginCaptures': + '0': 'name': 'support.gfm' + 'end': '^```$' + 'endCaptures': + '0': 'name': 'support.gfm' + 'patterns': [ + 'match': '.*' + 'name': 'markup.raw.gfm' + ] + } + { + 'match': '`[^`]+`' + 'name': 'markup.raw.gfm' + } + { + 'match': '\\!?\\[([^\\]]*)\\]\\(([^\\)]+)\\)' + 'captures': + '1': 'name': 'entity.gfm' + '2': 'name': 'markup.underline.gfm' + } + { + 'match': '^\\s*([\\*\\+-])[ \\t]+' + 'captures': + '1': 'name': 'variable.list.gfm' + } + { + 'match': '^\\s*(>)(.*)' + 'captures': + '1': 'name': 'support.quote.gfm' + '2': 'name': 'comment.quote.gfm' + } +] diff --git a/src/packages/gfm.tmbundle/spec/gfm-spec.coffee b/src/packages/gfm.tmbundle/spec/gfm-spec.coffee new file mode 100644 index 000000000..7947d632b --- /dev/null +++ b/src/packages/gfm.tmbundle/spec/gfm-spec.coffee @@ -0,0 +1,134 @@ +TextMatePackage = require 'text-mate-package' + +describe "GitHub Flavored Markdown grammar", -> + grammar = null + + beforeEach -> + spyOn(syntax, "addGrammar") + pack = new TextMatePackage(require.resolve("gfm.tmbundle")) + pack.load() + grammar = pack.grammars[0] + + it "parses the grammar", -> + expect(grammar).toBeTruthy() + expect(grammar.scopeName).toBe "source.gfm" + + it "tokenizes horizontal rules", -> + {tokens} = grammar.tokenizeLine("***") + expect(tokens[0]).toEqual value: "***", scopes: ["source.gfm", "comment.hr.gfm"] + + {tokens} = grammar.tokenizeLine("---") + expect(tokens[0]).toEqual value: "---", scopes: ["source.gfm", "comment.hr.gfm"] + + it "tokenizes **bold** text", -> + {tokens} = grammar.tokenizeLine("this is **bold** text") + expect(tokens[0]).toEqual value: "this is", scopes: ["source.gfm"] + expect(tokens[1]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[2]).toEqual value: "**bold**", scopes: ["source.gfm", "markup.bold.gfm"] + expect(tokens[3]).toEqual value: " text", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine("not**bold**") + expect(tokens[0]).toEqual value: "not**bold**", scopes: ["source.gfm"] + + it "tokenizes __bold__ text", -> + {tokens} = grammar.tokenizeLine("____") + expect(tokens[0]).toEqual value: "____", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine("this is __bold__ text") + expect(tokens[0]).toEqual value: "this is", scopes: ["source.gfm"] + expect(tokens[1]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[2]).toEqual value: "__bold__", scopes: ["source.gfm", "markup.bold.gfm"] + expect(tokens[3]).toEqual value: " text", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine("not__bold__") + expect(tokens[0]).toEqual value: "not__bold__", scopes: ["source.gfm"] + + it "tokenizes *italic* text", -> + {tokens} = grammar.tokenizeLine("**") + expect(tokens[0]).toEqual value: "**", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine("this is *italic* text") + expect(tokens[0]).toEqual value: "this is", scopes: ["source.gfm"] + expect(tokens[1]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[2]).toEqual value: "*italic*", scopes: ["source.gfm", "markup.italic.gfm"] + expect(tokens[3]).toEqual value: " text", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine("not*italic*") + expect(tokens[0]).toEqual value: "not*italic*", scopes: ["source.gfm"] + + it "tokenizes _italic_ text", -> + {tokens} = grammar.tokenizeLine("__") + expect(tokens[0]).toEqual value: "__", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine("this is _italic_ text") + expect(tokens[0]).toEqual value: "this is", scopes: ["source.gfm"] + expect(tokens[1]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[2]).toEqual value: "_italic_", scopes: ["source.gfm", "markup.italic.gfm"] + expect(tokens[3]).toEqual value: " text", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine("not_italic_") + expect(tokens[0]).toEqual value: "not_italic_", scopes: ["source.gfm"] + + it "tokenizes a ## Heading", -> + {tokens} = grammar.tokenizeLine("# Heading 1") + expect(tokens[0]).toEqual value: "# Heading 1", scopes: ["source.gfm", "markup.heading.gfm"] + {tokens} = grammar.tokenizeLine("### Heading 3") + expect(tokens[0]).toEqual value: "### Heading 3", scopes: ["source.gfm", "markup.heading.gfm"] + + it "tokenizies an :emoji:", -> + {tokens} = grammar.tokenizeLine("this is :no_good:") + expect(tokens[0]).toEqual value: "this is ", scopes: ["source.gfm"] + expect(tokens[1]).toEqual value: ":no_good:", scopes: ["source.gfm", "string.emoji.gfm"] + + {tokens} = grammar.tokenizeLine("this is :no good:") + expect(tokens[0]).toEqual value: "this is :no good:", scopes: ["source.gfm"] + + it "tokenizes a ``` code block```", -> + {tokens, ruleStack} = grammar.tokenizeLine("```coffeescript") + expect(tokens[0]).toEqual value: "```coffeescript", scopes: ["source.gfm", "support.gfm"] + {tokens, ruleStack} = grammar.tokenizeLine("-> 'hello'", ruleStack) + expect(tokens[0]).toEqual value: "-> 'hello'", scopes: ["source.gfm", "markup.raw.gfm"] + {tokens} = grammar.tokenizeLine("```", ruleStack) + expect(tokens[0]).toEqual value: "```", scopes: ["source.gfm", "support.gfm"] + + it "tokenizes inline `code` blocks", -> + {tokens} = grammar.tokenizeLine("`this` is `code`") + expect(tokens[0]).toEqual value: "`this`", scopes: ["source.gfm", "markup.raw.gfm"] + expect(tokens[1]).toEqual value: " is ", scopes: ["source.gfm"] + expect(tokens[2]).toEqual value: "`code`", scopes: ["source.gfm", "markup.raw.gfm"] + + it "tokenizes [links](links)", -> + {tokens} = grammar.tokenizeLine("please click [this link](website)") + expect(tokens[0]).toEqual value: "please click ", scopes: ["source.gfm"] + expect(tokens[1]).toEqual value: "[", scopes: ["source.gfm"] + expect(tokens[2]).toEqual value: "this link", scopes: ["source.gfm", "entity.gfm"] + expect(tokens[3]).toEqual value: "](", scopes: ["source.gfm"] + expect(tokens[4]).toEqual value: "website", scopes: ["source.gfm", "markup.underline.gfm"] + expect(tokens[5]).toEqual value: ")", scopes: ["source.gfm"] + + it "tokenizes lists", -> + {tokens} = grammar.tokenizeLine("*Item 1") + expect(tokens[0]).toEqual value: "*Item 1", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine(" * Item 1") + expect(tokens[0]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[1]).toEqual value: "*", scopes: ["source.gfm", "variable.list.gfm"] + expect(tokens[2]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[3]).toEqual value: "Item 1", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine(" + Item 2") + expect(tokens[0]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[1]).toEqual value: "+", scopes: ["source.gfm", "variable.list.gfm"] + expect(tokens[2]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[3]).toEqual value: "Item 2", scopes: ["source.gfm"] + + {tokens} = grammar.tokenizeLine(" - Item 3") + expect(tokens[0]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[1]).toEqual value: "-", scopes: ["source.gfm", "variable.list.gfm"] + expect(tokens[2]).toEqual value: " ", scopes: ["source.gfm"] + expect(tokens[3]).toEqual value: "Item 3", scopes: ["source.gfm"] + + it "tokenizes > quoted text", -> + {tokens} = grammar.tokenizeLine("> Quotation") + expect(tokens[0]).toEqual value: ">", scopes: ["source.gfm", "support.quote.gfm"] + expect(tokens[1]).toEqual value: " Quotation", scopes: ["source.gfm", "comment.quote.gfm"] diff --git a/src/packages/gists/index.coffee b/src/packages/gists/index.coffee deleted file mode 100644 index 6205cb366..000000000 --- a/src/packages/gists/index.coffee +++ /dev/null @@ -1,12 +0,0 @@ -DeferredAtomPackage = require 'deferred-atom-package' - -module.exports = -class GistsPackage extends DeferredAtomPackage - - loadEvents: - 'gist:create': '.editor' - - instanceClass: 'gists/lib/gists' - - onLoadEvent: (event, instance) -> - instance.createGist(event.currentTargetView()) diff --git a/src/packages/gists/lib/gists.coffee b/src/packages/gists/lib/gists.coffee index b46e5beab..4cc241fcd 100644 --- a/src/packages/gists/lib/gists.coffee +++ b/src/packages/gists/lib/gists.coffee @@ -3,12 +3,15 @@ $ = require 'jquery' module.exports = class Gists + @activate: -> new Gists - @activate: (rootView) -> new Gists(rootView) - - constructor: (@rootView) -> + constructor: -> + rootView.command 'gist:create', '.editor', => @createGist() createGist: (editor) -> + editor = rootView.getActiveEditor() + return unless editor? + gist = { public: false, files: {} } gist.files[editor.getBuffer().getBaseName()] = content: editor.getSelectedText() or editor.getText() @@ -27,5 +30,5 @@ class Gists @div class: 'content', => @h3 "Gist #{response.id} created", class: 'title' @p "The url is on your clipboard", class: 'message' - @rootView.append(notification.hide()) + rootView.append(notification.hide()) notification.fadeIn().delay(2000).fadeOut(complete: -> $(this).remove()) diff --git a/src/packages/gists/package.cson b/src/packages/gists/package.cson new file mode 100644 index 000000000..45603d61f --- /dev/null +++ b/src/packages/gists/package.cson @@ -0,0 +1,3 @@ +'main': 'lib/gists' +'activationEvents': + 'gist:create': '.editor' diff --git a/src/packages/gists/spec/gists-spec.coffee b/src/packages/gists/spec/gists-spec.coffee index 056d6e412..5382fb33f 100644 --- a/src/packages/gists/spec/gists-spec.coffee +++ b/src/packages/gists/spec/gists-spec.coffee @@ -2,12 +2,11 @@ RootView = require 'root-view' $ = require 'jquery' describe "Gists package", -> - - [rootView, editor] = [] + [editor] = [] beforeEach -> rootView = new RootView(fixturesProject.resolve('sample.js')) - atom.loadPackage('gists').getInstance() + atom.loadPackage('gists') editor = rootView.getActiveEditor() spyOn($, 'ajax') diff --git a/src/packages/go-to-line/index.coffee b/src/packages/go-to-line/index.coffee deleted file mode 100644 index 12601c00e..000000000 --- a/src/packages/go-to-line/index.coffee +++ /dev/null @@ -1,12 +0,0 @@ -DeferredAtomPackage = require 'deferred-atom-package' - -module.exports = -class GoToLinePackage extends DeferredAtomPackage - - loadEvents: - 'editor:go-to-line': '.editor' - - instanceClass: 'go-to-line/lib/go-to-line-view' - - onLoadEvent: (event, instance) -> - instance.toggle(event.currentTargetView()) diff --git a/src/packages/go-to-line/lib/go-to-line-view.coffee b/src/packages/go-to-line/lib/go-to-line-view.coffee index f207e11eb..1f24afd39 100644 --- a/src/packages/go-to-line/lib/go-to-line-view.coffee +++ b/src/packages/go-to-line/lib/go-to-line-view.coffee @@ -6,14 +6,15 @@ Point = require 'point' module.exports = class GoToLineView extends View - @activate: (rootView) -> new GoToLineView(rootView) + @activate: -> new GoToLineView @content: -> @div class: 'go-to-line overlay from-top mini', => @subview 'miniEditor', new Editor(mini: true) @div class: 'message', outlet: 'message' - initialize: (@rootView) -> + initialize: -> + rootView.command 'editor:go-to-line', '.editor', => @toggle() @miniEditor.on 'focusout', => @detach() @on 'core:confirm', => @confirm() @on 'core:cancel', => @detach() @@ -42,13 +43,13 @@ class GoToLineView extends View @detach() return unless editor and lineNumber.length - position = new Point(parseInt(lineNumber - 1, 0)) + position = new Point(parseInt(lineNumber - 1)) editor.scrollToBufferPosition(position, center: true) editor.setCursorBufferPosition(position) editor.moveCursorToFirstCharacterOfLine() attach: -> @previouslyFocusedElement = $(':focus') - @rootView.append(this) - @message.text("Enter a line number 1-#{@rootView.getActiveEditor().getLineCount()}") + rootView.append(this) + @message.text("Enter a line number 1-#{rootView.getActiveEditor().getLineCount()}") @miniEditor.focus() diff --git a/src/packages/go-to-line/package.cson b/src/packages/go-to-line/package.cson new file mode 100644 index 000000000..4711ed56c --- /dev/null +++ b/src/packages/go-to-line/package.cson @@ -0,0 +1,3 @@ +'activationEvents': + 'editor:go-to-line': '.editor' +'main': './lib/go-to-line-view' diff --git a/src/packages/go-to-line/spec/go-to-line-spec.coffee b/src/packages/go-to-line/spec/go-to-line-spec.coffee index 775bde131..8c9d13d90 100644 --- a/src/packages/go-to-line/spec/go-to-line-spec.coffee +++ b/src/packages/go-to-line/spec/go-to-line-spec.coffee @@ -1,13 +1,14 @@ RootView = require 'root-view' +GoToLineView = require 'go-to-line/lib/go-to-line-view' describe 'GoToLine', -> - [rootView, goToLine, editor] = [] + [goToLine, editor] = [] beforeEach -> - rootView = new RootView(require.resolve('fixtures/sample.js')) + new RootView(require.resolve('fixtures/sample.js')) rootView.enableKeymap() - goToLine = atom.loadPackage("go-to-line").getInstance() editor = rootView.getActiveEditor() + goToLine = GoToLineView.activate() editor.setCursorBufferPosition([1,0]) afterEach -> diff --git a/src/packages/markdown-preview/index.coffee b/src/packages/markdown-preview/index.coffee deleted file mode 100644 index 7cbba03f0..000000000 --- a/src/packages/markdown-preview/index.coffee +++ /dev/null @@ -1,9 +0,0 @@ -DeferredAtomPackage = require 'deferred-atom-package' - -module.exports = -class MarkdownPreview extends DeferredAtomPackage - loadEvents: ['markdown-preview:toggle'] - - instanceClass: 'markdown-preview/src/markdown-preview-view' - - onLoadEvent: (event, instance) -> instance.toggle() diff --git a/src/packages/markdown-preview/src/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee similarity index 75% rename from src/packages/markdown-preview/src/markdown-preview-view.coffee rename to src/packages/markdown-preview/lib/markdown-preview-view.coffee index c044623ab..f112d58e1 100644 --- a/src/packages/markdown-preview/src/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -5,19 +5,19 @@ $ = require 'jquery' module.exports = class MarkdownPreviewView extends ScrollView - @activate: (rootView, state) -> - @instance = new this(rootView) + @activate: -> + @instance = new MarkdownPreviewView - @content: (rootView) -> + @content: -> @div class: 'markdown-preview', tabindex: -1, => @div class: 'markdown-body', outlet: 'markdownBody' - initialize: (@rootView) -> + initialize: -> super - @editor = @rootView.getActiveEditor() - @subscribe @editor, 'focus', => @detach() unless @detaching - @command 'core:cancel', => @detach() unless @detaching + rootView.command 'markdown-preview:toggle', => @toggle() + @on 'blur', => @detach() unless document.activeElement is this[0] + @command 'core:cancel', => @detach() toggle: -> if @hasParent() @@ -27,22 +27,23 @@ class MarkdownPreviewView extends ScrollView attach: -> return unless @isMarkdownFile(@getActivePath()) - @rootView.append(this) + rootView.append(this) @markdownBody.html(@getLoadingHtml()) @loadHtml() @focus() detach: -> + return if @detaching @detaching = true super - @rootView.focus() + rootView.focus() @detaching = false getActivePath: -> - @editor.getPath() + rootView.getActiveEditor()?.getPath() getActiveText: -> - @editor.getText() + rootView.getActiveEditor()?.getText() getErrorHtml: (error) -> $$$ -> @@ -76,4 +77,4 @@ class MarkdownPreviewView extends ScrollView @markdownBody.html(html) if @hasParent() isMarkdownFile: (path) -> - fs.isMarkdownExtension(fs.extension(path)) + path and fs.isMarkdownExtension(fs.extension(path)) diff --git a/src/packages/markdown-preview/package.cson b/src/packages/markdown-preview/package.cson new file mode 100644 index 000000000..deea08f07 --- /dev/null +++ b/src/packages/markdown-preview/package.cson @@ -0,0 +1,3 @@ +'main': 'lib/markdown-preview-view' +'activationEvents': + 'markdown-preview:toggle': '.editor' diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 49066159b..6917e8240 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -1,12 +1,12 @@ $ = require 'jquery' RootView = require 'root-view' -MarkdownPreview = require 'markdown-preview/src/markdown-preview-view' +MarkdownPreview = require 'markdown-preview/lib/markdown-preview-view' describe "MarkdownPreview", -> - [rootView, markdownPreview] = [] - beforeEach -> rootView = new RootView(require.resolve('fixtures/markdown')) + atom.loadPackage("markdown-preview") + spyOn(MarkdownPreview.prototype, 'loadHtml') afterEach -> rootView.deactivate() @@ -15,44 +15,37 @@ describe "MarkdownPreview", -> it "toggles on/off a preview for a .md file", -> rootView.open('file.md') editor = rootView.getActiveEditor() - markdownPreview = atom.loadPackage("markdown-preview").getInstance() expect(rootView.find('.markdown-preview')).not.toExist() - spyOn(markdownPreview, 'loadHtml') editor.trigger('markdown-preview:toggle') markdownPreviewView = rootView.find('.markdown-preview')?.view() expect(rootView.find('.markdown-preview')).toExist() - expect(markdownPreview.loadHtml).toHaveBeenCalled(); + expect(markdownPreviewView.loadHtml).toHaveBeenCalled() markdownPreviewView.trigger('markdown-preview:toggle') expect(rootView.find('.markdown-preview')).not.toExist() it "displays a preview for a .markdown file", -> rootView.open('file.markdown') editor = rootView.getActiveEditor() - markdownPreview = atom.loadPackage("markdown-preview").getInstance() expect(rootView.find('.markdown-preview')).not.toExist() - spyOn(markdownPreview, 'loadHtml') editor.trigger('markdown-preview:toggle') expect(rootView.find('.markdown-preview')).toExist() - expect(markdownPreview.loadHtml).toHaveBeenCalled(); + markdownPreviewView = rootView.find('.markdown-preview')?.view() + expect(markdownPreviewView.loadHtml).toHaveBeenCalled() it "does not display a preview for non-markdown file", -> rootView.open('file.js') editor = rootView.getActiveEditor() - markdownPreview = atom.loadPackage("markdown-preview").getInstance() expect(rootView.find('.markdown-preview')).not.toExist() - spyOn(markdownPreview, 'loadHtml') editor.trigger('markdown-preview:toggle') expect(rootView.find('.markdown-preview')).not.toExist() - expect(markdownPreview.loadHtml).not.toHaveBeenCalled(); + expect(MarkdownPreview.prototype.loadHtml).not.toHaveBeenCalled() describe "core:cancel event", -> it "removes markdown preview", -> rootView.open('file.md') editor = rootView.getActiveEditor() - markdownPreview = atom.loadPackage("markdown-preview").getInstance() expect(rootView.find('.markdown-preview')).not.toExist() - spyOn(markdownPreview, 'loadHtml') editor.trigger('markdown-preview:toggle') markdownPreviewView = rootView.find('.markdown-preview')?.view() @@ -62,14 +55,19 @@ describe "MarkdownPreview", -> describe "when the editor receives focus", -> it "removes the markdown preview view", -> + rootView.attachToDom() rootView.open('file.md') editor = rootView.getActiveEditor() - markdownPreview = atom.loadPackage("markdown-preview").getInstance() expect(rootView.find('.markdown-preview')).not.toExist() - spyOn(markdownPreview, 'loadHtml') editor.trigger('markdown-preview:toggle') markdownPreviewView = rootView.find('.markdown-preview') - expect(markdownPreviewView).toExist() editor.focus() + expect(markdownPreviewView).toExist() + expect(rootView.find('.markdown-preview')).not.toExist() + + describe "when no editor is open", -> + it "does not attach", -> + expect(rootView.getActiveEditor()).toBeFalsy() + rootView.trigger('markdown-preview:toggle') expect(rootView.find('.markdown-preview')).not.toExist() diff --git a/themes/atom-dark-ui/markdown-preview.css b/src/packages/markdown-preview/stylesheets/markdown-preview.css similarity index 98% rename from themes/atom-dark-ui/markdown-preview.css rename to src/packages/markdown-preview/stylesheets/markdown-preview.css index 5d58e2de7..1138dc1b7 100644 --- a/themes/atom-dark-ui/markdown-preview.css +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.css @@ -1,10 +1,13 @@ .markdown-preview { + font-family: "Helvetica Neue", Helvetica, sans-serif; + font-size: 14px; + line-height: 1.6; position: absolute; width: 100%; height: 100%; top: 0px; left: 0px; - background-color: #F4F4F4; + background-color: #fff; overflow: auto; z-index: 3; box-sizing: border-box; @@ -12,14 +15,7 @@ } .markdown-body { - background-color: #fff; - box-shadow: rgba(0, 0, 0, 0.1) 0 0 0 1px,rgba(0, 0, 0, 0.3) 0 1px 3px; - border-radius: 5px; - max-width: 914px; min-width: 680px; - margin-left: auto; - margin-right: auto; - padding: 30px; } .markdown-body pre, diff --git a/src/packages/snippets/src/load-snippets-handler.coffee b/src/packages/snippets/lib/load-snippets-handler.coffee similarity index 88% rename from src/packages/snippets/src/load-snippets-handler.coffee rename to src/packages/snippets/lib/load-snippets-handler.coffee index ba4f16c13..8cb26b687 100644 --- a/src/packages/snippets/src/load-snippets-handler.coffee +++ b/src/packages/snippets/lib/load-snippets-handler.coffee @@ -20,8 +20,10 @@ module.exports = continue if fs.base(snippetsPath).indexOf('.') is 0 try - if object = fs.readPlist(snippetsPath) - snippets.push(object) if object + if fs.extension(snippetsPath) is '.cson' and object = fs.readObject(snippetsPath) + snippets.push(object) + else if object = fs.readPlist(snippetsPath) + snippets.push(object) else logWarning() catch e diff --git a/src/packages/snippets/src/load-snippets-task.coffee b/src/packages/snippets/lib/load-snippets-task.coffee similarity index 90% rename from src/packages/snippets/src/load-snippets-task.coffee rename to src/packages/snippets/lib/load-snippets-task.coffee index e32d70dde..b664c0b51 100644 --- a/src/packages/snippets/src/load-snippets-task.coffee +++ b/src/packages/snippets/lib/load-snippets-task.coffee @@ -4,8 +4,8 @@ TextMatePackage = require 'text-mate-package' module.exports = class LoadSnippetsTask extends Task constructor: (@snippets) -> - super('snippets/src/load-snippets-handler') - @packages = atom.getPackages() + super('snippets/lib/load-snippets-handler') + @packages = atom.getLoadedPackages() @packages.push(path: config.configDirPath) started: -> diff --git a/src/packages/snippets/src/snippet-body-parser.coffee b/src/packages/snippets/lib/snippet-body-parser.coffee similarity index 100% rename from src/packages/snippets/src/snippet-body-parser.coffee rename to src/packages/snippets/lib/snippet-body-parser.coffee diff --git a/src/packages/snippets/src/snippet-body.pegjs b/src/packages/snippets/lib/snippet-body.pegjs similarity index 100% rename from src/packages/snippets/src/snippet-body.pegjs rename to src/packages/snippets/lib/snippet-body.pegjs diff --git a/src/packages/snippets/src/snippet-expansion.coffee b/src/packages/snippets/lib/snippet-expansion.coffee similarity index 53% rename from src/packages/snippets/src/snippet-expansion.coffee rename to src/packages/snippets/lib/snippet-expansion.coffee index 9c2187af9..7c27222b8 100644 --- a/src/packages/snippets/src/snippet-expansion.coffee +++ b/src/packages/snippets/lib/snippet-expansion.coffee @@ -4,10 +4,11 @@ _ = require 'underscore' module.exports = class SnippetExpansion snippet: null - tabStopAnchorRanges: null + tabStopMarkers: null settingTabStop: false constructor: (@snippet, @editSession) -> + @editSession.selectToBeginningOfWord() startPosition = @editSession.getCursorBufferPosition() @editSession.transact => @@ -16,26 +17,21 @@ class SnippetExpansion editSession.pushOperation do: => @subscribe @editSession, 'cursor-moved.snippet-expansion', (e) => @cursorMoved(e) - @placeTabStopAnchorRanges(startPosition, snippet.tabStops) + @placeTabStopMarkers(startPosition, snippet.tabStops) @editSession.snippetExpansion = this undo: => @destroy() @editSession.normalizeTabsInBufferRange(newRange) @indentSubsequentLines(startPosition.row, snippet) if snippet.lineCount > 1 - cursorMoved: ({oldBufferPosition, newBufferPosition}) -> - return if @settingTabStop - + cursorMoved: ({oldBufferPosition, newBufferPosition, bufferChanged}) -> + return if @settingTabStop or bufferChanged oldTabStops = @tabStopsForBufferPosition(oldBufferPosition) newTabStops = @tabStopsForBufferPosition(newBufferPosition) - @destroy() unless _.intersect(oldTabStops, newTabStops).length - placeTabStopAnchorRanges: (startPosition, tabStopRanges) -> - @tabStopAnchorRanges = tabStopRanges.map ({start, end}) => - anchorRange = @editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)]) - @subscribe anchorRange, 'destroyed', => - _.remove(@tabStopAnchorRanges, anchorRange) - anchorRange + placeTabStopMarkers: (startPosition, tabStopRanges) -> + @tabStopMarkers = tabStopRanges.map ({start, end}) => + @editSession.markBufferRange([startPosition.add(start), startPosition.add(end)]) @setTabStopIndex(0) indentSubsequentLines: (startRow, snippet) -> @@ -45,46 +41,33 @@ class SnippetExpansion goToNextTabStop: -> nextIndex = @tabStopIndex + 1 - if @cursorIsInsideTabStops() and nextIndex < @tabStopAnchorRanges.length - @setTabStopIndex(nextIndex) - true + if nextIndex < @tabStopMarkers.length + if @setTabStopIndex(nextIndex) + true + else + @goToNextTabStop() else @destroy() false goToPreviousTabStop: -> - if @cursorIsInsideTabStops() - @setTabStopIndex(@tabStopIndex - 1) if @tabStopIndex > 0 - true - else - @destroy() - false - - ensureValidTabStops: -> - @tabStopAnchorRanges? and @destroyIfCursorIsOutsideTabStops() + @setTabStopIndex(@tabStopIndex - 1) if @tabStopIndex > 0 setTabStopIndex: (@tabStopIndex) -> @settingTabStop = true - @editSession.setSelectedBufferRange(@tabStopAnchorRanges[@tabStopIndex].getBufferRange()) + markerSelected = @editSession.selectMarker(@tabStopMarkers[@tabStopIndex]) @settingTabStop = false - - cursorIsInsideTabStops: -> - position = @editSession.getCursorBufferPosition() - for anchorRange in @tabStopAnchorRanges - return true if anchorRange.containsBufferPosition(position) - false + markerSelected tabStopsForBufferPosition: (bufferPosition) -> - _.intersection(@tabStopAnchorRanges, @editSession.anchorRangesForBufferPosition(bufferPosition)) + _.intersection(@tabStopMarkers, @editSession.markersForBufferPosition(bufferPosition)) destroy: -> @unsubscribe() - anchorRange.destroy() for anchorRange in @tabStopAnchorRanges + @editSession.destroyMarker(marker) for marker in @tabStopMarkers @editSession.snippetExpansion = null restore: (@editSession) -> @editSession.snippetExpansion = this - @tabStopAnchorRanges = @tabStopAnchorRanges.map (anchorRange) => - @editSession.addAnchorRange(anchorRange.getBufferRange()) _.extend(SnippetExpansion.prototype, Subscriber) diff --git a/src/packages/snippets/src/snippet.coffee b/src/packages/snippets/lib/snippet.coffee similarity index 100% rename from src/packages/snippets/src/snippet.coffee rename to src/packages/snippets/lib/snippet.coffee diff --git a/src/packages/snippets/index.coffee b/src/packages/snippets/lib/snippets.coffee similarity index 81% rename from src/packages/snippets/index.coffee rename to src/packages/snippets/lib/snippets.coffee index 6778eb145..fb514f11b 100644 --- a/src/packages/snippets/index.coffee +++ b/src/packages/snippets/lib/snippets.coffee @@ -1,22 +1,25 @@ AtomPackage = require 'atom-package' fs = require 'fs' _ = require 'underscore' -SnippetExpansion = require './src/snippet-expansion' -Snippet = require './src/snippet' -LoadSnippetsTask = require './src/load-snippets-task' +SnippetExpansion = require './snippet-expansion' +Snippet = require './snippet' +LoadSnippetsTask = require './load-snippets-task' module.exports = -class Snippets extends AtomPackage snippetsByExtension: {} loaded: false - activate: (@rootView) -> + activate: -> window.snippets = this @loadAll() - @rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor) + rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor) + + deactivate: -> + @loadSnippetsTask?.terminate() loadAll: -> - new LoadSnippetsTask(this).start() + @loadSnippetsTask = new LoadSnippetsTask(this) + @loadSnippetsTask.start() loadDirectory: (snippetsDirPath) -> for snippetsPath in fs.list(snippetsDirPath) when fs.base(snippetsPath).indexOf('.') isnt 0 @@ -41,7 +44,7 @@ class Snippets extends AtomPackage syntax.addProperties(selector, snippets: snippetsByPrefix) getBodyParser: -> - require 'snippets/src/snippet-body-parser' + require 'snippets/lib/snippet-body-parser' enableSnippetsInEditor: (editor) -> editor.command 'snippets:expand', (e) => diff --git a/src/packages/snippets/package.cson b/src/packages/snippets/package.cson new file mode 100644 index 000000000..94cd77cc9 --- /dev/null +++ b/src/packages/snippets/package.cson @@ -0,0 +1 @@ +'main': 'lib/snippets' diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index c9582a336..b00f2fdcb 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -1,18 +1,25 @@ -Snippets = require 'snippets' -Snippet = require 'snippets/src/snippet' -LoadSnippetsTask = require 'snippets/src/load-snippets-task' +Snippet = require 'snippets/lib/snippet' +LoadSnippetsTask = require 'snippets/lib/load-snippets-task' RootView = require 'root-view' Buffer = require 'buffer' Editor = require 'editor' _ = require 'underscore' fs = require 'fs' +Package = require 'package' describe "Snippets extension", -> [buffer, editor, editSession] = [] beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) spyOn(LoadSnippetsTask.prototype, 'start') + + packageWithSnippets = atom.loadPackage("package-with-snippets") + + spyOn(atom, "getLoadedPackages").andCallFake -> + window.textMatePackages.concat([packageWithSnippets]) + atom.loadPackage("snippets") + editor = rootView.getActiveEditor() editSession = rootView.getActiveEditSession() buffer = editor.getBuffer() @@ -20,7 +27,7 @@ describe "Snippets extension", -> rootView.enableKeymap() afterEach -> - rootView.remove() + rootView.deactivate() delete window.snippets describe "when 'tab' is triggered on the editor", -> @@ -86,7 +93,7 @@ describe "Snippets extension", -> describe "when the snippet contains tab stops", -> it "places the cursor at the first tab-stop, and moves the cursor in response to 'next-tab-stop' events", -> - anchorCountBefore = editor.activeEditSession.getAnchors().length + markerCountBefore = editor.activeEditSession.getMarkerCount() editor.setCursorScreenPosition([2, 0]) editor.insertText('t2') editor.trigger keydownEvent('tab', target: editor[0]) @@ -118,7 +125,7 @@ describe "Snippets extension", -> editor.trigger keydownEvent('tab', target: editor[0]) editor.trigger keydownEvent('tab', target: editor[0]) expect(buffer.lineForRow(2)).toBe "go here next:(abc) and finally go here:( )" - expect(editor.activeEditSession.getAnchors().length).toBe anchorCountBefore + expect(editor.activeEditSession.getMarkerCount()).toBe markerCountBefore describe "when tab stops are nested", -> it "destroys the inner tab stop if the outer tab stop is modified", -> @@ -139,7 +146,7 @@ describe "Snippets extension", -> editor.trigger 'snippets:next-tab-stop' expect(editSession.getCursorBufferPosition()).toEqual [3, 25] - describe "when the cursor is moved beyond the bounds of a tab stop", -> + describe "when the cursor is moved beyond the bounds of the current tab stop", -> it "terminates the snippet", -> editor.setCursorScreenPosition([2, 0]) editor.insertText('t2') @@ -232,7 +239,6 @@ describe "Snippets extension", -> describe "snippet loading", -> beforeEach -> - atom.packages = null jasmine.unspy(LoadSnippetsTask.prototype, 'start') spyOn(LoadSnippetsTask.prototype, 'loadAtomSnippets').andCallFake -> @snippetsLoaded({}) spyOn(LoadSnippetsTask.prototype, 'loadTextMateSnippets').andCallFake -> @snippetsLoaded({}) @@ -271,7 +277,7 @@ describe "Snippets extension", -> } """ - # warn about junk-file, but don't even try to parse a hidden file + # warn about invalid.plist expect(console.warn).toHaveBeenCalled() expect(console.warn.calls.length).toBe 1 @@ -290,6 +296,22 @@ describe "Snippets extension", -> expect(Worker.prototype.terminate).toHaveBeenCalled() expect(Worker.prototype.terminate.calls.length).toBe 1 + it "loads CSON snippets from TextMate packages", -> + jasmine.unspy(LoadSnippetsTask.prototype, 'loadTextMateSnippets') + snippets.loaded = false + task = new LoadSnippetsTask(snippets) + task.packages = [Package.build(fixturesProject.resolve('packages/package-with-a-cson-grammar.tmbundle'))] + task.start() + + waitsFor "CSON snippets to load", 5000, -> snippets.loaded + + runs -> + snippet = syntax.getProperty(['.source.alot'], 'snippets.really') + expect(snippet).toBeTruthy() + expect(snippet.prefix).toBe 'really' + expect(snippet.name).toBe 'Really' + expect(snippet.body).toBe "I really like alot" + describe "snippet body parser", -> it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", -> bodyTree = snippets.getBodyParser().parse """ diff --git a/src/packages/status-bar/index.coffee b/src/packages/status-bar/index.coffee deleted file mode 100644 index 7d05f5962..000000000 --- a/src/packages/status-bar/index.coffee +++ /dev/null @@ -1,6 +0,0 @@ -AtomPackage = require 'atom-package' -StatusBarView = require './src/status-bar-view' - -module.exports = -class StatusBar extends AtomPackage - activate: (rootView) -> StatusBarView.activate(rootView) diff --git a/src/packages/status-bar/src/status-bar-view.coffee b/src/packages/status-bar/lib/status-bar-view.coffee similarity index 96% rename from src/packages/status-bar/src/status-bar-view.coffee rename to src/packages/status-bar/lib/status-bar-view.coffee index 565532679..1e7e38694 100644 --- a/src/packages/status-bar/src/status-bar-view.coffee +++ b/src/packages/status-bar/lib/status-bar-view.coffee @@ -4,7 +4,7 @@ $ = require 'jquery' module.exports = class StatusBarView extends View - @activate: (rootView) -> + @activate: -> rootView.eachEditor (editor) => @appendToEditorPane(rootView, editor) if editor.attached @@ -24,7 +24,7 @@ class StatusBarView extends View @span class: 'cursor-position', outlet: 'cursorPosition' @span class: 'grammar-name', outlet: 'grammarName' - initialize: (@rootView, @editor) -> + initialize: (rootView, @editor) -> @updatePathText() @editor.on 'editor:path-changed', => @subscribeToBuffer() @@ -99,7 +99,7 @@ class StatusBarView extends View updatePathText: -> if path = @editor.getPath() - @currentPath.text(@rootView.project.relativize(path)) + @currentPath.text(rootView.project.relativize(path)) else @currentPath.text('untitled') diff --git a/src/packages/status-bar/package.cson b/src/packages/status-bar/package.cson new file mode 100644 index 000000000..31e9f7795 --- /dev/null +++ b/src/packages/status-bar/package.cson @@ -0,0 +1 @@ +'main': 'lib/status-bar-view' diff --git a/src/packages/status-bar/spec/status-bar-spec.coffee b/src/packages/status-bar/spec/status-bar-spec.coffee index 16e4bcc20..fc702734e 100644 --- a/src/packages/status-bar/spec/status-bar-spec.coffee +++ b/src/packages/status-bar/spec/status-bar-spec.coffee @@ -1,7 +1,7 @@ $ = require 'jquery' _ = require 'underscore' RootView = require 'root-view' -StatusBar = require 'status-bar/src/status-bar-view' +StatusBar = require 'status-bar/lib/status-bar-view' fs = require 'fs' describe "StatusBar", -> @@ -10,7 +10,7 @@ describe "StatusBar", -> beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) rootView.simulateDomAttachment() - StatusBar.activate(rootView) + StatusBar.activate() editor = rootView.getActiveEditor() statusBar = rootView.find('.status-bar').view() buffer = editor.getBuffer() @@ -38,7 +38,7 @@ describe "StatusBar", -> rootView = new RootView rootView.open() rootView.simulateDomAttachment() - StatusBar.activate(rootView) + StatusBar.activate() statusBar = rootView.find('.status-bar').view() expect(statusBar.currentPath.text()).toBe 'untitled' expect(statusBar.cursorPosition.text()).toBe '1,1' diff --git a/src/packages/status-bar/stylesheets/status-bar.css b/src/packages/status-bar/stylesheets/status-bar.css deleted file mode 100644 index 3fe635f49..000000000 --- a/src/packages/status-bar/stylesheets/status-bar.css +++ /dev/null @@ -1,9 +0,0 @@ -.status-bar { - position: relative; - -webkit-user-select: none; - cursor: default; -} - -.status-bar .git-branch { - float: right; -} \ No newline at end of file diff --git a/src/packages/strip-trailing-whitespace/index.coffee b/src/packages/strip-trailing-whitespace/lib/strip-trailing-whitespace.coffee similarity index 83% rename from src/packages/strip-trailing-whitespace/index.coffee rename to src/packages/strip-trailing-whitespace/lib/strip-trailing-whitespace.coffee index 20b3e6d74..1f93f4a68 100644 --- a/src/packages/strip-trailing-whitespace/index.coffee +++ b/src/packages/strip-trailing-whitespace/lib/strip-trailing-whitespace.coffee @@ -1,8 +1,5 @@ -AtomPackage = require 'atom-package' - module.exports = -class StripTrailingWhitespace extends AtomPackage - activate: (rootView) -> + activate: -> rootView.eachBuffer (buffer) => @stripTrailingWhitespaceBeforeSave(buffer) stripTrailingWhitespaceBeforeSave: (buffer) -> diff --git a/src/packages/strip-trailing-whitespace/package.cson b/src/packages/strip-trailing-whitespace/package.cson new file mode 100644 index 000000000..0daeeb412 --- /dev/null +++ b/src/packages/strip-trailing-whitespace/package.cson @@ -0,0 +1 @@ +'main': './lib/strip-trailing-whitespace' diff --git a/src/packages/symbols-view/index.coffee b/src/packages/symbols-view/index.coffee deleted file mode 100644 index 50d9a89c0..000000000 --- a/src/packages/symbols-view/index.coffee +++ /dev/null @@ -1,21 +0,0 @@ -DeferredAtomPackage = require 'deferred-atom-package' - -module.exports = -class Symbols extends DeferredAtomPackage - - loadEvents: [ - 'symbols-view:toggle-file-symbols' - 'symbols-view:toggle-project-symbols' - 'symbols-view:go-to-declaration' - ] - - instanceClass: 'symbols-view/src/symbols-view' - - onLoadEvent: (event, instance) -> - switch event.type - when 'symbols-view:toggle-file-symbols' - instance.toggleFileSymbols() - when 'symbols-view:toggle-project-symbols' - instance.toggleProjectSymbols() - when 'symbols-view:go-to-declaration' - instance.goToDeclaration() diff --git a/src/packages/symbols-view/src/symbols-view.coffee b/src/packages/symbols-view/lib/symbols-view.coffee similarity index 75% rename from src/packages/symbols-view/src/symbols-view.coffee rename to src/packages/symbols-view/lib/symbols-view.coffee index acf72cb78..c95c24559 100644 --- a/src/packages/symbols-view/src/symbols-view.coffee +++ b/src/packages/symbols-view/lib/symbols-view.coffee @@ -1,7 +1,7 @@ {$$} = require 'space-pen' SelectList = require 'select-list' -TagGenerator = require 'symbols-view/src/tag-generator' -TagReader = require 'symbols-view/src/tag-reader' +TagGenerator = require './tag-generator' +TagReader = require './tag-reader' Point = require 'point' fs = require 'fs' $ = require 'jquery' @@ -9,16 +9,20 @@ $ = require 'jquery' module.exports = class SymbolsView extends SelectList - @activate: (rootView) -> - @instance = new SymbolsView(rootView) + @activate: -> + new SymbolsView @viewClass: -> "#{super} symbols-view overlay from-top" filterKey: 'name' - initialize: (@rootView) -> + initialize: -> super + rootView.command 'symbols-view:toggle-file-symbols', => @toggleFileSymbols() + rootView.command 'symbols-view:toggle-project-symbols', => @toggleProjectSymbols() + rootView.command 'symbols-view:go-to-declaration', => @goToDeclaration() + itemForElement: ({position, name, file}) -> $$ -> @li => @@ -40,7 +44,8 @@ class SymbolsView extends SelectList populateFileSymbols: -> tags = [] callback = (tag) -> tags.push tag - path = @rootView.getActiveEditor().getPath() + path = rootView.getActiveEditor().getPath() + @list.empty() @setLoading("Generating symbols...") new TagGenerator(path, callback).generate().done => if tags.length > 0 @@ -50,7 +55,7 @@ class SymbolsView extends SelectList else @miniEditor.hide() @setError("No symbols found") - setTimeout (=> @detach()), 2000 + setTimeout (=> @cancel()), 2000 toggleProjectSymbols: -> if @hasParent() @@ -60,8 +65,9 @@ class SymbolsView extends SelectList @attach() populateProjectSymbols: -> + @list.empty() @setLoading("Loading symbols...") - TagReader.getAllTags(@rootView.project).done (tags) => + TagReader.getAllTags(rootView.project).done (tags) => if tags.length > 0 @miniEditor.show() @maxItems = 10 @@ -69,7 +75,7 @@ class SymbolsView extends SelectList else @miniEditor.hide() @setError("No symbols found") - setTimeout (=> @detach()), 2000 + setTimeout (=> @cancel()), 2000 confirmed : (tag) -> @cancel() @@ -78,11 +84,11 @@ class SymbolsView extends SelectList openTag: (tag) -> position = tag.position position = @getTagLine(tag) unless position - @rootView.open(tag.file, {changeFocus: true, allowActiveEditorChange:true}) if tag.file + rootView.open(tag.file, {changeFocus: true, allowActiveEditorChange:true}) if tag.file @moveToPosition(position) if position moveToPosition: (position) -> - editor = @rootView.getActiveEditor() + editor = rootView.getActiveEditor() editor.scrollToBufferPosition(position, center: true) editor.setCursorBufferPosition(position) editor.moveCursorToFirstCharacterOfLine() @@ -90,19 +96,19 @@ class SymbolsView extends SelectList attach: -> super - @rootView.append(this) + rootView.append(this) @miniEditor.focus() getTagLine: (tag) -> pattern = $.trim(tag.pattern?.replace(/(^^\/\^)|(\$\/$)/g, '')) # Remove leading /^ and trailing $/ return unless pattern - file = @rootView.project.resolve(tag.file) + file = rootView.project.resolve(tag.file) return unless fs.isFile(file) for line, index in fs.read(file).split('\n') return new Point(index, 0) if pattern is $.trim(line) goToDeclaration: -> - editor = @rootView.getActiveEditor() + editor = rootView.getActiveEditor() matches = TagReader.find(editor) return unless matches.length diff --git a/src/packages/symbols-view/src/tag-generator.coffee b/src/packages/symbols-view/lib/tag-generator.coffee similarity index 100% rename from src/packages/symbols-view/src/tag-generator.coffee rename to src/packages/symbols-view/lib/tag-generator.coffee diff --git a/src/packages/symbols-view/src/tag-reader.coffee b/src/packages/symbols-view/lib/tag-reader.coffee similarity index 100% rename from src/packages/symbols-view/src/tag-reader.coffee rename to src/packages/symbols-view/lib/tag-reader.coffee diff --git a/src/packages/symbols-view/package.cson b/src/packages/symbols-view/package.cson new file mode 100644 index 000000000..ceffedd45 --- /dev/null +++ b/src/packages/symbols-view/package.cson @@ -0,0 +1,5 @@ +'main': 'lib/symbols-view' +'activationEvents': + 'symbols-view:toggle-file-symbols': '.editor' + 'symbols-view:toggle-project-symbols': null + 'symbols-view:go-to-declaration': '.editor' diff --git a/src/packages/symbols-view/spec/symbols-view-spec.coffee b/src/packages/symbols-view/spec/symbols-view-spec.coffee index 3e0724795..5a8eff02c 100644 --- a/src/packages/symbols-view/spec/symbols-view-spec.coffee +++ b/src/packages/symbols-view/spec/symbols-view-spec.coffee @@ -1,16 +1,17 @@ RootView = require 'root-view' -SymbolsView = require 'symbols-view/src/symbols-view' -TagGenerator = require 'symbols-view/src/tag-generator' +SymbolsView = require 'symbols-view/lib/symbols-view' +TagGenerator = require 'symbols-view/lib/tag-generator' fs = require 'fs' describe "SymbolsView", -> - [rootView, symbolsView, setArraySpy] = [] + [symbolsView, setArraySpy] = [] beforeEach -> rootView = new RootView(require.resolve('fixtures')) - symbolsView = atom.loadPackage("symbols-view").getInstance() + atom.loadPackage("symbols-view") + rootView.attachToDom() - setArraySpy = spyOn(symbolsView, 'setArray').andCallThrough() + setArraySpy = spyOn(SymbolsView.prototype, 'setArray').andCallThrough() afterEach -> rootView.deactivate() @@ -19,8 +20,8 @@ describe "SymbolsView", -> describe "when tags can be generated for a file", -> it "initially displays all JavaScript functions with line numbers", -> rootView.open('sample.js') - expect(rootView.find('.symbols-view')).not.toExist() rootView.getActiveEditor().trigger "symbols-view:toggle-file-symbols" + symbolsView = rootView.find('.symbols-view').view() expect(symbolsView.find('.loading')).toHaveText 'Generating symbols...' waitsFor -> @@ -39,8 +40,8 @@ describe "SymbolsView", -> it "displays error when no tags match text in mini-editor", -> rootView.open('sample.js') - expect(rootView.find('.symbols-view')).not.toExist() rootView.getActiveEditor().trigger "symbols-view:toggle-file-symbols" + symbolsView = rootView.find('.symbols-view').view() waitsFor -> setArraySpy.callCount > 0 @@ -66,8 +67,8 @@ describe "SymbolsView", -> describe "when tags can't be generated for a file", -> it "shows an error message when no matching tags are found", -> rootView.open('sample.txt') - expect(rootView.find('.symbols-view')).not.toExist() rootView.getActiveEditor().trigger "symbols-view:toggle-file-symbols" + symbolsView = rootView.find('.symbols-view').view() setErrorSpy = spyOn(symbolsView, "setError").andCallThrough() waitsFor -> @@ -95,6 +96,7 @@ describe "SymbolsView", -> rootView.open('sample.js') expect(rootView.getActiveEditor().getCursorBufferPosition()).toEqual [0,0] expect(rootView.find('.symbols-view')).not.toExist() + symbolsView = SymbolsView.activate() symbolsView.setArray(tags) symbolsView.attach() expect(rootView.find('.symbols-view')).toExist() @@ -146,6 +148,7 @@ describe "SymbolsView", -> editor = rootView.getActiveEditor() editor.setCursorBufferPosition([8,14]) editor.trigger 'symbols-view:go-to-declaration' + symbolsView = rootView.find('.symbols-view').view() expect(symbolsView.list.children('li').length).toBe 2 expect(symbolsView).toBeVisible() symbolsView.confirmed(symbolsView.array[0]) @@ -164,6 +167,7 @@ describe "SymbolsView", -> editor = rootView.getActiveEditor() editor.setCursorBufferPosition([8,14]) editor.trigger 'symbols-view:go-to-declaration' + symbolsView = rootView.find('.symbols-view').view() expect(symbolsView.list.children('li').length).toBe 1 expect(symbolsView.list.children('li:first').find('.label')).toHaveText 'tagged.js' @@ -172,6 +176,7 @@ describe "SymbolsView", -> rootView.open("tagged.js") expect(rootView.find('.symbols-view')).not.toExist() rootView.trigger "symbols-view:toggle-project-symbols" + symbolsView = rootView.find('.symbols-view').view() expect(symbolsView.find('.loading')).toHaveText 'Loading symbols...' waitsFor -> diff --git a/src/packages/tabs/index.coffee b/src/packages/tabs/index.coffee deleted file mode 100644 index 58a88c08a..000000000 --- a/src/packages/tabs/index.coffee +++ /dev/null @@ -1,6 +0,0 @@ -AtomPackage = require 'atom-package' -TabsView = require './src/tabs-view' - -module.exports = -class Tabs extends AtomPackage - activate: (rootView) -> TabsView.activate(rootView) diff --git a/src/packages/tabs/src/tab.coffee b/src/packages/tabs/lib/tab.coffee similarity index 98% rename from src/packages/tabs/src/tab.coffee rename to src/packages/tabs/lib/tab.coffee index 6a8ba4338..cdb225442 100644 --- a/src/packages/tabs/src/tab.coffee +++ b/src/packages/tabs/lib/tab.coffee @@ -4,7 +4,7 @@ fs = require 'fs' module.exports = class Tab extends View @content: (editSession) -> - @div class: 'tab', => + @li class: 'tab', => @span class: 'file-name', outlet: 'fileName' @span class: 'close-icon' diff --git a/src/packages/tabs/src/tabs-view.coffee b/src/packages/tabs/lib/tabs-view.coffee similarity index 94% rename from src/packages/tabs/src/tabs-view.coffee rename to src/packages/tabs/lib/tabs-view.coffee index d2b21fa7c..85cd3887d 100644 --- a/src/packages/tabs/src/tabs-view.coffee +++ b/src/packages/tabs/lib/tabs-view.coffee @@ -1,10 +1,10 @@ $ = require 'jquery' {View} = require 'space-pen' -Tab = require 'tabs/src/tab' +Tab = require './tab' module.exports = class Tabs extends View - @activate: (rootView) -> + @activate: -> rootView.eachEditor (editor) => @prependToEditorPane(rootView, editor) if editor.attached @@ -13,7 +13,7 @@ class Tabs extends View pane.prepend(new Tabs(editor)) @content: -> - @div class: 'tabs' + @ul class: 'tabs' initialize: (@editor) -> for editSession, index in @editor.editSessions diff --git a/src/packages/tabs/package.cson b/src/packages/tabs/package.cson new file mode 100644 index 000000000..6cc9d8565 --- /dev/null +++ b/src/packages/tabs/package.cson @@ -0,0 +1 @@ +'main': 'lib/tabs-view' diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index e5b821204..a9bc5b358 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -1,11 +1,10 @@ $ = require 'jquery' _ = require 'underscore' RootView = require 'root-view' -Tabs = require 'tabs' fs = require 'fs' describe "Tabs", -> - [rootView, editor, buffer, tabs] = [] + [editor, buffer, tabs] = [] beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) diff --git a/src/packages/tabs/stylesheets/tabs.css b/src/packages/tabs/stylesheets/tabs.css deleted file mode 100644 index ebdfad7f2..000000000 --- a/src/packages/tabs/stylesheets/tabs.css +++ /dev/null @@ -1,33 +0,0 @@ -.tabs { - -webkit-user-select: none; - display: -webkit-box; - -webkit-box-align: center; -} - -.tab { - -webkit-box-flex: 2; - position: relative; - width: 175px; - max-width: 175px; - min-width: 40px; - box-sizing: border-box; -} - -.tab.active { - -webkit-box-flex: 1; -} - -.tab.file-modified .close-icon { - top: 6px; - width: 5px; - height: 5px; - right: 5px; -} - -.tab .file-name { - display: block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - padding: 3px 5px; -} \ No newline at end of file diff --git a/src/packages/tree-view/index.coffee b/src/packages/tree-view/index.coffee deleted file mode 100644 index b2228f183..000000000 --- a/src/packages/tree-view/index.coffee +++ /dev/null @@ -1,26 +0,0 @@ -DeferredAtomPackage = require 'deferred-atom-package' - -module.exports = -class Tree extends DeferredAtomPackage - - loadEvents: [ - 'tree-view:toggle' - 'tree-view:reveal-active-file' - ] - - instanceClass: 'tree-view/src/tree-view' - - activate: (rootView, state) -> - super - - if state - @getInstance().attach() if state.attached - else if rootView.project.getPath() and not rootView.pathToOpenIsFile - @getInstance().attach() - - onLoadEvent: (event, instance) -> - switch event.type - when 'tree-view:toggle' - instance.toggle() - when 'tree-view:reveal-active-file' - instance.revealActiveFile() diff --git a/src/packages/tree-view/src/dialog.coffee b/src/packages/tree-view/lib/dialog.coffee similarity index 100% rename from src/packages/tree-view/src/dialog.coffee rename to src/packages/tree-view/lib/dialog.coffee diff --git a/src/packages/tree-view/src/directory-view.coffee b/src/packages/tree-view/lib/directory-view.coffee similarity index 98% rename from src/packages/tree-view/src/directory-view.coffee rename to src/packages/tree-view/lib/directory-view.coffee index 3614f79da..29bd8e767 100644 --- a/src/packages/tree-view/src/directory-view.coffee +++ b/src/packages/tree-view/lib/directory-view.coffee @@ -1,5 +1,5 @@ {View, $$} = require 'space-pen' -FileView = require 'tree-view/src/file-view' +FileView = require './file-view' Directory = require 'directory' $ = require 'jquery' Git = require 'git' diff --git a/src/packages/tree-view/src/file-view.coffee b/src/packages/tree-view/lib/file-view.coffee similarity index 100% rename from src/packages/tree-view/src/file-view.coffee rename to src/packages/tree-view/lib/file-view.coffee diff --git a/src/packages/tree-view/src/tree-view.coffee b/src/packages/tree-view/lib/tree-view.coffee similarity index 85% rename from src/packages/tree-view/src/tree-view.coffee rename to src/packages/tree-view/lib/tree-view.coffee index 038f3d6bc..c48400895 100644 --- a/src/packages/tree-view/src/tree-view.coffee +++ b/src/packages/tree-view/lib/tree-view.coffee @@ -1,35 +1,28 @@ {View, $$} = require 'space-pen' ScrollView = require 'scroll-view' Directory = require 'directory' -DirectoryView = require 'tree-view/src/directory-view' -FileView = require 'tree-view/src/file-view' -Dialog = require 'tree-view/src/dialog' -Native = require 'native' +DirectoryView = require './directory-view' +FileView = require './file-view' +Dialog = require './dialog' fs = require 'fs' $ = require 'jquery' _ = require 'underscore' module.exports = class TreeView extends ScrollView - @activate: (rootView, state) -> + @activate: (state) -> if state - @instance = TreeView.deserialize(state, rootView) + TreeView.deserialize(state) else - @instance = new TreeView(rootView) - - @deactivate: -> - @instance.deactivate() - - @serialize: -> - @instance.serialize() + new TreeView @content: (rootView) -> @div class: 'tree-view-wrapper', => @ol class: 'tree-view tool-panel', tabindex: -1, outlet: 'treeViewList' @div class: 'tree-view-resizer', outlet: 'resizer' - @deserialize: (state, rootView) -> - treeView = new TreeView(rootView) + @deserialize: (state) -> + treeView = new TreeView treeView.root.deserializeEntryExpansionStates(state.directoryExpansionStates) treeView.selectEntryForPath(state.selectedPath) treeView.focusAfterAttach = state.hasFocus @@ -43,7 +36,7 @@ class TreeView extends ScrollView scrollTopAfterAttach: -1 selectedPath: null - initialize: (@rootView) -> + initialize: -> super @on 'click', '.entry', (e) => @entryClicked(e) @on 'mousedown', '.tree-view-resizer', (e) => @resizeStarted(e) @@ -56,15 +49,15 @@ class TreeView extends ScrollView @command 'tree-view:move', => @moveSelectedEntry() @command 'tree-view:add', => @add() @command 'tree-view:remove', => @removeSelectedEntry() - @command 'tool-panel:unfocus', => @rootView.focus() + @command 'tool-panel:unfocus', => rootView.focus() @command 'tree-view:directory-modified', => if @hasFocus() @selectEntryForPath(@selectedPath) if @selectedPath else @selectActiveFile() - @rootView.on 'root-view:active-path-changed', => @selectActiveFile() - @rootView.project.on 'path-changed', => @updateRoot() + rootView.on 'root-view:active-path-changed', => @selectActiveFile() + rootView.project.on 'path-changed', => @updateRoot() @observeConfig 'core.hideGitIgnoredFiles', => @updateRoot() @selectEntry(@root) if @root @@ -95,13 +88,13 @@ class TreeView extends ScrollView attach: -> return unless rootView.project.getPath() - @rootView.horizontal.prepend(this) + rootView.horizontal.prepend(this) @focus() detach: -> @scrollTopAfterAttach = @scrollTop() super - @rootView.focus() + rootView.focus() focus: -> @treeViewList.focus() @@ -117,7 +110,7 @@ class TreeView extends ScrollView @openSelectedEntry(false) if entry instanceof FileView when 2 if entry.is('.selected.file') - @rootView.getActiveEditor().focus() + rootView.getActiveEditor().focus() else if entry.is('.selected.directory') entry.toggleExpansion() @@ -138,22 +131,22 @@ class TreeView extends ScrollView updateRoot: -> @root?.remove() - if rootDirectory = @rootView.project.getRootDirectory() - @root = new DirectoryView(directory: rootDirectory, isExpanded: true, project: @rootView.project) + if rootDirectory = rootView.project.getRootDirectory() + @root = new DirectoryView(directory: rootDirectory, isExpanded: true, project: rootView.project) @treeViewList.append(@root) else @root = null selectActiveFile: -> - activeFilePath = @rootView.getActiveEditor()?.getPath() + activeFilePath = rootView.getActiveEditor()?.getPath() @selectEntryForPath(activeFilePath) if activeFilePath revealActiveFile: -> @attach() - return unless activeFilePath = @rootView.getActiveEditor()?.getPath() + return unless activeFilePath = rootView.getActiveEditor()?.getPath() - project = @rootView.project + project = rootView.project activePathComponents = project.relativize(activeFilePath).split('/') currentPath = project.getPath().replace(/\/$/, '') for pathComponent in activePathComponents @@ -220,7 +213,7 @@ class TreeView extends ScrollView if selectedEntry instanceof DirectoryView selectedEntry.view().toggleExpansion() else if selectedEntry instanceof FileView - @rootView.open(selectedEntry.getPath(), { changeFocus }) + rootView.open(selectedEntry.getPath(), { changeFocus }) moveSelectedEntry: -> entry = @selectedEntry() @@ -233,11 +226,11 @@ class TreeView extends ScrollView dialog = new Dialog prompt: prompt - path: @rootView.project.relativize(oldPath) + path: rootView.project.relativize(oldPath) select: true iconClass: 'move' onConfirm: (newPath) => - newPath = @rootView.project.resolve(newPath) + newPath = rootView.project.resolve(newPath) directoryPath = fs.directory(newPath) try fs.makeTree(directoryPath) unless fs.exists(directoryPath) @@ -246,7 +239,7 @@ class TreeView extends ScrollView catch e dialog.showError("Error: #{e.message} Try a different path.") - @rootView.append(dialog) + rootView.append(dialog) removeSelectedEntry: -> entry = @selectedEntry() @@ -256,7 +249,7 @@ class TreeView extends ScrollView atom.confirm( "Are you sure you would like to delete the selected #{entryType}?", "You are deleting #{entry.getPath()}", - "Move to Trash", (=> Native.moveToTrash(entry.getPath())), + "Move to Trash", (=> $native.moveToTrash(entry.getPath())), "Cancel", null "Delete", (=> fs.remove(entry.getPath())) ) @@ -265,7 +258,7 @@ class TreeView extends ScrollView selectedEntry = @selectedEntry() or @root selectedPath = selectedEntry.getPath() directoryPath = if fs.isFile(selectedPath) then fs.directory(selectedPath) else selectedPath - relativeDirectoryPath = @rootView.project.relativize(directoryPath) + relativeDirectoryPath = rootView.project.relativize(directoryPath) relativeDirectoryPath += '/' if relativeDirectoryPath.length > 0 dialog = new Dialog @@ -275,7 +268,7 @@ class TreeView extends ScrollView iconClass: 'add' onConfirm: (relativePath) => endsWithDirectorySeparator = /\/$/.test(relativePath) - path = @rootView.project.resolve(relativePath) + path = rootView.project.resolve(relativePath) try if fs.exists(path) pathType = if fs.isFile(path) then "file" else "directory" @@ -287,12 +280,12 @@ class TreeView extends ScrollView @selectEntryForPath(path) else fs.write(path, "") - @rootView.open(path) + rootView.open(path) dialog.close() catch e dialog.showError("Error: #{e.message} Try a different path.") - @rootView.append(dialog) + rootView.append(dialog) selectedEntry: -> @treeViewList.find('.selected')?.view() diff --git a/src/packages/tree-view/lib/tree.coffee b/src/packages/tree-view/lib/tree.coffee new file mode 100644 index 000000000..86b4f8fb4 --- /dev/null +++ b/src/packages/tree-view/lib/tree.coffee @@ -0,0 +1,27 @@ +module.exports = + treeView: null + + activate: (@state) -> + if state + @createView().attach() if state.attached + else if rootView.project.getPath() and not rootView.pathToOpenIsFile + @createView().attach() + + rootView.command 'tree-view:toggle', => @createView().toggle() + rootView.command 'tree-view:reveal-active-file', => @createView().revealActiveFile() + + deactivate: -> + @treeView?.deactivate() + @treeView = null + + serialize: -> + if @treeView? + @treeView.serialize() + else + @state + + createView: -> + unless @treeView? + TreeView = require 'tree-view/lib/tree-view' + @treeView = TreeView.activate(@state) + @treeView diff --git a/src/packages/tree-view/package.cson b/src/packages/tree-view/package.cson new file mode 100644 index 000000000..4bbfd3695 --- /dev/null +++ b/src/packages/tree-view/package.cson @@ -0,0 +1 @@ +'main': 'lib/tree' diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index ab6d16d1a..8459a06ae 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -1,19 +1,18 @@ $ = require 'jquery' _ = require 'underscore' -TreeView = require 'tree-view/src/tree-view' +TreeView = require 'tree-view/lib/tree-view' RootView = require 'root-view' Directory = require 'directory' -Native = require 'native' fs = require 'fs' describe "TreeView", -> - [rootView, project, treeView, sampleJs, sampleTxt] = [] + [project, treeView, sampleJs, sampleTxt] = [] beforeEach -> - rootView = new RootView(require.resolve('fixtures/tree-view')) + new RootView(require.resolve('fixtures/tree-view')) project = rootView.project - treeView = atom.loadPackage("tree-view").getInstance() + treeView = atom.loadPackage("tree-view").packageMain.createView() treeView.root = treeView.find('ol > li:first').view() sampleJs = treeView.find('.file:contains(tree-view.js)') sampleTxt = treeView.find('.file:contains(tree-view.txt)') @@ -21,7 +20,7 @@ describe "TreeView", -> expect(treeView.root.directory.subscriptionCount()).toBeGreaterThan 0 afterEach -> - rootView?.deactivate() + rootView.deactivate() describe ".initialize(project)", -> it "renders the root of the project and its contents alphabetically with subdirectories first in a collapsed state", -> @@ -49,8 +48,8 @@ describe "TreeView", -> beforeEach -> rootView.deactivate() - rootView = new RootView - treeView = atom.loadPackage("tree-view").getInstance() + new RootView + treeView = atom.loadPackage("tree-view").packageMain.createView() it "does not attach to the root view or create a root node when initialized", -> expect(treeView.hasParent()).toBeFalsy() @@ -75,8 +74,8 @@ describe "TreeView", -> beforeEach -> rootView.deactivate() - rootView = new RootView(require.resolve('fixtures/tree-view/tree-view.js')) - treeView = atom.loadPackage("tree-view").getInstance() + new RootView(require.resolve('fixtures/tree-view/tree-view.js')) + treeView = atom.loadPackage("tree-view").packageMain.createView() it "does not attach to the root view but does create a root node when initialized", -> expect(treeView.hasParent()).toBeFalsy() @@ -91,10 +90,11 @@ describe "TreeView", -> it "restores expanded directories and selected file when deserialized", -> treeView.find('.directory:contains(dir1)').click() sampleJs.click() + oldRootView = rootView newRootView = RootView.deserialize(rootView.serialize()) - rootView.deactivate() # Deactivates previous TreeView + oldRootView.deactivate() # Deactivates previous TreeView - newRootView.activatePackage('tree-view', TreeView) + atom.loadPackage('tree-view') newTreeView = newRootView.find(".tree-view").view() @@ -107,11 +107,12 @@ describe "TreeView", -> treeView.focus() expect(treeView.find(".tree-view")).toMatchSelector ':focus' + oldRootView = rootView newRootView = RootView.deserialize(rootView.serialize()) - rootView.deactivate() # Deactivates previous TreeView + oldRootView.deactivate() # Deactivates previous TreeView newRootView.attachToDom() - newRootView.activatePackage('tree-view', TreeView) + atom.loadPackage('tree-view') newTreeView = newRootView.find(".tree-view").view() expect(newTreeView.find(".tree-view")).toMatchSelector ':focus' @@ -605,7 +606,7 @@ describe "TreeView", -> fs.makeDirectory(dirPath) fs.write(filePath, "doesn't matter") - rootView = new RootView(rootDirPath) + new RootView(rootDirPath) project = rootView.project atom.loadPackage('tree-view') treeView = rootView.find(".tree-view").view() @@ -615,7 +616,6 @@ describe "TreeView", -> afterEach -> rootView.deactivate() - rootView = null fs.remove(rootDirPath) if fs.exists(rootDirPath) describe "tree-view:add", -> diff --git a/src/packages/wrap-guide/index.coffee b/src/packages/wrap-guide/index.coffee deleted file mode 100644 index 1f9ab8ffd..000000000 --- a/src/packages/wrap-guide/index.coffee +++ /dev/null @@ -1,6 +0,0 @@ -AtomPackage = require 'atom-package' -WrapGuideView = require './src/wrap-guide-view' - -module.exports = -class WrapGuide extends AtomPackage - activate: (rootView) -> WrapGuideView.activate(rootView) diff --git a/src/packages/wrap-guide/src/wrap-guide-view.coffee b/src/packages/wrap-guide/lib/wrap-guide-view.coffee similarity index 91% rename from src/packages/wrap-guide/src/wrap-guide-view.coffee rename to src/packages/wrap-guide/lib/wrap-guide-view.coffee index 35dd192d8..2035e8157 100644 --- a/src/packages/wrap-guide/src/wrap-guide-view.coffee +++ b/src/packages/wrap-guide/lib/wrap-guide-view.coffee @@ -4,20 +4,20 @@ _ = require 'underscore' module.exports = class WrapGuideView extends View - @activate: (rootView, state) -> + @activate: -> rootView.eachEditor (editor) => @appendToEditorPane(rootView, editor) if editor.attached @appendToEditorPane: (rootView, editor) -> if underlayer = editor.pane()?.find('.underlayer') - underlayer.append(new WrapGuideView(rootView, editor)) + underlayer.append(new WrapGuideView(editor)) @content: -> @div class: 'wrap-guide' defaultColumn: 80 - initialize: (@rootView, @editor) => + initialize: (@editor) => @observeConfig 'editor.fontSize', => @updateGuide() @subscribe @editor, 'editor:path-changed', => @updateGuide() @subscribe @editor, 'editor:min-width-changed', => @updateGuide() diff --git a/src/packages/wrap-guide/package.cson b/src/packages/wrap-guide/package.cson new file mode 100644 index 000000000..874306120 --- /dev/null +++ b/src/packages/wrap-guide/package.cson @@ -0,0 +1 @@ +'main': 'lib/wrap-guide-view' diff --git a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee index 6e38845be..e557555e8 100644 --- a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee +++ b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee @@ -2,10 +2,10 @@ $ = require 'jquery' RootView = require 'root-view' describe "WrapGuide", -> - [rootView, editor, wrapGuide] = [] + [editor, wrapGuide] = [] beforeEach -> - rootView = new RootView(require.resolve('fixtures/sample.js')) + new RootView(require.resolve('fixtures/sample.js')) atom.loadPackage('wrap-guide') rootView.attachToDom() editor = rootView.getActiveEditor() diff --git a/src/stdlib/native.coffee b/src/stdlib/native.coffee deleted file mode 100644 index 04c05ae32..000000000 --- a/src/stdlib/native.coffee +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = -class Native - @reload: -> $native.reload() - - @moveToTrash: (args...) -> $native.moveToTrash(args...) diff --git a/src/stdlib/require.coffee b/src/stdlib/require.coffee index 4a6f919aa..a51d89f11 100644 --- a/src/stdlib/require.coffee +++ b/src/stdlib/require.coffee @@ -63,19 +63,9 @@ exts = compiled = CoffeeScript.compile(__read(file), filename: file) writeToCache = true - try - evaluated = exts.js(file, compiled) - $native.write(cacheFilePath, compiled) if writeToCache - evaluated - catch e - if retry - # Attempt a second compile to work around mysterious CEF/CoffeeScript - # timing issue where the CoffeeScript compiler generates invalid - # JavaScript such as [object Object]. - console.warn "Error evaluating #{file}. Trying again...", e.stack - exts.coffee(file, false) - else - throw e + evaluated = exts.js(file, compiled) + $native.write(cacheFilePath, compiled) if writeToCache + evaluated getPath = (path) -> path = resolve(path) diff --git a/src/stdlib/task.coffee b/src/stdlib/task.coffee index 4a66a7042..146a62928 100644 --- a/src/stdlib/task.coffee +++ b/src/stdlib/task.coffee @@ -1,10 +1,15 @@ module.exports = class Task + terminated: false + constructor: (@path) -> start: -> + throw new Error("Task already started") if @worker? + @worker = new Worker(require.getPath('task-shell')) @worker.onmessage = ({data}) => + return if @terminated if data.method and this[data.method] this[data.method](data.args...) else @@ -35,4 +40,7 @@ class Task @worker.postMessage(data) terminate: -> - @worker.terminate() + unless @terminated + @terminated = true + @worker?.terminate() + @worker = null diff --git a/static/command-panel.css b/static/command-panel.css index 5f10228c4..3fa292585 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -1,11 +1,12 @@ .command-panel { padding: 5px; + position: relative; } .command-panel .preview-list { max-height: 300px; overflow: auto; - margin: 0 1px 5px 10px; + margin: 10px 0 10px 10px; position: relative; cursor: default; } @@ -13,7 +14,10 @@ .command-panel .preview-count { font-size: 11px; text-align: right; - padding-bottom: 1px; + position: absolute; + right: 15px; + top: 24px; + z-index: 9999; } .command-panel .preview-list .path { diff --git a/static/overlay.css b/static/overlay.css index 421b2f71f..60bd0a88f 100644 --- a/static/overlay.css +++ b/static/overlay.css @@ -1,15 +1,14 @@ .overlay { position: absolute; - top: 0; left: 50%; width: 500px; margin-left: -250px; - background: #303030; + background: #202123; color: #eee; padding: 10px; z-index: 9999; box-sizing: border-box; - border: 1px solid rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.05); box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); border-radius: 3px; } @@ -26,10 +25,19 @@ .overlay.mini { width: 200px; margin-left: -100px; + font-size: 12px; } .overlay.from-top { + top: 0; border-top: none; border-top-left-radius: 0; border-top-right-radius: 0; +} + +.overlay.from-bottom { + bottom: 0; + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } \ No newline at end of file diff --git a/static/status-bar.css b/static/status-bar.css index fe46c8408..4cc898c41 100644 --- a/static/status-bar.css +++ b/static/status-bar.css @@ -1,16 +1,25 @@ .status-bar { - padding: 4px 10px 3px; + padding: 5px 10px; font-size: 11px; line-height: 14px; + position: relative; + -webkit-user-select: none; + cursor: default; +} + +.status-bar .git-branch { + float: right; } .status-bar .cursor-position, .status-bar .grammar-name { - padding-left: 10px; + margin-left: 10px; } .status-bar .grammar-name { cursor: pointer; + border: 1px solid transparent; + padding: 1px 2px; } .status-bar .branch-label { diff --git a/static/tabs.css b/static/tabs.css index 2a4faa887..a6c9cb8b8 100644 --- a/static/tabs.css +++ b/static/tabs.css @@ -1,59 +1,45 @@ .tabs { font: caption; - margin-bottom: 1px; + -webkit-user-select: none; + display: -webkit-box; + -webkit-box-align: center; } .tab { cursor: default; - padding: 2px 21px 2px 9px; + -webkit-box-flex: 2; + position: relative; + width: 175px; + max-width: 175px; + min-width: 40px; + box-sizing: border-box; + text-shadow: -1px -1px 0 #000; + font-size: 11px; + padding: 5px 10px; } -.tab.file-modified .close-icon { - border-radius: 10px; -} - -.tab.file-modified .close-icon:before { - content: ""; -} - -.tab.active:before, -.tab.active:after { - position: absolute; - bottom: -1px; - width: 4px; - height: 4px; - content: " "; - z-index: 3; -} - -.tab.active:before { - left: -4px; -} - -.tab.active:after { - right: -4px; - border-width: 0 0 1px 1px; -} - -.tab.active:first-child:before { - display: none; +.tab.active { + -webkit-box-flex: 1; } .tab .file-name { - font-size: 11px; - text-shadow: 0 -1px 1px black; + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 3px 5px; } .tab .close-icon { font-family: 'Octicons Regular'; - font-size: 14px; - width: 14px; - height: 14px; - display: block; + font-size: 12px; + width: 12px; + height: 12px; cursor: pointer; position: absolute; - right: 4px; - top: -1px; + color: rgba(255, 255, 255, 0.5); + right: 5px; + top: 4px; -webkit-font-smoothing: antialiased; } @@ -62,5 +48,31 @@ } .tab .close-icon:hover { - color: white; + color: #fff; +} + +.tab.file-modified .close-icon { + top: 10px; + width: 5px; + height: 5px; + right: 6px; + border: 2px solid #66a6ff; + border-radius: 12px; +} + +.tab.file-modified .close-icon:before { + content: ""; +} + +.tab.file-modified:hover .close-icon { + border: none; + width: 12px; + height: 12px; + right: 5px; + top: 4px; +} + +.tab.file-modified:hover .close-icon:before { + content: "\f081"; + color: #66a6ff; } \ No newline at end of file diff --git a/themes/atom-dark-syntax.css b/themes/atom-dark-syntax.css index 5f19316c2..23a1d9e38 100644 --- a/themes/atom-dark-syntax.css +++ b/themes/atom-dark-syntax.css @@ -15,6 +15,12 @@ background-color: rgba(255, 255, 255, 0.14); } +.bracket-matcher { + border-bottom: 1px solid #f8de7e; + margin-top: -1px; + opacity: .7; +} + .comment { color: #7C7C7C; } diff --git a/themes/atom-dark-ui/blurred.css b/themes/atom-dark-ui/blurred.css index cb9601b51..71b366fba 100644 --- a/themes/atom-dark-ui/blurred.css +++ b/themes/atom-dark-ui/blurred.css @@ -1,21 +1,7 @@ .is-blurred .tree-view { - background-color: #2a2a2a; -} - -.is-blurred .tabs { - opacity: 0.8; - border-bottom-color: #525252; + background-color: #202123; } .is-blurred .tab { - background-image: -webkit-linear-gradient(#444, #555); -} - -.is-blurred .tab.active { - border: 1px solid #525252 -} - -.is-blurred .tab.active:before { - box-shadow: 2px 2px 0 #525252; - border-color: #696969; + opacity: 0.5; } \ No newline at end of file diff --git a/themes/atom-dark-ui/bracket-matcher.css b/themes/atom-dark-ui/bracket-matcher.css deleted file mode 100644 index d579fdc35..000000000 --- a/themes/atom-dark-ui/bracket-matcher.css +++ /dev/null @@ -1,5 +0,0 @@ -.bracket-matcher { - border-bottom: 1px solid #f8de7e; - margin-top: -1px; - opacity: .7; -} diff --git a/themes/atom-dark-ui/command-panel.css b/themes/atom-dark-ui/command-panel.css index f3e8412cd..f2c6648ba 100644 --- a/themes/atom-dark-ui/command-panel.css +++ b/themes/atom-dark-ui/command-panel.css @@ -1,19 +1,24 @@ .command-panel { - background-color: #303030; - border: 1px solid #252525; + background-color: #1b1c1e; color: #dedede; + border-top: 1px solid rgba(0, 0, 0, 0.5); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); + padding: 10px; } .command-panel .preview-list { - background-color: #1e1e1e; + background-color: #19191b; color: #C5C8C6; border: 1px solid rgba(0, 0, 0, 0.5); - border-bottom: 1px solid rgba(180, 180, 180, 0.2); - border-right: 1px solid rgba(180, 180, 180, 0.2); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + border-right: 1px solid rgba(255, 255, 255, 0.05); } .command-panel .preview-count { color: #969696; + background: #161719; + padding: 5px; + border-radius: 3px; } .command-panel .preview-list li.selected, .command-panel .preview-list li.operation:hover { diff --git a/themes/atom-dark-ui/editor.css b/themes/atom-dark-ui/editor.css index 3006ed4ef..27c26d81b 100644 --- a/themes/atom-dark-ui/editor.css +++ b/themes/atom-dark-ui/editor.css @@ -1,7 +1,7 @@ .editor.mini { - border: 1px solid rgba(0, 0, 0, 0.2); - border-bottom: 1px solid rgba(180, 180, 180, 0.2); - border-right: 1px solid rgba(180, 180, 180, 0.2); + border: 1px solid rgba(0, 0, 0, 0.5); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + border-right: 1px solid rgba(255, 255, 255, 0.05); } .editor .gutter.drop-shadow { diff --git a/themes/atom-dark-ui/package.cson b/themes/atom-dark-ui/package.cson new file mode 100644 index 000000000..53831688d --- /dev/null +++ b/themes/atom-dark-ui/package.cson @@ -0,0 +1,11 @@ +'stylesheets': [ + 'atom.css' + 'editor.css' + 'select-list.css' + 'tree-view.css' + 'tabs.css' + 'status-bar.css' + 'command-panel.css' + 'command-logger.css' + 'blurred.css' +] diff --git a/themes/atom-dark-ui/package.json b/themes/atom-dark-ui/package.json deleted file mode 100644 index b223f8deb..000000000 --- a/themes/atom-dark-ui/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "stylesheets":[ - "atom.css", - "editor.css", - "select-list.css", - "tree-view.css", - "tabs.css", - "status-bar.css", - "markdown-preview.css", - "command-panel.css", - "command-logger.css", - "blurred.css", - "bracket-matcher.css" - ] -} diff --git a/themes/atom-dark-ui/select-list.css b/themes/atom-dark-ui/select-list.css index 235e604ff..2de49745b 100644 --- a/themes/atom-dark-ui/select-list.css +++ b/themes/atom-dark-ui/select-list.css @@ -1,5 +1,5 @@ .select-list ol li { - background-color: #292929; + background-color: #27292b; border-bottom: 1px solid #1e1e1e; } diff --git a/themes/atom-dark-ui/status-bar.css b/themes/atom-dark-ui/status-bar.css index 4ca4f2dc5..1c0af57e0 100644 --- a/themes/atom-dark-ui/status-bar.css +++ b/themes/atom-dark-ui/status-bar.css @@ -1,9 +1,16 @@ .status-bar { - background-color: #303030; - border-top: 1px solid #454545; + background-color: #1b1c1e; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); + border-top: 1px solid rgba(0, 0, 0, 0.8); color: #969696; } +.status-bar .grammar-name:hover { + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 2px; +} + .status-bar .git-status.octicons.modified-status-icon { color: #f78a46; display: inline-block; diff --git a/themes/atom-dark-ui/tabs.css b/themes/atom-dark-ui/tabs.css index 81b52334e..ec3b0f7b1 100644 --- a/themes/atom-dark-ui/tabs.css +++ b/themes/atom-dark-ui/tabs.css @@ -1,80 +1,38 @@ .tabs { - background: #333333; - border-bottom: 4px solid #424242; - box-shadow: inset 0 -1px 0 #2e2e2e, 0 1px 0 #191919; + background: #161719; + border: none; + box-shadow: none; + border-bottom: 4px solid rgba(41, 44, 47, 1); + box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.2); } .tab { - background-image: -webkit-linear-gradient(#444, #3d3d3d); - border-top: 1px solid #383838; - border-right: 1px solid #2e2e2e; - border-bottom: 1px solid #2e2e2e; - box-shadow: inset 0 0 5px #383838, 0 1px 0 #585858, inset -1px 0 0 #4a4a4a, inset 1px 0 0 #4a4a4a; -} - - .tab:first-child { - box-shadow: inset 0 0 5px #383838, 0 1px 0 #585858, inset -1px 0 0 #4a4a4a; -} - -.tab.active, -.tab.active:hover { - border-top: 1px solid #4a4a4a; - box-shadow: inset -1px 0 0 #595959, inset 1px 0 0 #595959; - border-bottom-color: #424242; - background-image: -webkit-linear-gradient(#555555, #424242); -} - -.tab, -.tab .close-icon { - color: #aaa; -} - -.tab.file-modified .close-icon { - border-color: #aaa; -} - -.tab.active, -.tab.active:hover, -.tab.active .close-icon { - color: #e6e6e6; -} - -.tab.file-modified.active .close-icon { - border-color: #e6e6e6; -} - -.tab:hover .close-icon { - color: #c8c8c8; - border-color: #c8c8c8; -} - -.tab.file-modified .close-icon { - border: 3px solid #777; -} - -.tab.active:first-child, -.tab.active:first-child:hover { - box-shadow: inset -1px 0 0 #595959; -} - -.tab.active:before, -.tab.active:after { - border: 1px solid #595959; -} - -.tab.active:before { - border-bottom-right-radius: 4px; - border-width: 0 1px 1px 0; - box-shadow: 2px 2px 0 #424242; -} - -.tab.active:after { - border-bottom-left-radius: 4px; - border-width: 0 0 1px 1px; - box-shadow: -2px 2px 0 #424242; + margin: 0 0 1px 1px; + background-image: -webkit-linear-gradient(#292c2f, #1d1f21); + box-shadow: + inset 0 2px 0 rgba(255, 255, 255, 0.1), + inset 0 1px 0 rgba(0, 0, 0, 1), + inset 1px 0 0 rgba(255, 255, 255, 0.05), + inset -1px 0 0 rgba(255, 255, 255, 0.05); + color: #b9bdc1; } .tab:hover { - color: #c8c8c8; - background-image: -webkit-linear-gradient(#474747, #444444); + color: #fff; } + +.tab.active { + background-image: -webkit-linear-gradient(#43484d, #303337); + color: #fff; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2); + box-shadow: + inset 0 1px 0 rgba(0, 0, 0, 0.5), + inset 0 2px 0 rgba(255, 255, 255, 0.1), + inset 1px 0 0 rgba(255, 255, 255, 0.1), + inset -1px 0 0 rgba(255, 255, 255, 0.1), + inset 0 -1px 0 rgba(0, 0, 0, 0.3); +} + +.tab.active:hover .close-icon { + color: #fff; +} \ No newline at end of file diff --git a/themes/atom-dark-ui/tree-view.css b/themes/atom-dark-ui/tree-view.css index a51e58c44..26851216e 100644 --- a/themes/atom-dark-ui/tree-view.css +++ b/themes/atom-dark-ui/tree-view.css @@ -1,6 +1,9 @@ .tree-view { - background: #1e1e1e; - border-right: 1px solid #191919; + background: #1b1c1e; + box-shadow: + 1px 0 0 #131516, + inset -1px 0 0 rgba(255, 255, 255, 0.02), + 1px 0 3px rgba(0, 0, 0, 0.2); } .tree-view .entry { diff --git a/themes/atom-light-syntax.css b/themes/atom-light-syntax.css index c86205d69..c9bfc94de 100644 --- a/themes/atom-light-syntax.css +++ b/themes/atom-light-syntax.css @@ -20,6 +20,12 @@ font-style: italic; } +.bracket-matcher { + background-color: #C9C9C9; + opacity: .7; + border-bottom: 0 none; +} + .editor .string { color: #D14; } diff --git a/themes/atom-light-ui/blurred.css b/themes/atom-light-ui/blurred.css index cc085ff29..9820f2f96 100644 --- a/themes/atom-light-ui/blurred.css +++ b/themes/atom-light-ui/blurred.css @@ -1,23 +1,23 @@ -.is-blurred .tab { - background-image: none; - background-color: #e0e0e0); - border-top: none; - border-right: 1px solid #959595; - border-bottom: 1px solid #959595; - box-shadow: inset 0 0 5px #eee, 0 1px 0 #eee, inset -1px 0 0 #e0e0e0, inset 1px 0 0 #e0e0e0; - color: #323232; -} - -.is-blurred .tab.active { - border-bottom: 1px solid #e5e5e5; - box-shadow: inset 0 0 5px #eee, inset -1px 0 0 #e0e0e0, inset 1px 0 0 #e0e0e0; -} - .is-blurred .tree-view { background: #f3f3f3; border-right: 1px solid #c5c5c5; } +.is-blurred .tab { + background-image: -webkit-linear-gradient(#e7e7e7, #cfcfcf); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.5), /* top hightlight */ + inset 1px 0 0 rgba(255, 255, 255, 0.3), /* left highlight */ + inset 0 -1px 0 rgba(255, 255, 255, 0.2), /* bottom shadow */ + inset -1px 0 0 rgba(0, 0, 0, 0.2), /* right shadow */ + inset -2px 0 0 rgba(255, 255, 255, 0.1); /* right hightlight */ + color: #777; +} + +.is-blurred .tab .close-icon { + color: #777; +} + .is-blurred .tree-view:focus .selected > .highlight { border-top: 1px solid #3D4552; border-bottom: 1px solid #3D4552; @@ -35,4 +35,4 @@ .is-blurred .tree-view .name:before { color: #7e7e7e; -} +} \ No newline at end of file diff --git a/themes/atom-light-ui/bracket-matcher.css b/themes/atom-light-ui/bracket-matcher.css deleted file mode 100644 index d579fdc35..000000000 --- a/themes/atom-light-ui/bracket-matcher.css +++ /dev/null @@ -1,5 +0,0 @@ -.bracket-matcher { - border-bottom: 1px solid #f8de7e; - margin-top: -1px; - opacity: .7; -} diff --git a/themes/atom-light-ui/markdown-preview.css b/themes/atom-light-ui/markdown-preview.css deleted file mode 100644 index 5d58e2de7..000000000 --- a/themes/atom-light-ui/markdown-preview.css +++ /dev/null @@ -1,442 +0,0 @@ -.markdown-preview { - position: absolute; - width: 100%; - height: 100%; - top: 0px; - left: 0px; - background-color: #F4F4F4; - overflow: auto; - z-index: 3; - box-sizing: border-box; - padding: 20px; -} - -.markdown-body { - background-color: #fff; - box-shadow: rgba(0, 0, 0, 0.1) 0 0 0 1px,rgba(0, 0, 0, 0.3) 0 1px 3px; - border-radius: 5px; - max-width: 914px; - min-width: 680px; - margin-left: auto; - margin-right: auto; - padding: 30px; -} - -.markdown-body pre, -.markdown-body code, -.markdown-body tt { - font-size: 12px; - font-family: Consolas, "Liberation Mono", Courier, monospace; -} - -.markdown-body a { - color: #4183c4; -} - -.markdown-body ol > li { - list-style-type: decimal; -} - -.markdown-body ul > li { - list-style-type: disc; -} - -.markdown-spinner { - margin: auto; - background-image: url(images/octocat-spinner-128.gif); - background-repeat: no-repeat; - background-size: 64px; - background-position: top center; - padding-top: 70px; - text-align: center; -} - - -/* this code below was copied from https://github.com/assets/stylesheets/primer/components/markdown.css */ -/* we really need to get primer in here somehow. */ -.markdown-body { - font-size: 14px; - line-height: 1.6; - overflow: hidden; } - .markdown-body > *:first-child { - margin-top: 0 !important; } - .markdown-body > *:last-child { - margin-bottom: 0 !important; } - .markdown-body a.absent { - color: #c00; } - .markdown-body a.anchor { - display: block; - padding-left: 30px; - margin-left: -30px; - cursor: pointer; - position: absolute; - top: 0; - left: 0; - bottom: 0; } - .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { - margin: 20px 0 10px; - padding: 0; - font-weight: bold; - -webkit-font-smoothing: antialiased; - cursor: text; - position: relative; } - .markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link { - display: none; - color: #000; } - .markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor { - text-decoration: none; - line-height: 1; - padding-left: 0; - margin-left: -22px; - top: 15%; } - .markdown-body h1:hover a.anchor .mini-icon-link, .markdown-body h2:hover a.anchor .mini-icon-link, .markdown-body h3:hover a.anchor .mini-icon-link, .markdown-body h4:hover a.anchor .mini-icon-link, .markdown-body h5:hover a.anchor .mini-icon-link, .markdown-body h6:hover a.anchor .mini-icon-link { - display: inline-block; } - .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { - font-size: inherit; } - .markdown-body h1 { - font-size: 28px; - color: #000; } - .markdown-body h2 { - font-size: 24px; - border-bottom: 1px solid #ccc; - color: #000; } - .markdown-body h3 { - font-size: 18px; } - .markdown-body h4 { - font-size: 16px; } - .markdown-body h5 { - font-size: 14px; } - .markdown-body h6 { - color: #777; - font-size: 14px; } - .markdown-body p, - .markdown-body blockquote, - .markdown-body ul, .markdown-body ol, .markdown-body dl, - .markdown-body table, - .markdown-body pre { - margin: 15px 0; } - .markdown-body hr { - background: transparent url("https://a248.e.akamai.net/assets.github.com/assets/primer/markdown/dirty-shade-0e7d81b119cc9beae17b0c98093d121fa0050a74.png") repeat-x 0 0; - border: 0 none; - color: #ccc; - height: 4px; - padding: 0; } - .markdown-body > h2:first-child, .markdown-body > h1:first-child, .markdown-body > h1:first-child + h2, .markdown-body > h3:first-child, .markdown-body > h4:first-child, .markdown-body > h5:first-child, .markdown-body > h6:first-child { - margin-top: 0; - padding-top: 0; } - .markdown-body a:first-child h1, .markdown-body a:first-child h2, .markdown-body a:first-child h3, .markdown-body a:first-child h4, .markdown-body a:first-child h5, .markdown-body a:first-child h6 { - margin-top: 0; - padding-top: 0; } - .markdown-body h1 + p, - .markdown-body h2 + p, - .markdown-body h3 + p, - .markdown-body h4 + p, - .markdown-body h5 + p, - .markdown-body h6 + p { - margin-top: 0; } - .markdown-body li p.first { - display: inline-block; } - .markdown-body ul, .markdown-body ol { - padding-left: 30px; } - .markdown-body ul.no-list, .markdown-body ol.no-list { - list-style-type: none; - padding: 0; } - .markdown-body ul li > :first-child, - .markdown-body ul li ul:first-of-type, .markdown-body ol li > :first-child, - .markdown-body ol li ul:first-of-type { - margin-top: 0px; } - .markdown-body ul ul, - .markdown-body ul ol, - .markdown-body ol ol, - .markdown-body ol ul { - margin-bottom: 0; } - .markdown-body dl { - padding: 0; } - .markdown-body dl dt { - font-size: 14px; - font-weight: bold; - font-style: italic; - padding: 0; - margin: 15px 0 5px; } - .markdown-body dl dt:first-child { - padding: 0; } - .markdown-body dl dt > :first-child { - margin-top: 0px; } - .markdown-body dl dt > :last-child { - margin-bottom: 0px; } - .markdown-body dl dd { - margin: 0 0 15px; - padding: 0 15px; } - .markdown-body dl dd > :first-child { - margin-top: 0px; } - .markdown-body dl dd > :last-child { - margin-bottom: 0px; } - .markdown-body blockquote { - border-left: 4px solid #DDD; - padding: 0 15px; - color: #777; } - .markdown-body blockquote > :first-child { - margin-top: 0px; } - .markdown-body blockquote > :last-child { - margin-bottom: 0px; } - .markdown-body table th { - font-weight: bold; } - .markdown-body table th, .markdown-body table td { - border: 1px solid #ccc; - padding: 6px 13px; } - .markdown-body table tr { - border-top: 1px solid #ccc; - background-color: #fff; } - .markdown-body table tr:nth-child(2n) { - background-color: #f8f8f8; } - .markdown-body img { - max-width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; } - .markdown-body span.frame { - display: block; - overflow: hidden; } - .markdown-body span.frame > span { - border: 1px solid #ddd; - display: block; - float: left; - overflow: hidden; - margin: 13px 0 0; - padding: 7px; - width: auto; } - .markdown-body span.frame span img { - display: block; - float: left; } - .markdown-body span.frame span span { - clear: both; - color: #333; - display: block; - padding: 5px 0 0; } - .markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; } - .markdown-body span.align-center > span { - display: block; - overflow: hidden; - margin: 13px auto 0; - text-align: center; } - .markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; } - .markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; } - .markdown-body span.align-right > span { - display: block; - overflow: hidden; - margin: 13px 0 0; - text-align: right; } - .markdown-body span.align-right span img { - margin: 0; - text-align: right; } - .markdown-body span.float-left { - display: block; - margin-right: 13px; - overflow: hidden; - float: left; } - .markdown-body span.float-left span { - margin: 13px 0 0; } - .markdown-body span.float-right { - display: block; - margin-left: 13px; - overflow: hidden; - float: right; } - .markdown-body span.float-right > span { - display: block; - overflow: hidden; - margin: 13px auto 0; - text-align: right; } - .markdown-body code, .markdown-body tt { - margin: 0 2px; - padding: 0px 5px; - border: 1px solid #eaeaea; - background-color: #f8f8f8; - border-radius: 3px; } - .markdown-body code { - white-space: nowrap; } - .markdown-body pre > code { - margin: 0; - padding: 0; - white-space: pre; - border: none; - background: transparent; } - .markdown-body .highlight pre, .markdown-body pre { - background-color: #f8f8f8; - border: 1px solid #ccc; - font-size: 13px; - line-height: 19px; - overflow: auto; - padding: 6px 10px; - border-radius: 3px; } - .markdown-body pre code, .markdown-body pre tt { - margin: 0; - padding: 0; - background-color: transparent; - border: none; } - -/* this code was copied from https://github.com/assets/stylesheets/primer/components/pygments.css */ -/* the .markdown-body class was then added to all rules */ -.markdown-body .highlight { - background: #ffffff; } - .markdown-body .highlight .c { - color: #999988; - font-style: italic; } - .markdown-body .highlight .err { - color: #a61717; - background-color: #e3d2d2; } - .markdown-body .highlight .k { - font-weight: bold; } - .markdown-body .highlight .o { - font-weight: bold; } - .markdown-body .highlight .cm { - color: #999988; - font-style: italic; } - .markdown-body .highlight .cp { - color: #999999; - font-weight: bold; } - .markdown-body .highlight .c1 { - color: #999988; - font-style: italic; } - .markdown-body .highlight .cs { - color: #999999; - font-weight: bold; - font-style: italic; } - .markdown-body .highlight .gd { - color: #000000; - background-color: #ffdddd; } - .markdown-body .highlight .gd .x { - color: #000000; - background-color: #ffaaaa; } - .markdown-body .highlight .ge { - font-style: italic; } - .markdown-body .highlight .gr { - color: #aa0000; } - .markdown-body .highlight .gh { - color: #999999; } - .markdown-body .highlight .gi { - color: #000000; - background-color: #ddffdd; } - .markdown-body .highlight .gi .x { - color: #000000; - background-color: #aaffaa; } - .markdown-body .highlight .go { - color: #888888; } - .markdown-body .highlight .gp { - color: #555555; } - .markdown-body .highlight .gs { - font-weight: bold; } - .markdown-body .highlight .gu { - color: #800080; - font-weight: bold; } - .markdown-body .highlight .gt { - color: #aa0000; } - .markdown-body .highlight .kc { - font-weight: bold; } - .markdown-body .highlight .kd { - font-weight: bold; } - .markdown-body .highlight .kn { - font-weight: bold; } - .markdown-body .highlight .kp { - font-weight: bold; } - .markdown-body .highlight .kr { - font-weight: bold; } - .markdown-body .highlight .kt { - color: #445588; - font-weight: bold; } - .markdown-body .highlight .m { - color: #009999; } - .markdown-body .highlight .s { - color: #d14; } - .markdown-body .highlight .na { - color: #008080; } - .markdown-body .highlight .nb { - color: #0086B3; } - .markdown-body .highlight .nc { - color: #445588; - font-weight: bold; } - .markdown-body .highlight .no { - color: #008080; } - .markdown-body .highlight .ni { - color: #800080; } - .markdown-body .highlight .ne { - color: #990000; - font-weight: bold; } - .markdown-body .highlight .nf { - color: #990000; - font-weight: bold; } - .markdown-body .highlight .nn { - color: #555555; } - .markdown-body .highlight .nt { - color: #000080; } - .markdown-body .highlight .nv { - color: #008080; } - .markdown-body .highlight .ow { - font-weight: bold; } - .markdown-body .highlight .w { - color: #bbbbbb; } - .markdown-body .highlight .mf { - color: #009999; } - .markdown-body .highlight .mh { - color: #009999; } - .markdown-body .highlight .mi { - color: #009999; } - .markdown-body .highlight .mo { - color: #009999; } - .markdown-body .highlight .sb { - color: #d14; } - .markdown-body .highlight .sc { - color: #d14; } - .markdown-body .highlight .sd { - color: #d14; } - .markdown-body .highlight .s2 { - color: #d14; } - .markdown-body .highlight .se { - color: #d14; } - .markdown-body .highlight .sh { - color: #d14; } - .markdown-body .highlight .si { - color: #d14; } - .markdown-body .highlight .sx { - color: #d14; } - .markdown-body .highlight .sr { - color: #009926; } - .markdown-body .highlight .s1 { - color: #d14; } - .markdown-body .highlight .ss { - color: #990073; } - .markdown-body .highlight .bp { - color: #999999; } - .markdown-body .highlight .vc { - color: #008080; } - .markdown-body .highlight .vg { - color: #008080; } - .markdown-body .highlight .vi { - color: #008080; } - .markdown-body .highlight .il { - color: #009999; } - .markdown-body .highlight .gc { - color: #999; - background-color: #EAF2F5; } - -.type-csharp .markdown-body .highlight .k { - color: #0000FF; } -.type-csharp .markdown-body .highlight .kt { - color: #0000FF; } -.type-csharp .markdown-body .highlight .nf { - color: #000000; - font-weight: normal; } -.type-csharp .markdown-body .highlight .nc { - color: #2B91AF; } -.type-csharp .markdown-body .highlight .nn { - color: #000000; } -.type-csharp .markdown-body .highlight .s { - color: #A31515; } -.type-csharp .markdown-body .highlight .sc { - color: #A31515; } diff --git a/themes/atom-light-ui/package.cson b/themes/atom-light-ui/package.cson new file mode 100644 index 000000000..53831688d --- /dev/null +++ b/themes/atom-light-ui/package.cson @@ -0,0 +1,11 @@ +'stylesheets': [ + 'atom.css' + 'editor.css' + 'select-list.css' + 'tree-view.css' + 'tabs.css' + 'status-bar.css' + 'command-panel.css' + 'command-logger.css' + 'blurred.css' +] diff --git a/themes/atom-light-ui/package.json b/themes/atom-light-ui/package.json deleted file mode 100644 index b223f8deb..000000000 --- a/themes/atom-light-ui/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "stylesheets":[ - "atom.css", - "editor.css", - "select-list.css", - "tree-view.css", - "tabs.css", - "status-bar.css", - "markdown-preview.css", - "command-panel.css", - "command-logger.css", - "blurred.css", - "bracket-matcher.css" - ] -} diff --git a/themes/atom-light-ui/tabs.css b/themes/atom-light-ui/tabs.css index 6c37140a7..98bf424a5 100644 --- a/themes/atom-light-ui/tabs.css +++ b/themes/atom-light-ui/tabs.css @@ -1,84 +1,50 @@ .tabs { - background: #e3e3e3; - border-bottom: 4px solid #e5e5e5; - box-shadow: inset 0 -1px 0 #959595, 0 1px 0 #989898; + background: #e1e1e1; + border: none; + border-bottom: 4px solid #ddd; + box-shadow: inset 0 -1px 0 #666; } .tab { - background-image: -webkit-linear-gradient(#e0e0e0, #bfbfbf); - border-top: none; - border-right: 1px solid #959595; - border-bottom: 1px solid #959595; - box-shadow: inset 0 0 5px #eee, 0 1px 0 #eee, inset -1px 0 0 #e0e0e0, inset 1px 0 0 #e0e0e0; + margin: 0 0 1px 0; + background-image: -webkit-linear-gradient(#e7e7e7, #cfcfcf); + color: #444; + text-shadow: 1px 1px rgba(255, 255, 255, 0.1); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.5), /* top hightlight */ + inset 1px 0 0 rgba(255, 255, 255, 0.3), /* left highlight */ + inset 0 -1px 0 rgba(255, 255, 255, 0.2), /* bottom shadow */ + inset -1px 0 0 rgba(0, 0, 0, 0.2), /* right shadow */ + inset -2px 0 0 rgba(255, 255, 255, 0.1); /* right hightlight */ +} + +.tab.active, +.tab:hover.active { + background-image: -webkit-linear-gradient(#fefefe, #e7e6e7); color: #323232; + text-shadow: 1px 1px rgba(255, 255, 255, 0.5); + box-shadow: + inset -1px 0 0 rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 1), + inset 0 -1px 0 rgba(255, 255, 255, 1); } .tab:hover { - background-image: -webkit-linear-gradient(#e2e2e2, #e0e0e0); + background-image: -webkit-linear-gradient(#e2e2e2, #ddd); } .tab .close-icon { - color: rgba(0,0,0,0.3); -} - -.tab.active, -.tab.active:hover { - color: #010101; -} - -.tab.active:first-child, -.tab.active:first-child:hover { - box-shadow: none; -} - -.tab.file-modified.active .close-icon { - border-color: #aaa; -} - -.tab:hover .close-icon { - color: #c8c8c8; - border-color: #c8c8c8; -} - -.tab.file-modified .close-icon { - border: 3px solid #777; - border-radius: 10px; -} - -.tab.active, -.tab.active:hover { - border-bottom-color: #e5e5e5; - box-shadow: inset -1px 0 0 #e0e0e0, inset 1px 0 0 #e0e0e0; - background-image: -webkit-linear-gradient(#fefefe, #e7e6e7); -} - -.tab.active:before, -.tab.active:after { - border: 1px solid #959595; -} - -.tab.active:before { - left: -5px; - border-bottom-right-radius: 4px; - border-width: 0 1px 1px 0; - box-shadow: 2px 2px 0 #e5e5e5; -} - -.tab.active:after { - right: -5px; - border-bottom-left-radius: 4px; - border-width: 0 0 1px 1px; - box-shadow: -2px 2px 0 #e5e5e5; -} - -.tab:hover { - background-image: -webkit-linear-gradient(#e2e2e2, #e0e0e0); -} - -.tab .file-name { - text-shadow: 0 1px 0 #e0e0e0; + color: #323232; } .tab .close-icon:hover { - color: #aaa; + color: #989898; +} + +.tab.file-modified .close-icon { + border-color: #1965d0; +} + +.tab.file-modified:hover .close-icon:before { + color: #1965d0; } diff --git a/vendor/space-pen.coffee b/vendor/space-pen.coffee index 022c0c58b..08b715960 100644 --- a/vendor/space-pen.coffee +++ b/vendor/space-pen.coffee @@ -1,4 +1,4 @@ -# Modified from 248eef5762e395c7d496510dbd06a20b65f89f45 +# Modified from d9b6b4a2fe3fdeca07bb82e14e412718540f89f3 $ = jQuery = require('jquery') elements = @@ -58,7 +58,6 @@ class View extends jQuery fragment constructor: (args...) -> - # args[0] ?= {} [html, postProcessingSteps] = @constructor.buildHtml -> @content(args...) jQuery.fn.init.call(this, html) @constructor = jQuery # sadly, jQuery assumes this.constructor == jQuery in pushStack @@ -198,7 +197,9 @@ for methodName in ['prependTo', 'appendTo', 'insertAfter', 'insertBefore'] originalCleanData = jQuery.cleanData jQuery.cleanData = (elements) -> - $(element).view()?.afterRemove?() for element in elements + for element in elements + view = $(element).view() + view.afterRemove?() if view and view?[0] == element originalCleanData(elements) (exports ? this).View = View