Merge remote-tracking branch 'origin/master' into config

This commit is contained in:
Corey Johnson
2012-12-27 10:38:24 -08:00
34 changed files with 476 additions and 150 deletions

View File

@@ -10,7 +10,7 @@ class Directory
constructor: (@path) ->
getBaseName: ->
fs.base(@path) + '/'
fs.base(@path)
getPath: -> @path

View File

@@ -326,6 +326,14 @@ class Editor extends View
@removeClass 'focused'
@autosave() if config.get "editor.autosave"
@underlayer.on 'click', (e) =>
return unless e.target is @underlayer[0]
return unless e.offsetY > @overlayer.height()
if e.shiftKey
@selectToBottom()
else
@moveCursorToBottom()
@overlayer.on 'mousedown', (e) =>
@overlayer.hide()
clickedElement = document.elementFromPoint(e.pageX, e.pageY)
@@ -738,7 +746,7 @@ class Editor extends View
height = @lineHeight * @screenLineCount()
unless @layerHeight == height
@renderedLines.height(height)
@underlayer.height(height)
@underlayer.css('min-height', height)
@overlayer.height(height)
@layerHeight = height
@@ -751,6 +759,7 @@ class Editor extends View
@underlayer.css('min-width', minWidth)
@overlayer.css('min-width', minWidth)
@layerMinWidth = minWidth
@trigger 'editor:min-width-changed'
clearRenderedLines: ->
@renderedLines.empty()

View File

@@ -11,6 +11,11 @@ class Range
else
new Range(object.start, object.end)
@fromPointWithDelta: (point, rowDelta, columnDelta) ->
pointA = Point.fromObject(point)
pointB = new Point(point.row + rowDelta, point.column + columnDelta)
new Range(pointA, pointB)
constructor: (pointA = new Point(0, 0), pointB = new Point(0, 0)) ->
pointA = Point.fromObject(pointA)
pointB = Point.fromObject(pointB)

View File

@@ -42,11 +42,11 @@ class TextMateBundle
@grammarByShebang: (filePath) ->
try
firstLine = fs.read(filePath).match(/.*/)[0]
fileContents = fs.read(filePath)
catch e
null
_.find @grammars, (grammar) -> grammar.firstLineRegex?.test(firstLine)
_.find @grammars, (grammar) -> grammar.firstLineRegex?.test(fileContents)
@grammarForScopeName: (scopeName) ->
@grammarsByScopeName[scopeName]

View File

@@ -53,6 +53,7 @@ class TextMateGrammar
tokens.push(nextTokens...)
position = tokensEndPosition
break if position is line.length and nextTokens.length is 0
else # push filler token for unmatched text at end of line
if position < line.length

View File

@@ -96,6 +96,15 @@ describe "Autocomplete", ->
expect(autocomplete.matchesList.find('li').length).toBe 1
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveText "No matches found"
it "autocompletes word and replaces case of prefix with case of word", ->
editor.getBuffer().insert([10,0] ,"extra:SO:extra")
editor.setCursorBufferPosition([10,8])
autocomplete.attach()
expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra"
expect(editor.getCursorBufferPosition()).toEqual [10,10]
expect(editor.getSelection().isEmpty()).toBeTruthy()
describe "when text is selected", ->
it 'autocompletes word when there is only a prefix', ->
editor.getBuffer().insert([10,0] ,"extra:sort:extra")
@@ -399,5 +408,3 @@ describe "Autocomplete", ->
editor.trigger 'core:move-up'
expect(editor.getCursorBufferPosition().row).toBe 0

View File

@@ -185,11 +185,10 @@ class Autocomplete extends View
{prefix, suffix} = @prefixAndSuffixOfSelection(selection)
if (prefix.length + suffix.length) > 0
regex = new RegExp("^#{prefix}(.+)#{suffix}$", "i")
regex = new RegExp("^#{prefix}.+#{suffix}$", "i")
currentWord = prefix + @editor.getSelectedText() + suffix
for word in @wordList when regex.test(word) and word != currentWord
match = regex.exec(word)
{prefix, suffix, word, infix: match[1]}
{prefix, suffix, word}
else
[]
@@ -197,9 +196,15 @@ class Autocomplete extends View
selection = @editor.getSelection()
startPosition = selection.getBufferRange().start
@isAutocompleting = true
@editor.insertText(match.infix)
buffer = @editor.getBuffer()
@editor.activeEditSession.transact =>
selection.deleteSelectedText()
buffer.delete(Range.fromPointWithDelta(@editor.getCursorBufferPosition(), 0, -match.prefix.length))
buffer.delete(Range.fromPointWithDelta(@editor.getCursorBufferPosition(), 0, match.suffix.length))
@editor.insertText(match.word)
@currentMatchBufferRange = [startPosition, [startPosition.row, startPosition.column + match.infix.length]]
infixLength = match.word.length - match.prefix.length - match.suffix.length
@currentMatchBufferRange = [startPosition, [startPosition.row, startPosition.column + infixLength]]
@editor.setSelectedBufferRange(@currentMatchBufferRange)
@isAutocompleting = false

View File

@@ -2,6 +2,7 @@ RootView = require 'root-view'
FuzzyFinder = require 'fuzzy-finder'
$ = require 'jquery'
{$$} = require 'space-pen'
fs = require 'fs'
describe 'FuzzyFinder', ->
[rootView, finder] = []
@@ -56,7 +57,7 @@ describe 'FuzzyFinder', ->
runs ->
expect(finder.list.children('li').length).toBe paths.length, finder.maxResults
for path in paths
expect(finder.list.find("li:contains(#{path})")).toExist()
expect(finder.list.find("li:contains(#{fs.base(path)})")).toExist()
expect(finder.list.children().first()).toHaveClass 'selected'
expect(finder.find(".loading")).not.toBeVisible()

View File

@@ -2,6 +2,7 @@
SelectList = require 'select-list'
_ = require 'underscore'
$ = require 'jquery'
fs = require 'fs'
module.exports =
class FuzzyFinder extends SelectList
@@ -25,7 +26,20 @@ class FuzzyFinder extends SelectList
@projectPaths = null
itemForElement: (path) ->
$$ -> @li path
$$ ->
@li =>
ext = fs.extension(path)
if fs.isCompressedExtension(ext)
typeClass = 'compressed-name'
else if fs.isImageExtension(ext)
typeClass = 'image-name'
else if fs.isPdfExtension(ext)
typeClass = 'pdf-name'
else
typeClass = 'text-name'
@span fs.base(path), class: "file #{typeClass}"
if folder = fs.directory(path)
@span "- #{folder}/", class: 'directory'
confirmed : (path) ->
return unless path.length

View File

@@ -19,15 +19,15 @@ class StatusBar extends View
@content: ->
@div class: 'status-bar', =>
@div class: 'file-info', =>
@span class: 'git-branch', outlet: 'branchArea', =>
@span class: 'octicons branch-icon'
@span class: 'branch-label', outlet: 'branchLabel'
@span class: 'git-status', outlet: 'gitStatusIcon'
@span class: 'file-info', =>
@span class: 'current-path', outlet: 'currentPath'
@span class: 'buffer-modified', outlet: 'bufferModified'
@div class: 'cursor-position', =>
@span outlet: 'gitStatusIcon'
@span outlet: 'branchArea', =>
@span class: 'octicons branch-icon'
@span class: 'branch-label', outlet: 'branchLabel'
@span outlet: 'cursorPosition'
@span class: 'cursor-position', outlet: 'cursorPosition'
initialize: (@rootView, @editor) ->
@updatePathText()
@@ -76,7 +76,7 @@ class StatusBar extends View
@gitStatusIcon.empty()
return unless path
@gitStatusIcon.removeClass().addClass('octicons')
@gitStatusIcon.removeClass().addClass('git-status octicons')
if @buffer.getGit()?.isPathModified(path)
@gitStatusIcon.addClass('modified-status-icon')
else if @buffer.getGit()?.isPathNew(path)

View File

@@ -1,34 +1,87 @@
.tabs {
background: #222;
border-bottom: 4px solid #555;
background: #333333;
border-bottom: 4px solid #424242;
font: caption !important;
}
.tab {
cursor: default;
float: left;
margin: 4px;
margin-bottom: 0;
margin-right: 0;
padding-left: 4px;
padding-right: 4px;
background: #3a3a3a;
color: #d0d0d0;
-webkit-border-top-left-radius: 4px;
-webkit-border-top-right-radius: 4px;
font-size: 90%;
padding: 2px 21px 2px 9px;
background-image: -webkit-linear-gradient(#444, #3d3d3d);
color: #a5aaaa;
display: table-cell;
position: relative;
width:175px;
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;
min-width: 40px;
box-sizing: border-box;
height: 24px;
}
.tab.active {
background: #555;
color: white;
.tab:first-child {
box-shadow: inset 0 0 5px #383838, 0 1px 0 #585858, inset -1px 0 0 #4a4a4a;
}
.tab:last-child {
margin-right: 4px;
.tab.active:first-child,
.tab.active:first-child:hover {
box-shadow: inset -1px 0 0 #595959;
}
.tab.active,
.tab.active:hover {
color: #dae6e6;
border-top: 1px solid #4a4a4a;
box-shadow: inset -1px 0 0 #595959, inset 1px 0 0 #595959;
border-bottom: 0 none;
background-image: -webkit-linear-gradient(#555555, #424242);
}
.tab.active:before,
.tab.active:after {
position: absolute;
bottom: -1px;
width: 4px;
height: 4px;
content: " ";
z-index: 3;
border: 1px solid #595959;
}
.tab.active:before {
border-bottom-right-radius: 4px;
border-width: 0 1px 1px 0;
box-shadow: 2px 2px 0 #424242;
left: -4px;
}
.tab.active:after {
right: -4px;
border-bottom-left-radius: 4px;
border-width: 0 0 1px 1px;
box-shadow: -2px 2px 0 #424242;
}
.tab.active:first-child:before {
display: none;
}
.tab:hover {
color: #c8c8c5;
background-image: -webkit-linear-gradient(#474747, #444444);
}
.tab .file-name {
margin-right: 5px;
font-size: 11px !important;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-shadow: 0 -1px 1px black;
position: absolute;
left: 9px;
top:4px;
bottom:4px;
right: 21px;
}
.tab .close-icon {
@@ -36,12 +89,17 @@
font-size: 14px;
width: 14px;
height: 14px;
display: block;
color: #777;
cursor: pointer;
position: absolute;
right: 4px;
top: -1px;
-webkit-font-smoothing: antialiased;
}
.tab .close-icon:before {
content: "\f050";
content: "\f081";
}
.tab .close-icon:hover {

View File

@@ -26,18 +26,18 @@ describe "TreeView", ->
describe ".initialize(project)", ->
it "renders the root of the project and its contents alphabetically with subdirectories first in a collapsed state", ->
expect(treeView.root.find('> .header .disclosure-arrow')).toHaveText('')
expect(treeView.root.find('> .header .name')).toHaveText('tree-view/')
expect(treeView.root.find('> .header .disclosure-arrow')).not.toHaveClass('expanded')
expect(treeView.root.find('> .header .name')).toHaveText('tree-view')
rootEntries = treeView.root.find('.entries')
subdir0 = rootEntries.find('> li:eq(0)')
expect(subdir0.find('.disclosure-arrow')).toHaveText('')
expect(subdir0.find('.name')).toHaveText('dir1/')
expect(subdir0).not.toHaveClass('expanded')
expect(subdir0.find('.name')).toHaveText('dir1')
expect(subdir0.find('.entries')).not.toExist()
subdir2 = rootEntries.find('> li:eq(1)')
expect(subdir2.find('.disclosure-arrow')).toHaveText('')
expect(subdir2.find('.name')).toHaveText('dir2/')
expect(subdir2).not.toHaveClass('expanded')
expect(subdir2.find('.name')).toHaveText('dir2')
expect(subdir2.find('.entries')).not.toExist()
expect(rootEntries.find('> .file:contains(tree-view.js)')).toExist()
@@ -194,40 +194,27 @@ describe "TreeView", ->
expect(treeView).not.toMatchSelector(':focus')
expect(rootView.getActiveEditor().isFocused).toBeTruthy()
describe "when core:close is triggered on the tree view", ->
it "detaches the TreeView, focuses the RootView and does not bubble the core:close event", ->
treeView.attach()
treeView.focus()
rootViewCloseHandler = jasmine.createSpy('rootViewCloseHandler')
rootView.on 'core:close', rootViewCloseHandler
spyOn(rootView, 'focus')
treeView.trigger('core:close')
expect(rootView.focus).toHaveBeenCalled()
expect(rootViewCloseHandler).not.toHaveBeenCalled()
expect(treeView.hasParent()).toBeFalsy()
describe "when a directory's disclosure arrow is clicked", ->
it "expands / collapses the associated directory", ->
subdir = treeView.root.find('.entries > li:contains(dir1/)').view()
subdir = treeView.root.find('.entries > li:contains(dir1)').view()
expect(subdir.disclosureArrow).toHaveText('')
expect(subdir).not.toHaveClass('expanded')
expect(subdir.find('.entries')).not.toExist()
subdir.disclosureArrow.click()
expect(subdir.disclosureArrow).toHaveText('')
expect(subdir).toHaveClass('expanded')
expect(subdir.find('.entries')).toExist()
subdir.disclosureArrow.click()
expect(subdir.disclosureArrow).toHaveText('')
expect(subdir).not.toHaveClass('expanded')
expect(subdir.find('.entries')).not.toExist()
it "restores the expansion state of descendant directories", ->
child = treeView.root.find('.entries > li:contains(dir1/)').view()
child = treeView.root.find('.entries > li:contains(dir1)').view()
child.disclosureArrow.click()
grandchild = child.find('.entries > li:contains(sub-dir1/)').view()
grandchild = child.find('.entries > li:contains(sub-dir1)').view()
grandchild.disclosureArrow.click()
treeView.root.disclosureArrow.click()
@@ -235,16 +222,16 @@ describe "TreeView", ->
treeView.root.disclosureArrow.click()
# previously expanded descendants remain expanded
expect(treeView.root.find('> .entries > li:contains(dir1/) > .entries > li:contains(sub-dir1/) > .entries').length).toBe 1
expect(treeView.root.find('> .entries > li:contains(dir1) > .entries > li:contains(sub-dir1) > .entries').length).toBe 1
# collapsed descendants remain collapsed
expect(treeView.root.find('> .entries > li.contains(dir2/) > .entries')).not.toExist()
expect(treeView.root.find('> .entries > li.contains(dir2) > .entries')).not.toExist()
it "when collapsing a directory, removes change subscriptions from the collapsed directory and its descendants", ->
child = treeView.root.entries.find('li:contains(dir1/)').view()
child = treeView.root.entries.find('li:contains(dir1)').view()
child.disclosureArrow.click()
grandchild = child.entries.find('li:contains(sub-dir1/)').view()
grandchild = child.entries.find('li:contains(sub-dir1)').view()
grandchild.disclosureArrow.click()
expect(treeView.root.directory.subscriptionCount()).toBe 1
@@ -359,7 +346,7 @@ describe "TreeView", ->
beforeEach ->
nested = treeView.root.find('.directory:eq(2)').view()
expect(nested.find('.header').text()).toContain 'nested/'
expect(nested.find('.header').text()).toContain 'nested'
nested.expand()
nested2 = nested.entries.find('.entry:last').view()
nested2.click()
@@ -486,7 +473,7 @@ describe "TreeView", ->
entryCount = treeView.find(".entry").length
_.times entryCount, -> treeView.moveDown()
expect(treeView.scrollBottom()).toBe treeView.prop('scrollHeight')
expect(treeView.scrollBottom() + 2).toBe treeView.prop('scrollHeight')
_.times entryCount, -> treeView.moveUp()
expect(treeView.scrollTop()).toBe 0
@@ -668,7 +655,7 @@ describe "TreeView", ->
expect(rootView.getActiveEditor().getPath()).not.toBe newPath
expect(treeView).toMatchSelector(':focus')
expect(rootView.getActiveEditor().isFocused).toBeFalsy()
expect(dirView.find('.directory.selected:contains(new/)').length).toBe(1)
expect(dirView.find('.directory.selected:contains(new)').length).toBe(1)
it "selects the created directory", ->
treeView.attachToDom()
@@ -681,7 +668,7 @@ describe "TreeView", ->
expect(rootView.getActiveEditor().getPath()).not.toBe newPath
expect(treeView).toMatchSelector(':focus')
expect(rootView.getActiveEditor().isFocused).toBeFalsy()
expect(dirView.find('.directory.selected:contains(new2/)').length).toBe(1)
expect(dirView.find('.directory.selected:contains(new2)').length).toBe(1)
describe "when a file or directory already exists at the given path", ->
it "shows an error message and does not close the dialog", ->

View File

@@ -9,8 +9,9 @@ class DirectoryView extends View
@content: ({directory, isExpanded} = {}) ->
@li class: 'directory entry', =>
@div outlet: 'header', class: 'header', =>
@span '', class: 'disclosure-arrow', outlet: 'disclosureArrow'
@span class: 'disclosure-arrow', outlet: 'disclosureArrow'
@span directory.getBaseName(), class: 'name', outlet: 'directoryName'
@span "", class: 'highlight'
directory: null
entries: null
@@ -47,7 +48,6 @@ class DirectoryView extends View
expand: ->
return if @isExpanded
@addClass('expanded')
@disclosureArrow.text('')
@buildEntries()
@watchEntries()
@deserializeEntryExpansionStates(@entryStates) if @entryStates?
@@ -57,7 +57,6 @@ class DirectoryView extends View
collapse: ->
@entryStates = @serializeEntryExpansionStates()
@removeClass('expanded')
@disclosureArrow.text('')
@unwatchEntries()
@entries.remove()
@entries = null

View File

@@ -1,16 +1,31 @@
{View, $$} = require 'space-pen'
$ = require 'jquery'
Git = require 'git'
fs = require 'fs'
module.exports =
class FileView extends View
@content: (file) ->
@li file.getBaseName(), class: 'file entry'
@li class: 'file entry', =>
@span file.getBaseName(), class: 'name', outlet: 'fileName'
@span "", class: 'highlight'
file: null
initialize: (@file) ->
@addClass('ignored') if new Git(@getPath()).isPathIgnored(@getPath())
path = @getPath()
extension = fs.extension(path)
if fs.isCompressedExtension(extension)
@fileName.addClass('compressed-name')
else if fs.isImageExtension(extension)
@fileName.addClass('image-name')
else if fs.isPdfExtension(extension)
@fileName.addClass('pdf-name')
else
@fileName.addClass('text-name')
@addClass('ignored') if new Git(path).isPathIgnored(path)
getPath: ->
@file.path

View File

@@ -27,7 +27,7 @@ class TreeView extends ScrollView
@instance.serialize()
@content: (rootView) ->
@div class: 'tree-view tool-panel', tabindex: -1
@ol class: 'tree-view tool-panel', tabindex: -1
@deserialize: (state, rootView) ->
treeView = new TreeView(rootView)
@@ -48,7 +48,7 @@ class TreeView extends ScrollView
@on 'click', '.entry', (e) => @entryClicked(e)
@command 'core:move-up', => @moveUp()
@command 'core:move-down', => @moveDown()
@command 'core:close', => @detach(); false
@command 'core:close', => false
@command 'tree-view:expand-directory', => @expandDirectory()
@command 'tree-view:collapse-directory', => @collapseDirectory()
@command 'tree-view:open-selected-entry', => @openSelectedEntry(true)

View File

@@ -10,6 +10,7 @@ describe "WrapGuide", ->
rootView.attachToDom()
editor = rootView.getActiveEditor()
wrapGuide = rootView.find('.wrap-guide').view()
editor.width(editor.charWidth * wrapGuide.defaultColumn * 2)
afterEach ->
rootView.deactivate()
@@ -27,6 +28,7 @@ describe "WrapGuide", ->
width = editor.charWidth * wrapGuide.defaultColumn
expect(width).toBeGreaterThan(0)
expect(wrapGuide.position().left).toBe(width)
expect(wrapGuide).toBeVisible()
describe "when the font size changes", ->
it "updates the wrap guide position", ->
@@ -34,6 +36,7 @@ describe "WrapGuide", ->
expect(initial).toBeGreaterThan(0)
rootView.trigger('window:increase-font-size')
expect(wrapGuide.position().left).toBeGreaterThan(initial)
expect(wrapGuide).toBeVisible()
describe "overriding getGuideColumn", ->
it "invokes the callback with the editor path", ->
@@ -41,7 +44,7 @@ describe "WrapGuide", ->
wrapGuide.getGuideColumn = (path) ->
editorPath = path
80
wrapGuide.updateGuide(editor)
wrapGuide.updateGuide()
expect(editorPath).toBe(require.resolve('fixtures/sample.js'))
it "invokes the callback with a default value", ->
@@ -51,7 +54,7 @@ describe "WrapGuide", ->
column = defaultColumn
defaultColumn
wrapGuide.updateGuide(editor)
wrapGuide.updateGuide()
expect(column).toBeGreaterThan(0)
# this is disabled because we no longer support passing config to an extension
@@ -68,5 +71,11 @@ describe "WrapGuide", ->
it "hides the guide when the column is less than 1", ->
wrapGuide.getGuideColumn = (path) ->
-1
wrapGuide.updateGuide(editor)
wrapGuide.updateGuide()
expect(wrapGuide).toBeHidden()
describe "when no lines exceed the guide column and the editor width is smaller than the guide column position", ->
it "hides the guide", ->
editor.width(10)
wrapGuide.updateGuide()
expect(wrapGuide).toBeHidden()

View File

@@ -1,4 +1,5 @@
{View} = require 'space-pen'
$ = require 'jquery'
module.exports =
class WrapGuide extends View
@@ -28,13 +29,18 @@ class WrapGuide extends View
else
@getGuideColumn = (path, defaultColumn) -> defaultColumn
@observeConfig 'editor.fontSize', => @updateGuide(@editor)
@subscribe @editor, 'editor-path-change', => @updateGuide(@editor)
@subscribe @editor, 'before-remove', => @rootView.off('.wrap-guide')
@observeConfig 'editor.fontSize', => @updateGuide()
@subscribe @editor, 'editor-path-change', => @updateGuide()
@subscribe @editor, 'editor:min-width-changed', => @updateGuide()
@subscribe $(window), 'resize', => @updateGuide()
updateGuide: (editor) ->
column = @getGuideColumn(editor.getPath(), @defaultColumn)
updateGuide: ->
column = @getGuideColumn(@editor.getPath(), @defaultColumn)
if column > 0
@css('left', "#{editor.charWidth * column}px").show()
columnWidth = @editor.charWidth * column
if columnWidth < @editor.layerMinWidth or columnWidth < @editor.width()
@css('left', "#{columnWidth}px").show()
else
@hide()
else
@hide()

View File

@@ -123,3 +123,24 @@ module.exports =
md5ForPath: (path) ->
$native.md5ForPath(path)
isCompressedExtension: (ext) ->
_.contains([
'.gz'
'.jar'
'.tar'
'.zip'
], ext)
isImageExtension: (ext) ->
_.contains([
'.gif'
'.jpeg'
'.jpg'
'.png'
], ext)
isPdfExtension: (ext) ->
_.contains([
'.pdf'
], ext)