Merge pull request #98 from github/tabs

Tabs
This commit is contained in:
Nathan Sobo
2012-11-20 15:44:52 -08:00
13 changed files with 235 additions and 17 deletions

View File

@@ -9,3 +9,4 @@ requireExtension 'status-bar'
requireExtension 'wrap-guide'
requireExtension 'markdown-preview'
requireExtension 'outline-view'
requireExtension 'tabs'

View File

@@ -152,7 +152,7 @@ describe "Editor", ->
expect(otherEditSession.buffer.subscriptionCount()).toBe 0
describe "when 'close' is triggered", ->
it "closes active edit session and loads next edit session", ->
it "closes the active edit session and loads next edit session", ->
editor.edit(rootView.project.buildEditSessionForPath())
editSession = editor.activeEditSession
spyOn(editSession.buffer, 'isModified').andReturn false
@@ -163,6 +163,19 @@ describe "Editor", ->
expect(editor.remove).not.toHaveBeenCalled()
expect(editor.getBuffer()).toBe buffer
it "triggers the 'editor:edit-session-removed' event with the edit session and its former index", ->
editor.edit(rootView.project.buildEditSessionForPath())
editSession = editor.activeEditSession
index = editor.getActiveEditSessionIndex()
spyOn(editSession.buffer, 'isModified').andReturn false
editSessionRemovedHandler = jasmine.createSpy('editSessionRemovedHandler')
editor.on 'editor:edit-session-removed', editSessionRemovedHandler
editor.trigger "core:close"
expect(editSessionRemovedHandler).toHaveBeenCalled()
expect(editSessionRemovedHandler.argsForCall[0][1..2]).toEqual [editSession, index]
it "calls remove on the editor if there is one edit session and mini is false", ->
editSession = editor.activeEditSession
expect(editor.mini).toBeFalsy()
@@ -193,12 +206,18 @@ describe "Editor", ->
otherEditSession = rootView.project.buildEditSessionForPath()
describe "when the edit session wasn't previously assigned to this editor", ->
it "adds edit session to editor", ->
it "adds edit session to editor and triggers the 'editor:edit-session-added' event", ->
editSessionAddedHandler = jasmine.createSpy('editSessionAddedHandler')
editor.on 'editor:edit-session-added', editSessionAddedHandler
originalEditSessionCount = editor.editSessions.length
editor.edit(otherEditSession)
expect(editor.activeEditSession).toBe otherEditSession
expect(editor.editSessions.length).toBe originalEditSessionCount + 1
expect(editSessionAddedHandler).toHaveBeenCalled()
expect(editSessionAddedHandler.argsForCall[0][1..2]).toEqual [otherEditSession, originalEditSessionCount]
describe "when the edit session was previously assigned to this editor", ->
it "restores the previous edit session associated with the editor", ->
previousEditSession = editor.activeEditSession
@@ -277,6 +296,19 @@ describe "Editor", ->
runs ->
expect(atom.confirm).toHaveBeenCalled()
it "emits an editor:active-edit-session-changed event with the edit session and its index", ->
activeEditSessionChangeHandler = jasmine.createSpy('activeEditSessionChangeHandler')
editor.on 'editor:active-edit-session-changed', activeEditSessionChangeHandler
editor.setActiveEditSessionIndex(2)
expect(activeEditSessionChangeHandler).toHaveBeenCalled()
expect(activeEditSessionChangeHandler.argsForCall[0][1..2]).toEqual [editor.activeEditSession, 2]
activeEditSessionChangeHandler.reset()
editor.setActiveEditSessionIndex(0)
expect(activeEditSessionChangeHandler.argsForCall[0][1..2]).toEqual [editor.activeEditSession, 0]
activeEditSessionChangeHandler.reset()
describe ".loadNextEditSession()", ->
it "loads the next editor state and wraps to beginning when end is reached", ->
expect(editor.activeEditSession).toBe session2

View File

@@ -10,7 +10,7 @@ describe "TextMateGrammar", ->
beforeEach ->
grammar = TextMateBundle.grammarForFilePath("hello.coffee")
describe ".tokenizeLine(line, { ruleStack, tabLength })", ->
describe ".tokenizeLine(line, ruleStack)", ->
describe "when the entire line matches a single pattern with no capture groups", ->
it "returns a single token with the correct scope", ->
{tokens} = grammar.tokenizeLine("return")
@@ -142,7 +142,7 @@ describe "TextMateGrammar", ->
describe "when the pattern spans multiple lines", ->
it "uses the ruleStack returned by the first line to parse the second line", ->
{tokens: firstTokens, ruleStack} = grammar.tokenizeLine("'''single-quoted")
{tokens: secondTokens, ruleStack} = grammar.tokenizeLine("heredoc'''", {ruleStack})
{tokens: secondTokens, ruleStack} = grammar.tokenizeLine("heredoc'''", ruleStack)
expect(firstTokens.length).toBe 2
expect(secondTokens.length).toBe 2

View File

@@ -136,13 +136,17 @@ window.fakeClearTimeout = (idToClear) ->
window.advanceClock = (delta=1) ->
window.now += delta
callbacks = []
window.timeouts = window.timeouts.filter ([id, strikeTime, callback]) ->
if strikeTime <= window.now
callback()
callbacks.push(callback)
false
else
true
callback() for callback in callbacks
window.pagePixelPositionForPoint = (editor, point) ->
point = Point.fromObject point
top = editor.renderedLines.offset().top + point.row * editor.lineHeight

View File

@@ -388,6 +388,7 @@ class Editor extends View
if index == -1
index = @editSessions.length
@editSessions.push(editSession)
@trigger 'editor:edit-session-added', [editSession, index]
@setActiveEditSessionIndex(index)
@@ -398,9 +399,11 @@ class Editor extends View
@remove()
else
editSession = @activeEditSession
index = @getActiveEditSessionIndex()
@loadPreviousEditSession()
_.remove(@editSessions, editSession)
editSession.destroy()
@trigger 'editor:edit-session-removed', [editSession, index]
loadNextEditSession: ->
nextIndex = (@getActiveEditSessionIndex() + 1) % @editSessions.length
@@ -430,6 +433,7 @@ class Editor extends View
@trigger 'editor-path-change'
@trigger 'editor-path-change'
@trigger 'editor:active-edit-session-changed', [@activeEditSession, index]
@resetDisplay()
if @attached and @activeEditSession.buffer.isInConflict()

View File

@@ -2,7 +2,8 @@ _ = require 'underscore'
module.exports =
class ScreenLine
constructor: ({@tokens, @ruleStack, @bufferRows, @startBufferColumn, @fold}) ->
constructor: ({tokens, @ruleStack, @bufferRows, @startBufferColumn, @fold, tabLength}) ->
@tokens = @breakOutAtomicTokens(tokens, tabLength)
@bufferRows ?= 1
@startBufferColumn ?= 0
@text = _.pluck(@tokens, 'value').join('')
@@ -90,3 +91,11 @@ class ScreenLine
delta += token.bufferDelta
return token if delta >= bufferColumn
token
breakOutAtomicTokens: (inputTokens, tabLength) ->
outputTokens = []
breakOutLeadingWhitespace = true
for token in inputTokens
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingWhitespace)...)
breakOutLeadingWhitespace = token.isOnlyWhitespace() if breakOutLeadingWhitespace
outputTokens

View File

@@ -29,7 +29,7 @@ class TextMateGrammar
data = {patterns: [data], tempName: name} if data.begin? or data.match?
@repository[name] = new Rule(this, data)
tokenizeLine: (line, {ruleStack, tabLength}={}) ->
tokenizeLine: (line, ruleStack=[@initialRule]) ->
ruleStack ?= [@initialRule]
ruleStack = new Array(ruleStack...) # clone ruleStack
tokens = []
@@ -62,15 +62,7 @@ class TextMateGrammar
))
break
{ tokens: @breakOutAtomicTokens(tokens, tabLength), ruleStack }
breakOutAtomicTokens: (inputTokens, tabLength) ->
outputTokens = []
breakOutLeadingWhitespace = true
for token in inputTokens
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingWhitespace)...)
breakOutLeadingWhitespace = token.isOnlyWhitespace() if breakOutLeadingWhitespace
outputTokens
{ tokens, ruleStack }
ruleForInclude: (name) ->
if name[0] == "#"

View File

@@ -66,7 +66,8 @@ class TokenizedBuffer
buildScreenLineForRow: (row, ruleStack) ->
line = @buffer.lineForRow(row)
new ScreenLine(@languageMode.tokenizeLine(line, {ruleStack, @tabLength}))
{ tokens, ruleStack } = @languageMode.tokenizeLine(line, ruleStack)
new ScreenLine({tokens, ruleStack, @tabLength})
lineForScreenRow: (row) ->
@screenLines[row]

View File

@@ -0,0 +1 @@
module.exports = require 'tabs/src/tabs'

View File

@@ -0,0 +1,91 @@
$ = require 'jquery'
_ = require 'underscore'
RootView = require 'root-view'
Tabs = require 'tabs'
fs = require 'fs'
describe "Tabs", ->
[rootView, editor, statusBar, buffer, tabs] = []
beforeEach ->
rootView = new RootView(require.resolve('fixtures/sample.js'))
rootView.open('sample.txt')
rootView.simulateDomAttachment()
rootView.activateExtension(Tabs)
editor = rootView.getActiveEditor()
tabs = rootView.find('.tabs').view()
afterEach ->
rootView.remove()
describe "@activate", ->
it "appends a status bear to all existing and new editors", ->
expect(rootView.panes.find('.pane').length).toBe 1
expect(rootView.panes.find('.pane > .tabs').length).toBe 1
editor.splitRight()
expect(rootView.find('.pane').length).toBe 2
expect(rootView.panes.find('.pane > .tabs').length).toBe 2
describe "#initialize()", ->
it "creates a tab for each edit session on the editor to which the tab-strip belongs", ->
expect(editor.editSessions.length).toBe 2
expect(tabs.find('.tab').length).toBe 2
expect(tabs.find('.tab:eq(0) .file-name').text()).toBe editor.editSessions[0].buffer.getBaseName()
expect(tabs.find('.tab:eq(1) .file-name').text()).toBe editor.editSessions[1].buffer.getBaseName()
it "highlights the tab for the current active edit session", ->
expect(editor.getActiveEditSessionIndex()).toBe 1
expect(tabs.find('.tab:eq(1)')).toHaveClass 'active'
describe "when the active edit session changes", ->
it "highlights the tab for the newly-active edit session", ->
editor.setActiveEditSessionIndex(0)
expect(tabs.find('.active').length).toBe 1
expect(tabs.find('.tab:eq(0)')).toHaveClass 'active'
editor.setActiveEditSessionIndex(1)
expect(tabs.find('.active').length).toBe 1
expect(tabs.find('.tab:eq(1)')).toHaveClass 'active'
describe "when a new edit session is created", ->
it "adds a tab for the new edit session", ->
rootView.open('two-hundred.txt')
expect(tabs.find('.tab').length).toBe 3
expect(tabs.find('.tab:eq(2) .file-name').text()).toBe 'two-hundred.txt'
describe "when the edit session's buffer has an undefined path", ->
it "makes the tab text 'untitled'", ->
rootView.open()
expect(tabs.find('.tab').length).toBe 3
expect(tabs.find('.tab:eq(2) .file-name').text()).toBe 'untitled'
describe "when an edit session is removed", ->
it "removes the tab for the removed edit session", ->
editor.setActiveEditSessionIndex(0)
editor.destroyActiveEditSession()
expect(tabs.find('.tab').length).toBe 1
expect(tabs.find('.tab:eq(0) .file-name').text()).toBe 'sample.txt'
describe "when a tab is clicked", ->
it "activates the associated edit session", ->
expect(editor.getActiveEditSessionIndex()).toBe 1
tabs.find('.tab:eq(0)').click()
expect(editor.getActiveEditSessionIndex()).toBe 0
tabs.find('.tab:eq(1)').click()
expect(editor.getActiveEditSessionIndex()).toBe 1
describe "when a file name associated with a tab changes", ->
[buffer, newPath] = []
beforeEach ->
buffer = editor.editSessions[0].buffer
oldPath = buffer.getPath()
newPath = oldPath.replace(/sample.js$/, "foobar.js")
afterEach ->
fs.remove(newPath)
it "updates the file name in the tab", ->
buffer.saveAs(newPath)
expect(tabs.find('.tab:first .file-name')).toHaveText "foobar.js"

View File

@@ -0,0 +1,14 @@
{View} = require 'space-pen'
module.exports =
class Tab extends View
@content: (editSession) ->
@div class: 'tab', =>
@div class: 'file-name', outlet: 'fileName'
initialize: (@editSession) ->
@updateFileName()
@editSession.on 'buffer-path-change.tab', => @updateFileName()
updateFileName: ->
@fileName.text(@editSession.buffer.getBaseName() ? 'untitled')

View File

@@ -0,0 +1,43 @@
$ = require 'jquery'
{View} = require 'space-pen'
Tab = require 'tabs/src/tab'
module.exports =
class Tabs extends View
@activate: (rootView) ->
requireStylesheet 'tabs/src/tabs.css'
for editor in rootView.getEditors()
@prependToEditorPane(rootView, editor) if rootView.parents('html').length
rootView.on 'editor-open', (e, editor) =>
@prependToEditorPane(rootView, editor)
@prependToEditorPane: (rootView, editor) ->
if pane = editor.pane()
pane.prepend(new Tabs(editor))
@content: ->
@div class: 'tabs'
initialize: (@editor) ->
for editSession, index in @editor.editSessions
@addTabForEditSession(editSession)
@setActiveTab(@editor.getActiveEditSessionIndex())
@editor.on 'editor:active-edit-session-changed', (e, editSession, index) => @setActiveTab(index)
@editor.on 'editor:edit-session-added', (e, editSession) => @addTabForEditSession(editSession)
@editor.on 'editor:edit-session-removed', (e, editSession, index) => @removeTabAtIndex(index)
@on 'click', '.tab', (e) =>
@editor.setActiveEditSessionIndex($(e.target).closest('.tab').index())
addTabForEditSession: (editSession) ->
@append(new Tab(editSession))
setActiveTab: (index) ->
@find(".tab.active").removeClass('active')
@find(".tab:eq(#{index})").addClass('active')
removeTabAtIndex: (index) ->
@find(".tab:eq(#{index})").remove()

View File

@@ -0,0 +1,26 @@
.tabs {
background: #222;
border-bottom: 4px solid #555;
}
.tab {
cursor: default;
float: left;
margin: 4px;
margin-bottom: 0;
margin-right: 0;
padding: 4px;
background: #3a3a3a;
color: white;
-webkit-border-top-left-radius: 4px;
-webkit-border-top-right-radius: 4px;
}
.tab.active {
background: #555;
}
.tab:last-child {
margin-right: 4px;
}