From 94a0bf3f905688a738c44b88881bdb1cd34df14e Mon Sep 17 00:00:00 2001 From: Jess Lin Date: Thu, 22 Jan 2015 21:06:25 -0800 Subject: [PATCH] [Gutter] Create Gutter and GutterContainer w/ API to hide/show --- spec/gutter-container-spec.coffee | 49 +++++++++++++++++++ spec/gutter-spec.coffee | 64 +++++++++++++++++++++++++ src/gutter-container.coffee | 78 +++++++++++++++++++++++++++++++ src/gutter.coffee | 58 +++++++++++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 spec/gutter-container-spec.coffee create mode 100644 spec/gutter-spec.coffee create mode 100644 src/gutter-container.coffee create mode 100644 src/gutter.coffee diff --git a/spec/gutter-container-spec.coffee b/spec/gutter-container-spec.coffee new file mode 100644 index 000000000..299ae7097 --- /dev/null +++ b/spec/gutter-container-spec.coffee @@ -0,0 +1,49 @@ +Gutter = require '../src/gutter' +GutterContainer = require '../src/gutter-container' + +describe 'GutterContainer', -> + gutterContainer = null + beforeEach -> + gutterContainer = new GutterContainer + + describe 'when initialized', -> + it 'it has no gutters', -> + expect(gutterContainer.getGutters().length).toBe 0 + + describe '::addGutter', -> + it 'creates a new gutter', -> + newGutter = gutterContainer.addGutter {'test-gutter', priority: 1} + expect(gutterContainer.getGutters()).toEqual [newGutter] + expect(newGutter.priority).toBe 1 + + it 'throws an error if the provided gutter name is already in use', -> + name = 'test-gutter' + gutterContainer.addGutter {name} + expect(gutterContainer.addGutter.bind(null, {name})).toThrow() + + it 'keeps added gutters sorted by ascending priority', -> + gutter1 = gutterContainer.addGutter {name: 'first', priority: 1} + gutter3 = gutterContainer.addGutter {name: 'third', priority: 3} + gutter2 = gutterContainer.addGutter {name: 'second', priority: 2} + expect(gutterContainer.getGutters()).toEqual [gutter1, gutter2, gutter3] + + describe '::removeGutter', -> + removedGutters = null + + beforeEach -> + gutterContainer = new GutterContainer + removedGutters = [] + gutterContainer.onDidRemoveGutter (gutterName) -> + removedGutters.push gutterName + + it 'removes the gutter if it is contained by this GutterContainer', -> + gutter = gutterContainer.addGutter {'test-gutter'} + expect(gutterContainer.getGutters()).toEqual [gutter] + gutterContainer.removeGutter gutter + expect(gutterContainer.getGutters().length).toBe 0 + expect(removedGutters).toEqual [gutter.name] + + it 'throws an error if the gutter is not within this GutterContainer', -> + otherGutterContainer = new GutterContainer + gutter = new Gutter 'gutter-name', otherGutterContainer + expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow() diff --git a/spec/gutter-spec.coffee b/spec/gutter-spec.coffee new file mode 100644 index 000000000..80b919423 --- /dev/null +++ b/spec/gutter-spec.coffee @@ -0,0 +1,64 @@ +Gutter = require '../src/gutter' + +describe 'Gutter', -> + fakeGutterContainer = {} + name = 'name' + + describe '::hide', -> + it 'hides the gutter if it is visible.', -> + options = + name: name + visible: true + gutter = new Gutter fakeGutterContainer, options + events = [] + gutter.onDidChangeVisible (gutter) -> + events.push gutter.isVisible() + + expect(gutter.isVisible()).toBe true + gutter.hide() + expect(gutter.isVisible()).toBe false + expect(events).toEqual [false] + gutter.hide() + expect(gutter.isVisible()).toBe false + # An event should only be emitted when the visibility changes. + expect(events.length).toBe 1 + + describe '::show', -> + it 'shows the gutter if it is hidden.', -> + options = + name: name + visible: false + gutter = new Gutter fakeGutterContainer, options + events = [] + gutter.onDidChangeVisible (gutter) -> + events.push gutter.isVisible() + + expect(gutter.isVisible()).toBe false + gutter.show() + expect(gutter.isVisible()).toBe true + expect(events).toEqual [true] + gutter.show() + expect(gutter.isVisible()).toBe true + # An event should only be emitted when the visibility changes. + expect(events.length).toBe 1 + + describe '::destroy', -> + [mockGutterContainer, mockGutterContainerRemovedGutters] = [] + + beforeEach -> + mockGutterContainerRemovedGutters = []; + mockGutterContainer = removeGutter: (destroyedGutter) -> + mockGutterContainerRemovedGutters.push destroyedGutter + + it 'removes the gutter from its container.', -> + gutter = new Gutter mockGutterContainer, {name} + gutter.destroy() + expect(mockGutterContainerRemovedGutters).toEqual([gutter]) + + it 'calls all callbacks registered on ::onDidDestroy.', -> + gutter = new Gutter mockGutterContainer, {name} + didDestroy = false + gutter.onDidDestroy -> + didDestroy = true + gutter.destroy() + expect(didDestroy).toBe true diff --git a/src/gutter-container.coffee b/src/gutter-container.coffee new file mode 100644 index 000000000..06d85a628 --- /dev/null +++ b/src/gutter-container.coffee @@ -0,0 +1,78 @@ +{Emitter} = require 'event-kit' +{Subscriber} = require 'emissary' +Gutter = require './gutter' + +# This class encapsulates the logic for adding and modifying a set of gutters. + +module.exports = +class GutterContainer + Subscriber.includeInto(this) + constructor: -> + @gutters = [] + @emitter = new Emitter + + destroy: -> + @gutters = null + @emitter.dispose() + @unsubscribe() + + # Creates and returns a {Gutter}. + # * `options` An {Object} with the following fields: + # * `name` (required) A unique {String} to identify this gutter. + # * `priority` (optional) A {Number} that determines stacking order between + # gutters. Lower priority items are forced closer to the edges of the + # window. (default: -100) + # * `visible` (optional) {Boolean} specifying whether the gutter is visible + # initially after being created. (default: true) + addGutter: (options) -> + options = options ? {} + gutterName = options.name + if gutterName == null + throw new Error 'A name is required to create a gutter.' + if @gutterWithName gutterName + throw new Error 'Tried to create a gutter with a name that is already in use.' + newGutter = new Gutter this, options + + inserted = false + # Insert the gutter into the gutters array, sorted in ascending order by 'priority'. + # This could be optimized, but there are unlikely to be many gutters. + for i in [0...@gutters.length] + if @gutters[i].priority >= newGutter.priority + @gutters.splice(i, 0, newGutter) + inserted = true + break + if !inserted + @gutters.push newGutter + return newGutter + + getGutters: -> + @gutters.slice() + + gutterWithName: (name) -> + for gutter in @gutters + if gutter.name == name then return gutter + null + + ### + Section: Event Subscription + ### + + # @param callback: function( nameOfRemovedGutter ) + onDidRemoveGutter: (callback) -> + @emitter.on 'did-remove-gutter', callback + + ### + Section: Private Methods + ### + + # Processes the destruction of the gutter. Throws an error if this gutter is + # not within this gutterContainer. + removeGutter: (gutter) -> + index = @gutters.indexOf gutter + if index > -1 + @gutters.splice(index, 1) + @unsubscribe gutter + @emitter.emit 'did-remove-gutter', gutter.name + else + throw new Error 'The given gutter cannot be removed because it is not ' + + 'within this GutterContainer.' diff --git a/src/gutter.coffee b/src/gutter.coffee new file mode 100644 index 000000000..bd50b4157 --- /dev/null +++ b/src/gutter.coffee @@ -0,0 +1,58 @@ +{Emitter} = require 'event-kit' + +# Public: This class represents a gutter within a TextEditor. + +DefaultPriority = -100 + +module.exports = +class Gutter + # * `gutterContainer` The {GutterContainer} object to which this gutter belongs. + # * `options` An {Object} with the following fields: + # * `name` (required) A unique {String} to identify this gutter. + # * `priority` (optional) A {Number} that determines stacking order between + # gutters. Lower priority items are forced closer to the edges of the + # window. (default: -100) + # * `visible` (optional) {Boolean} specifying whether the gutter is visible + # initially after being created. (default: true) + constructor: (gutterContainer, options) -> + @gutterContainer = gutterContainer + @name = options?.name + @priority = options?.priority ? DefaultPriority + @visible = options?.visible ? true + + @emitter = new Emitter + + destroy: -> + @gutterContainer.removeGutter(this) + @emitter.emit 'did-destroy' + @emitter.dispose() + + hide: -> + if @visible + @visible = false + @emitter.emit 'did-change-visible', this + + show: -> + if not @visible + @visible = true + @emitter.emit 'did-change-visible', this + + isVisible: -> + @visible + + # Calls your `callback` when the {Gutter}'s' visibility changes. + # + # * `callback` {Function} + # * `gutter` The {Gutter} whose visibility changed. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeVisible: (callback) -> + @emitter.on 'did-change-visible', callback + + # Calls your `callback` when the {Gutter} is destroyed + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy: (callback) -> + @emitter.on 'did-destroy', callback