diff --git a/Rakefile b/Rakefile index 2dd0680d3..1f9896159 100644 --- a/Rakefile +++ b/Rakefile @@ -97,7 +97,7 @@ task "clone-default-bundles" => "create-dot-atom" do "https://github.com/textmate/css.tmbundle.git" => "aa549903ff01e9ba7dc0bd83f2cfe7ab54feab2d", "https://github.com/textmate/html.tmbundle.git" => "af4fef34e1df538eda9a166912047b610530ece0", "https://github.com/textmate/javascript.tmbundle.git" => "58e81b0eae498c9a4eb6e395368df3b7a01d9851", - "https://github.com/textmate/ruby-on-rails.tmbundle.git" => "7c410a098f0e343d52f70b4f9c08b8669d0a594c", + "https://github.com/drnic/ruby-on-rails-tmbundle.git" => "683f6e652cc467e0007f80aa11f174a4d9189f3c", "https://github.com/textmate/ruby.tmbundle.git" => "daad8ef03de9630e74578a046240fd9acc63b8b5", "https://github.com/textmate/text.tmbundle.git" => "061224bd78fd98d02035466cdd959bf29995c2c5", "https://github.com/jashkenas/coffee-script-tmbundle.git" => "20d9b95240bbbc27565c74c7227b8c6eb9049f78", @@ -105,8 +105,11 @@ task "clone-default-bundles" => "create-dot-atom" do "https://github.com/textmate/c.tmbundle.git" => "c8a6516c1131055bfcd1bca5e2ee6567c2f50058", "https://github.com/textmate/objective-c.tmbundle.git" => "b0826e645a3d8ca37dd625a56935d49cc8eeb9fc", "https://github.com/textmate/git.tmbundle.git" => "132724ab03dfc70fc77deca8ef1a359dc8404d80", + "https://github.com/textmate/json.tmbundle.git" => "5765750aa970c72decef3e2fcefec16a0f22e427", } + `rm -rf #{File.join(DOT_ATOM_PATH, 'bundles', 'ruby-on-rails.tmbundle')}` # github/textmate removed this repo, use drnic's repo instead + for bundle_url, sha in bundles bundle_dir = bundle_url[/([^\/]+?)(\.git)?$/, 1] dest_path = File.join(DOT_ATOM_PATH, "packages", bundle_dir) @@ -195,4 +198,4 @@ if [ $WAIT ]; then sleep 1 done fi -EOF \ No newline at end of file +EOF diff --git a/native/mac/atom.icns b/native/mac/atom.icns index f3beba809..259c290e2 100644 Binary files a/native/mac/atom.icns and b/native/mac/atom.icns differ diff --git a/script/cibuild b/script/cibuild index 81a2e7f06..9860c8f30 100755 --- a/script/cibuild +++ b/script/cibuild @@ -10,6 +10,6 @@ system 'ssh', '-o', 'StrictHostKeyChecking=no', '-p', '6000', 'atom.githubapp.com', - "cd /Users/atom/code/atom && git fetch -q origin && git reset -q --hard #{sha} && rake test" + "rm -rf ~/.atom && cd /Users/atom/code/atom && git fetch -q origin && git reset -q --hard #{sha} && rake test" exit $?.exitstatus diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 1ee20fa91..10fdf5331 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1992,13 +1992,36 @@ describe "Editor", -> expect(editor.getText()).toBe(originalPathText) describe "when clicking a gutter line", -> - it "moves the cursor to the start of the selected line", -> + beforeEach -> rootView.attachToDom() + + it "moves the cursor to the start of the selected line", -> expect(editor.getCursorScreenPosition()).toEqual [0,0] editor.gutter.find(".line-number:eq(1)").trigger 'click' expect(editor.getCursorScreenPosition()).toEqual [1,0] it "selects to the start of the selected line when shift is pressed", -> - expect(editor.getSelection().getScreenRange()).toEqual [0,0], [0,0] - editor.gutter.find(".line-number:eq(1)").trigger 'click', {shiftKey: true} - expect(editor.getSelection().getScreenRange()).toEqual [0,0], [1,0] + expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [0,0]] + event = $.Event("click") + event.shiftKey = true + editor.gutter.find(".line-number:eq(1)").trigger event + expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [1,0]] + + describe "when clicking below the last line", -> + beforeEach -> + rootView.attachToDom() + + it "move the cursor to the end of the file", -> + expect(editor.getCursorScreenPosition()).toEqual [0,0] + event = $.Event("click") + event.offsetY = Infinity + editor.underlayer.trigger event + expect(editor.getCursorScreenPosition()).toEqual [12,2] + + it "selects to the end of the files when shift is pressed", -> + expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [0,0]] + event = $.Event("click") + event.offsetY = Infinity + event.shiftKey = true + editor.underlayer.trigger event + expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [12,2]] diff --git a/spec/app/text-mate-grammar-spec.coffee b/spec/app/text-mate-grammar-spec.coffee index 16b15e5fa..96c860306 100644 --- a/spec/app/text-mate-grammar-spec.coffee +++ b/spec/app/text-mate-grammar-spec.coffee @@ -242,12 +242,12 @@ describe "TextMateGrammar", -> expect(tokens[0].value).toBe "//" expect(tokens[1].value).toBe " a singleLineComment" - it "does not loop infinitley (regression)", -> + it "does not loop infinitely (regression)", -> grammar = TextMateBundle.grammarForFilePath("hello.js") {tokens, ruleStack} = grammar.tokenizeLine("// line comment") {tokens, ruleStack} = grammar.tokenizeLine(" // second line comment with a single leading space", ruleStack) - describe "when inside an C block", -> + describe "when inside a C block", -> it "correctly parses a method. (regression)", -> grammar = TextMateBundle.grammarForFilePath("hello.c") {tokens, ruleStack} = grammar.tokenizeLine("if(1){m()}") diff --git a/spec/app/tokenized-buffer-spec.coffee b/spec/app/tokenized-buffer-spec.coffee index 2eb7b74fb..7fbff436b 100644 --- a/spec/app/tokenized-buffer-spec.coffee +++ b/spec/app/tokenized-buffer-spec.coffee @@ -383,3 +383,20 @@ describe "TokenizedBuffer", -> expect(tokens[0].scopes).toEqual ["source.c++", "meta.preprocessor.c.include"] expect(tokens[1].value).toBe 'include' expect(tokens[1].scopes).toEqual ["source.c++", "meta.preprocessor.c.include", "keyword.control.import.include.c"] + + describe "when a Ruby source file is tokenized", -> + beforeEach -> + editSession = fixturesProject.buildEditSessionForPath('hello.rb', autoIndent: false) + buffer = editSession.buffer + tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer + editSession.setVisible(true) + fullyTokenize(tokenizedBuffer) + + afterEach -> + editSession.destroy() + + it "doesn't loop infinitely (regression)", -> + expect(tokenizedBuffer.lineForScreenRow(0).text).toBe 'a = {' + expect(tokenizedBuffer.lineForScreenRow(1).text).toBe ' "b" => "c",' + expect(tokenizedBuffer.lineForScreenRow(2).text).toBe '}' + expect(tokenizedBuffer.lineForScreenRow(3).text).toBe '' diff --git a/spec/fixtures/hello.rb b/spec/fixtures/hello.rb new file mode 100644 index 000000000..7cd6688c7 --- /dev/null +++ b/spec/fixtures/hello.rb @@ -0,0 +1,3 @@ +a = { + "b" => "c", +} diff --git a/src/app/directory.coffee b/src/app/directory.coffee index 45e74cad5..79eefe334 100644 --- a/src/app/directory.coffee +++ b/src/app/directory.coffee @@ -10,7 +10,7 @@ class Directory constructor: (@path) -> getBaseName: -> - fs.base(@path) + '/' + fs.base(@path) getPath: -> @path diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 774a3142a..cd2fdc521 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -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() diff --git a/src/app/range.coffee b/src/app/range.coffee index 11efe0c94..3464a3bce 100644 --- a/src/app/range.coffee +++ b/src/app/range.coffee @@ -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) diff --git a/src/app/text-mate-bundle.coffee b/src/app/text-mate-bundle.coffee index aa1fca794..777f245df 100644 --- a/src/app/text-mate-bundle.coffee +++ b/src/app/text-mate-bundle.coffee @@ -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] diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index dbba03344..28c599528 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -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 diff --git a/src/packages/autocomplete/spec/autocomplete-spec.coffee b/src/packages/autocomplete/spec/autocomplete-spec.coffee index c9a22d507..8190f3d31 100644 --- a/src/packages/autocomplete/spec/autocomplete-spec.coffee +++ b/src/packages/autocomplete/spec/autocomplete-spec.coffee @@ -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 - - diff --git a/src/packages/autocomplete/src/autocomplete.coffee b/src/packages/autocomplete/src/autocomplete.coffee index 7f9bad470..73d6f66ec 100644 --- a/src/packages/autocomplete/src/autocomplete.coffee +++ b/src/packages/autocomplete/src/autocomplete.coffee @@ -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 diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index aca8c5153..1306dcf1f 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -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() diff --git a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee index 51c21166a..d51871207 100644 --- a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee +++ b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee @@ -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 diff --git a/src/packages/status-bar/src/status-bar.coffee b/src/packages/status-bar/src/status-bar.coffee index 744304287..83a3e4833 100644 --- a/src/packages/status-bar/src/status-bar.coffee +++ b/src/packages/status-bar/src/status-bar.coffee @@ -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) diff --git a/src/packages/tabs/src/tabs.css b/src/packages/tabs/src/tabs.css index 03388f65c..dc4be74fa 100644 --- a/src/packages/tabs/src/tabs.css +++ b/src/packages/tabs/src/tabs.css @@ -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 { diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index 6dce97b10..d3995dd1b 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -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", -> diff --git a/src/packages/tree-view/src/directory-view.coffee b/src/packages/tree-view/src/directory-view.coffee index a039232b6..197917f61 100644 --- a/src/packages/tree-view/src/directory-view.coffee +++ b/src/packages/tree-view/src/directory-view.coffee @@ -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 diff --git a/src/packages/tree-view/src/file-view.coffee b/src/packages/tree-view/src/file-view.coffee index 6fb40540a..ea636cbfb 100644 --- a/src/packages/tree-view/src/file-view.coffee +++ b/src/packages/tree-view/src/file-view.coffee @@ -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 diff --git a/src/packages/tree-view/src/tree-view.coffee b/src/packages/tree-view/src/tree-view.coffee index fc7b27e9c..92a7b4af8 100644 --- a/src/packages/tree-view/src/tree-view.coffee +++ b/src/packages/tree-view/src/tree-view.coffee @@ -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) diff --git a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee index 82129d6dd..1bc562624 100644 --- a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee +++ b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee @@ -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() diff --git a/src/packages/wrap-guide/src/wrap-guide.coffee b/src/packages/wrap-guide/src/wrap-guide.coffee index 752443e95..8442cc736 100644 --- a/src/packages/wrap-guide/src/wrap-guide.coffee +++ b/src/packages/wrap-guide/src/wrap-guide.coffee @@ -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() diff --git a/src/stdlib/fs.coffee b/src/stdlib/fs.coffee index 59a034984..9b9e97cda 100644 --- a/src/stdlib/fs.coffee +++ b/src/stdlib/fs.coffee @@ -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) diff --git a/static/atom.css b/static/atom.css index e7b073fb5..d666aaac3 100644 --- a/static/atom.css +++ b/static/atom.css @@ -1,5 +1,5 @@ html, body { - font: 16px Inconsolata, Monaco, Courier !important; + font: caption !important; width: 100%; height: 100%; overflow: hidden; @@ -7,9 +7,9 @@ html, body { #root-view { height: 100%; - overflow-y: auto; + overflow: hidden; position: relative; - background-image: url(images/linen.png); + background-color: #f5f5f5; } #root-view #horizontal { diff --git a/static/editor.css b/static/editor.css index e1892bee1..f95816056 100644 --- a/static/editor.css +++ b/static/editor.css @@ -6,6 +6,7 @@ -webkit-box-flex: 1; position: relative; z-index: 0; + font-family: Inconsolata, Monaco, Courier !important; } .editor.mini { diff --git a/static/fuzzy-finder.css b/static/fuzzy-finder.css index 5f3456371..4257e3729 100644 --- a/static/fuzzy-finder.css +++ b/static/fuzzy-finder.css @@ -8,3 +8,39 @@ .fuzzy-finder ol:empty { margin-bottom: 0; } + +.fuzzy-finder .directory { + color: #b2b2b2; + padding-left: .5em; +} + +.fuzzy-finder .file:before { + font-family: 'Octicons Regular'; + font-size: 16px; + width: 16px; + height: 16px; + margin-right: 5px; + margin-left: 5px; + -webkit-font-smoothing: antialiased; + color: #ccc; +} + +.fuzzy-finder .file.text-name:before { + content: "\f011"; +} + +.fuzzy-finder .file.image-name:before { + content: "\f012"; +} + +.fuzzy-finder .file.compressed-name:before { + content: "\f013"; +} + +.fuzzy-finder .file.pdf-name:before { + content: "\f014"; +} + +.fuzzy-finder ol li { + border-bottom: 1px solid rgba(255, 255, 255, .05); +} diff --git a/static/images/belt-small.png b/static/images/belt-small.png deleted file mode 100644 index 3396f0881..000000000 Binary files a/static/images/belt-small.png and /dev/null differ diff --git a/static/images/belt.png b/static/images/belt.png deleted file mode 100644 index 0ee9690e4..000000000 Binary files a/static/images/belt.png and /dev/null differ diff --git a/static/images/linen.png b/static/images/linen.png deleted file mode 100644 index 2288044ac..000000000 Binary files a/static/images/linen.png and /dev/null differ diff --git a/static/select-list.css b/static/select-list.css index a9b099bf9..7adc209ff 100644 --- a/static/select-list.css +++ b/static/select-list.css @@ -9,6 +9,7 @@ color: #eee; -webkit-box-shadow: 0 0 5px 5px #222; padding: 5px; + z-index: 99; } .select-list ol { diff --git a/static/status-bar.css b/static/status-bar.css index 9554c0455..b3e6c1069 100644 --- a/static/status-bar.css +++ b/static/status-bar.css @@ -1,37 +1,30 @@ .status-bar { - background: black; - color: white; - padding: 5px; + background-image: -webkit-linear-gradient(#303030, #252525); + border-top: 1px solid #454545; + padding: 4px 10px 3px; + font-size: 11px; + line-height: 14px; + color: #969696; position: relative; } -.status-bar .file-info { - float: left; - display: inline-block; -} - .status-bar .cursor-position { - position: absolute; - right: 5px; - top: 5px; + padding-left: 10px; } -.status-bar .modified-status-icon { - color: #6C6912; - padding-right: 5px; +.status-bar .git-branch { + float: right; } -.status-bar .modified-status-icon:before { - content: "\f26d"; +.status-bar .branch-label { + padding-left: 5px; + vertical-align: baseline; } -.status-bar .new-status-icon { - color: #269F81; - padding-right: 5px; -} - -.status-bar .new-status-icon:before { - content: "\f26b"; +.status-bar .git-status.octicons { + display: none; + padding-left: 10px; + margin-top:-2px; } .status-bar .octicons { @@ -39,13 +32,31 @@ font-size: 14px; width: 14px; height: 14px; + line-height: 14px; + -webkit-font-smoothing: antialiased; + display: inline-block; + vertical-align: middle; } .status-bar .branch-icon:before { content: "\f020"; } -.status-bar .branch-label { - padding-left: 5px; - padding-right: 10px; +.status-bar .git-status.octicons.modified-status-icon { + color: #f78a46; + display: inline-block; } + +.status-bar .modified-status-icon:before { + content: "\f26d"; +} + +.status-bar .git-status.octicons.new-status-icon { + color: #5293d8; + display: inline-block; +} + +.status-bar .new-status-icon:before { + content: "\f26b"; +} + diff --git a/static/tree-view.css b/static/tree-view.css index 975ad5140..18751fad1 100644 --- a/static/tree-view.css +++ b/static/tree-view.css @@ -1,42 +1,75 @@ .tree-view { position: relative; height: 100%; - background: black; - color: white; + background: #1e1e1e; overflow: auto; - padding: 0 1em; cursor: default; -webkit-user-select: none; + border-right: 2px solid #191919; + min-width: 100px; + z-index: 2; + padding-left: 12px; +} + +.tree-view .entry { + text-shadow: 0 -1px 0 #000; +} + +.tree-view .entries { + margin-left: 12px; +} + +.tree-view .entries .file .name { + margin-left: 15px; +} + +.tree-view .directory.selected .header, +.tree-view .directory.selected > .header .name, +.tree-view .selected > .name { + color: #d2d2d2; +} + +.tree-view .selected > .highlight { + background-image: -webkit-linear-gradient(#4e4e4e, #434343); + position: absolute; + left: 0; + right: 0; + height: 24px; + margin-top:-24px; + z-index: -1; +} + +.tree-view .entry.file .name { + display: block; } .tree-view .disclosure-arrow { - width: 2ex; display: inline-block; } -.tree-view .directory .entries { - padding-left: 1ex; +.tree-view .directory .header { + color: #bebebe; } -.file, .directory > .header { - padding: 0 1ex; +.tree-view .file { + color: #7d7d7d; +} + +.tree-view .entry:hover, +.tree-view .directory .header:hover .name, +.tree-view .directory .header:hover .disclosure-arrow { + color: #ebebeb; +} + +.tree-view .file .name, +.tree-view .directory .header { + padding-top: 4px; + padding-bottom: 4px; + padding-right: 10px; } .tree-view .ignored { - color: #BBB; -} - -.tree-view .directory .entries .file { - padding-left: 3ex; -} - -.tree-view .file.selected, .tree-view .directory.selected > .header { - background: #444; -} - -.tree-view:focus .file.selected, .tree-view:focus .directory.selected > .header { - background: #a3fd97; - color: black; + color: #555; } .tree-view-dialog { @@ -48,3 +81,64 @@ border: 2px solid #222; -webkit-box-shadow: 0 0 3px 3px rgba(0, 0, 0, .5); } + +.tree-view .directory .header .name, +.tree-view .file .name { + position: relative; + padding-left: 21px; +} + +.tree-view .directory .header .name:before, +.tree-view .file .name:before { + font-family: 'Octicons Regular'; + font-size: 16px; + width: 16px; + height: 16px; + margin-right: 5px; + -webkit-font-smoothing: antialiased; + position: absolute; + left: 0; +} + +.tree-view .disclosure-arrow:before { + font-family: 'Octicons Regular'; + font-size: 12px; + width: 12px; + height: 12px; + line-height: 16px; + margin-right: 3px; + -webkit-font-smoothing: antialiased; +} + +.tree-view .directory .header .name:before { + content: "\f016"; + top: -5px; +} + +.tree-view .file .text-name:before { + content: "\f011"; + top: -2px; +} + +.tree-view .file .image-name:before { + content: "\f012"; + top: -2px; +} + +.tree-view .file .compressed-name:before { + content: "\f013"; + top: -2px; +} + +.tree-view .file .pdf-name:before { + content: "\f014"; + top: -2px; +} + +.tree-view .directory > .header .disclosure-arrow:before { + content: "\f05a"; +} + +.tree-view .directory.expanded > .header .disclosure-arrow:before { + content: "\f05b"; +}