Merge pull request #271 from github/super-command-panel

Super command panel
This commit is contained in:
Kevin Sawicki
2013-02-14 10:13:40 -08:00
13 changed files with 458 additions and 124 deletions

View File

@@ -12,3 +12,13 @@
'meta-e': 'command-panel:set-selection-as-regex-address'
'meta-f': 'command-panel:find-in-file'
'meta-F': 'command-panel:find-in-project'
'.command-panel':
'ctrl-{': 'command-panel:collapse-all'
'ctrl-}': 'command-panel:expand-all'
'.command-panel .preview-list':
'left': 'command-panel:collapse-result'
'ctrl-[': 'command-panel:collapse-result'
'right': 'command-panel:expand-result'
'ctrl-]': 'command-panel:expand-result'

View File

@@ -11,7 +11,13 @@ module.exports =
class CommandPanelView extends View
@content: ->
@div class: 'command-panel tool-panel', =>
@div outlet: 'previewCount', class: 'preview-count'
@div class: 'loading', outlet: 'loadingMessage'
@div class: 'header', outlet: 'previewHeader', =>
@ul outlet: 'expandCollapse', class: 'expand-collapse', =>
@li class: 'expand', 'Expand All'
@li class: 'collapse', 'Collapse All'
@span outlet: 'previewCount', class: 'preview-count'
@subview 'previewList', new PreviewList(rootView)
@ul class: 'error-messages', outlet: 'errorMessages'
@div class: 'prompt-and-editor', =>
@@ -40,8 +46,11 @@ class CommandPanelView extends View
rootView.command 'command-panel:repeat-relative-address-in-reverse', => @repeatRelativeAddressInReverse()
rootView.command 'command-panel:set-selection-as-regex-address', => @setSelectionAsLastRelativeAddress()
@on 'click', '.expand', @onExpandAll
@on 'click', '.collapse', @onCollapseAll
@previewList.hide()
@previewCount.hide()
@previewHeader.hide()
@errorMessages.hide()
@prompt.iconSize(@miniEditor.getFontSize())
@@ -66,17 +75,35 @@ class CommandPanelView extends View
togglePreview: ->
if @previewList.is(':focus')
@previewList.hide()
@previewCount.hide()
@previewHeader.hide()
@detach()
rootView.focus()
else
@attach() unless @hasParent()
if @previewList.hasOperations()
@previewList.show().focus()
@previewCount.show()
@previewHeader.show()
else
@miniEditor.focus()
toggleLoading: ->
if @loadingMessage.hasClass 'is-loading'
@loadingMessage.removeClass 'is-loading'
@loadingMessage.html ''
@loadingMessage.hide()
else
@loadingMessage.addClass 'is-loading'
@loadingMessage.html 'Searching...'
@loadingMessage.show()
onExpandAll: (event) =>
@previewList.expandAllPaths()
@previewList.focus()
onCollapseAll: (event) =>
@previewList.collapseAllPaths()
@previewList.focus()
attach: (text='', options={}) ->
@errorMessages.hide()
@@ -89,17 +116,19 @@ class CommandPanelView extends View
detach: ->
rootView.focus()
@previewList.hide()
@previewCount.hide()
@previewHeader.hide()
super
escapedCommand: ->
@miniEditor.getText()
execute: (command=@escapedCommand())->
@toggleLoading()
@errorMessages.empty()
try
@commandInterpreter.eval(command, rootView.getActiveEditSession()).done ({operationsToPreview, errorMessages}) =>
@toggleLoading()
@history.push(command)
@historyIndex = @history.length
@@ -109,6 +138,7 @@ class CommandPanelView extends View
@errorMessages.append $$ ->
@li errorMessage for errorMessage in errorMessages
else if operationsToPreview?.length
@previewHeader.show()
@previewList.populate(operationsToPreview)
@previewList.focus()
@previewCount.text("#{_.pluralize(operationsToPreview.length, 'match', 'matches')} in #{_.pluralize(@previewList.getPathCount(), 'file')}").show()

View File

@@ -0,0 +1,28 @@
{View} = require 'space-pen'
module.exports =
class OperationView extends View
@content: ({operation} = {}) ->
{prefix, suffix, match, range} = operation.preview()
@li 'data-index': operation.index, class: 'operation', =>
@span range.start.row + 1, class: 'line-number'
@span class: 'preview', =>
@span prefix
@span match, class: 'match'
@span suffix
initialize: ({@previewList, @operation}) ->
@subscribe @previewList, 'core:confirm', =>
if @hasClass('selected')
@executeOperation()
false
@on 'mousedown', (e) =>
@executeOperation()
@previewList.find('.selected').removeClass('selected')
@addClass('selected')
executeOperation: ->
editSession = rootView.open(@operation.getPath())
bufferRange = @operation.execute(editSession)
editSession.setSelectedBufferRange(bufferRange, autoscroll: true) if bufferRange
@previewList.focus()

View File

@@ -0,0 +1,58 @@
{View} = require 'space-pen'
fs = require 'fs'
OperationView = require './operation-view'
$ = require 'jquery'
module.exports =
class PathView extends View
@content: ({path, operations, previewList} = {}) ->
classes = ['path']
classes.push('readme') if fs.isReadmePath(path)
@li class: classes.join(' '), =>
@div outlet: 'pathDetails', class: 'path-details', =>
@span class: 'path-name', path
@span "(#{operations.length})", class: 'path-match-number'
@ul outlet: 'matches', class: 'matches', =>
for operation in operations
@subview "operation#{operation.index}", new OperationView({operation, previewList})
initialize: ({@previewList}) ->
@pathDetails.on 'mousedown', => @toggle(true)
@subscribe @previewList, 'command-panel:collapse-result', =>
@collapse(true) if @isSelected()
@subscribe @previewList, 'command-panel:expand-result', =>
@expand(true) if @isSelected()
@subscribe @previewList, 'core:confirm', =>
if @hasClass('selected')
@toggle(true)
false
isSelected: ->
@hasClass('selected') or @find('.selected').length
setSelected: ->
@previewList.find('.selected').removeClass('selected')
@addClass('selected')
toggle: (animate) ->
if @hasClass('is-collapsed')
@expand(animate)
else
@collapse(animate)
expand: (animate=false) ->
if animate
@matches.show 100, => @removeClass 'is-collapsed'
else
@matches.show()
@removeClass 'is-collapsed'
collapse: (animate=false) ->
if animate
@matches.hide 100, =>
@addClass 'is-collapsed'
@setSelected() if @isSelected()
else
@matches.hide()
@addClass 'is-collapsed'
@setSelected() if @isSelected()

View File

@@ -3,24 +3,30 @@ $ = require 'jquery'
ScrollView = require 'scroll-view'
_ = require 'underscore'
fs = require 'fs'
PathView = require './path-view'
OperationView = require './operation-view'
module.exports =
class PreviewList extends ScrollView
@content: ->
@ol class: 'preview-list', tabindex: -1, ->
@ol class: 'preview-list', tabindex: -1
selectedOperationIndex: 0
operations: null
initialize: (@rootView) ->
initialize: ->
super
@on 'core:move-down', => @selectNextOperation(); false
@on 'core:move-up', => @selectPreviousOperation(); false
@on 'core:confirm', => @executeSelectedOperation()
@on 'mousedown', 'li.operation', (e) =>
@setSelectedOperationIndex(parseInt($(e.target).closest('li').data('index')))
@executeSelectedOperation()
@command 'command-panel:collapse-all', => @collapseAllPaths()
@command 'command-panel:expand-all', => @expandAllPaths()
expandAllPaths: ->
@children().each (index, element) -> $(element).view().expand()
collapseAllPaths: ->
@children().each (index, element) -> $(element).view().collapse()
destroy: ->
@destroyOperations() if @operations
@@ -31,26 +37,14 @@ class PreviewList extends ScrollView
@destroyOperations() if @operations
@operations = operations
@empty()
@html $$$ ->
operation.index = index for operation, index in operations
operationsByPath = _.groupBy(operations, (operation) -> operation.getPath())
for path, ops of operationsByPath
classes = ['path']
classes.push('readme') if fs.isReadmePath(path)
@li class: classes.join(' '), =>
@span path
@span "(#{ops.length})", class: 'path-match-number'
for operation in ops
{prefix, suffix, match, range} = operation.preview()
@li 'data-index': operation.index, class: 'operation', =>
@span range.start.row + 1, class: 'line-number'
@span class: 'preview', =>
@span prefix
@span match, class: 'match'
@span suffix
@setSelectedOperationIndex(0)
operation.index = index for operation, index in operations
operationsByPath = _.groupBy(operations, (operation) -> operation.getPath())
for path, operations of operationsByPath
@append new PathView({path, operations, previewList: this})
@show()
@find('.operation:first').addClass('selected')
@setLineNumberWidth()
setLineNumberWidth: ->
@@ -61,33 +55,33 @@ class PreviewList extends ScrollView
lineNumbers.width(maxWidth)
selectNextOperation: ->
@setSelectedOperationIndex(@selectedOperationIndex + 1)
selectedView = @find('.selected').view()
if selectedView instanceof PathView
if selectedView.hasClass('is-collapsed')
nextView = selectedView.next().view()
else
nextView = selectedView.find('.operation:first')
else
nextView = selectedView.next().view() ? selectedView.closest('.path').next().view()
if nextView?
selectedView.removeClass('selected')
nextView.addClass('selected')
@scrollToElement(nextView)
selectPreviousOperation: ->
@setSelectedOperationIndex(@selectedOperationIndex - 1)
selectedView = @find('.selected').view()
setSelectedOperationIndex: (index, scrollToOperation=true) ->
index = Math.max(0, index)
index = Math.min(@operations.length - 1, index)
@children(".selected").removeClass('selected')
element = @children("li.operation:eq(#{index})")
element.addClass('selected')
if selectedView instanceof PathView
previousView = selectedView.prev()
previousView = previousView.find('.operation:last').view() unless previousView.hasClass('is-collapsed')
else
previousView = selectedView.prev().view() ? selectedView.closest('.path').view()
if scrollToOperation
if index is 0
@scrollToTop()
else
@scrollToElement(element)
@selectedOperationIndex = index
executeSelectedOperation: ->
operation = @getSelectedOperation()
editSession = @rootView.open(operation.getPath())
bufferRange = operation.execute(editSession)
editSession.setSelectedBufferRange(bufferRange, autoscroll: true) if bufferRange
@focus()
false
if previousView?
selectedView.removeClass('selected')
previousView.addClass('selected')
@scrollToElement(previousView)
getPathCount: ->
_.keys(_.groupBy(@operations, (operation) -> operation.getPath())).length
@@ -100,23 +94,27 @@ class PreviewList extends ScrollView
@operations = null
getSelectedOperation: ->
@operations[@selectedOperationIndex]
@find('.operation.selected').view()?.operation
scrollToElement: (element) ->
top = @scrollTop() + element.position().top
top = @scrollTop() + element.offset().top - @offset().top
bottom = top + element.outerHeight()
if bottom > @scrollBottom()
@scrollBottom(bottom)
if top < @scrollTop()
@scrollTop(top)
@scrollBottom(bottom) if bottom > @scrollBottom()
@scrollTop(top) if top < @scrollTop()
scrollToBottom: ->
super()
@setSelectedOperationIndex(Infinity, false)
@find('.selected').removeClass('selected')
lastPath = @find('.path:last')
if lastPath.hasClass('is-collapsed')
lastPath.addClass('selected')
else
lastPath.find('.operation:last').addClass('selected')
scrollToTop: ->
super()
@setSelectedOperationIndex(0, false)
@find('.selected').removeClass('selected')
@find('.path:first').addClass('selected')

View File

@@ -128,6 +128,9 @@ describe "CommandPanel", ->
beforeEach ->
expect(commandPanel.previewList).toBeVisible()
it "shows the expand and collapse all buttons", ->
expect(commandPanel.find('.expand-collapse')).toBeVisible()
describe "when the preview list is focused", ->
it "hides the command panel", ->
expect(commandPanel.previewList).toMatchSelector(':focus')
@@ -171,19 +174,19 @@ describe "CommandPanel", ->
expect(commandPanel.hasParent()).toBeTruthy()
describe "when the mini editor is focused", ->
it "retains focus on the mini editor and does not show the preview list or preview count", ->
it "retains focus on the mini editor and does not show the preview list or preview header", ->
expect(commandPanel.miniEditor.isFocused).toBeTruthy()
rootView.trigger 'command-panel:toggle-preview'
expect(commandPanel.previewList).toBeHidden()
expect(commandPanel.previewCount).toBeHidden()
expect(commandPanel.previewHeader).toBeHidden()
expect(commandPanel.miniEditor.isFocused).toBeTruthy()
describe "when the mini editor is not focused", ->
it "focuses the mini editor and does not show the preview list or preview count", ->
it "focuses the mini editor and does not show the preview list or preview header", ->
rootView.focus()
rootView.trigger 'command-panel:toggle-preview'
expect(commandPanel.previewList).toBeHidden()
expect(commandPanel.previewCount).toBeHidden()
expect(commandPanel.previewHeader).toBeHidden()
expect(commandPanel.miniEditor.isFocused).toBeTruthy()
describe "when the command panel is not visible", ->
@@ -290,7 +293,7 @@ describe "CommandPanel", ->
expect(commandPanel.previewList).toBeVisible()
expect(commandPanel.previewList).toMatchSelector ':focus'
previewItem = commandPanel.previewList.find("li:contains(sample.js):first")
expect(previewItem.text()).toBe "sample.js(1)"
expect(previewItem.find('.path-details').text()).toBe "sample.js(1)"
expect(previewItem.next().find('.preview').text()).toBe "var quicksort = function () {"
expect(previewItem.next().find('.preview > .match').text()).toBe "quicksort"
@@ -392,29 +395,38 @@ describe "CommandPanel", ->
expect(previewList.find('li.operation:eq(1)')).toHaveClass 'selected'
expect(previewList.getSelectedOperation()).toBe previewList.getOperations()[1]
_.times previewList.getOperations().length - 2, -> previewList.trigger 'core:move-down'
_.times previewList.getOperations().length + previewList.getPathCount(), -> previewList.trigger 'core:move-down'
expect(previewList.find("li.operation:last")).toHaveClass 'selected'
expect(previewList.getSelectedOperation()).toBe _.last(previewList.getOperations())
expect(previewList.scrollBottom()).toBeCloseTo previewList.prop('scrollHeight'), -1
_.times previewList.getOperations().length, -> previewList.trigger 'core:move-up'
_.times previewList.getOperations().length + previewList.getPathCount(), -> previewList.trigger 'core:move-up'
expect(previewList.scrollTop()).toBe 0
it "doesn't bubble up the event and the command panel text doesn't change", ->
rootView.attachToDom()
commandPanel.miniEditor.setText "command"
previewList.focus()
previewList.trigger 'core:move-up'
expect(previewList.find('li.operation:eq(0)')).toHaveClass 'selected'
expect(commandPanel.miniEditor.getText()).toBe 'command'
previewList.trigger 'core:move-down'
expect(previewList.find('li.operation:eq(1)')).toHaveClass 'selected'
expect(commandPanel.miniEditor.getText()).toBe 'command'
previewList.trigger 'core:move-up'
expect(previewList.find('li.operation:eq(0)')).toHaveClass 'selected'
expect(commandPanel.miniEditor.getText()).toBe 'command'
it "doesn't select collapsed operations", ->
rootView.attachToDom()
previewList.trigger 'command-panel:collapse-result'
expect(previewList.find('li.path:eq(0)')).toHaveClass 'selected'
previewList.trigger 'core:move-down'
expect(previewList.find('li.path:eq(1)')).toHaveClass 'selected'
previewList.trigger 'core:move-up'
expect(previewList.find('li.path:eq(0)')).toHaveClass 'selected'
describe "when move-to-top and move-to-bottom are triggered on the preview list", ->
it "selects the first/last operation", ->
it "selects the first path or last operation", ->
rootView.attachToDom()
expect(previewList.getOperations().length).toBeGreaterThan 0
expect(previewList.find('li.operation:eq(0)')).toHaveClass 'selected'
@@ -425,8 +437,8 @@ describe "CommandPanel", ->
expect(previewList.getSelectedOperation()).toBe _.last(previewList.getOperations())
previewList.trigger 'core:move-to-top'
expect(previewList.find('li.operation:eq(0)')).toHaveClass 'selected'
expect(previewList.getSelectedOperation()).toBe previewList.getOperations()[0]
expect(previewList.find('li.path:eq(0)')).toHaveClass 'selected'
expect(previewList.getSelectedOperation()).toBeUndefined()
describe "when core:confirm is triggered on the preview list", ->
it "opens the operation's buffer, selects and scrolls to the search result, and refocuses the preview list", ->
@@ -453,6 +465,15 @@ describe "CommandPanel", ->
expect(executeHandler).not.toHaveBeenCalled()
it "toggles the expansion state when a path is selected", ->
rootView.attachToDom()
previewList.trigger 'core:move-to-top'
expect(previewList.find('li.path:first')).toHaveClass 'selected'
expect(previewList.find('li.path:first')).not.toHaveClass 'is-collapsed'
previewList.trigger 'core:confirm'
expect(previewList.find('li.path:first')).toHaveClass 'selected'
expect(previewList.find('li.path:first')).toHaveClass 'is-collapsed'
describe "when an operation in the preview list is clicked", ->
it "opens the operation's buffer, selects the search result, and refocuses the preview list", ->
spyOn(previewList, 'focus')
@@ -465,3 +486,24 @@ describe "CommandPanel", ->
expect(editSession.buffer.getPath()).toBe project.resolve(operation.getPath())
expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange()
expect(previewList.focus).toHaveBeenCalled()
describe "when a path in the preview list is clicked", ->
it "shows and hides the matches for that path", ->
rootView.attachToDom()
expect(previewList.find('li.path:first-child ul.matches')).toBeVisible()
previewList.find('li.path:first-child .path-details').mousedown()
expect(previewList.find('li.path:first-child ul.matches')).toBeHidden()
previewList.find('li.path:first-child .path-details').mousedown()
expect(previewList.find('li.path:first-child ul.matches')).toBeVisible()
describe "when command-panel:collapse-result and command-panel:expand-result are triggered", ->
it "collapses and selects the path, and then expands the selected path", ->
rootView.attachToDom()
expect(previewList.find('li.path:first-child ul.matches')).toBeVisible()
previewList.trigger 'command-panel:collapse-result'
expect(previewList.find('li.path:first-child ul.matches')).toBeHidden()
expect(previewList.find('li.path:first-child')).toHaveClass 'selected'
previewList.trigger 'command-panel:expand-result'
expect(previewList.find('li.path:first-child ul.matches')).toBeVisible()
expect(previewList.find('li.path:first-child')).toHaveClass 'selected'