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

Conflicts:
	native/v8_extensions/native.mm
	spec/app/config-spec.coffee
	spec/app/window-spec.coffee
	spec/spec-helper.coffee
	spec/stdlib/fs-utils-spec.coffee
	src/app/atom-package.coffee
	src/app/config.coffee
	src/app/window.coffee
	src/packages/fuzzy-finder/lib/load-paths-handler.coffee
	src/packages/markdown-preview/lib/markdown-preview-view.coffee
	src/packages/tree-view/spec/tree-view-spec.coffee
	src/stdlib/require.coffee
This commit is contained in:
Kevin Sawicki & Nathan Sobo 2013-03-20 10:46:50 -06:00
commit 501dc9b76c
104 changed files with 6970 additions and 1164 deletions

View File

@ -7,7 +7,7 @@ Config = require 'config'
Project = require 'project'
require 'window'
requireStylesheet "jasmine.css"
requireStylesheet "jasmine.less"
# Load TextMate bundles, which specs rely on (but not other packages)
atom.loadTextMatePackages()

View File

@ -11,7 +11,7 @@ always hit `meta-p` to bring up a list of commands that are relevant to the
currently focused UI element. If there is a key binding for a given command, it
is also displayed. This is a great way to explore the system and get to know the
key commands interactively. If you'd like to add or change a binding for a
command, refer to the [keymaps](#keymaps) section to learn how.
command, refer to the [key bindings](#customizing-key-bindings) section to learn how.
![Command Palette](http://f.cl.ly/items/32041o3w471F3C0F0V2O/Screen%20Shot%202013-02-13%20at%207.27.41%20PM.png)

View File

@ -0,0 +1,97 @@
## Serialization in Atom
When a window is refreshed or restored from a previous session, the view and its
associated objects are *deserialized* from a JSON representation that was stored
during the window's previous shutdown. For your own views and objects to be
compatible with refreshing, you'll need to make them play nicely with the
serializing and deserializing.
### Package Serialization Hook
Your package's main module can optionally include a `serialize` method, which
will be called before your package is deactivated. You should return JSON, which
will be handed back to you as an argument to `activate` next time it is called.
In the following example, the package keeps an instance of `MyObject` in the
same state across refreshes.
```coffee-script
module.exports =
activate: (state) ->
@myObject =
if state
deserialize(state)
else
new MyObject("Hello")
serialize: ->
@myObject.serialize()
```
### Serialization Methods
```coffee-script
class MyObject
registerDeserializer(this)
@deserialize: ({data}) -> new MyObject(data)
constructor: (@data) ->
serialize: -> { deserializer: 'MyObject', data: @data }
```
#### .serialize()
Objects that you want to serialize should implement `.serialize()`. This method
should return a serializable object, and it must contain a key named
`deserializer` whose value is the name of a registered deserializer that can
convert the rest of the data to an object. It's usually just the name of the
class itself.
#### @deserialize(data)
The other side of the coin is the `deserialize` method, which is usually a
class-level method on the same class that implements `serialize`. This method's
job is to convert a state object returned from a previous call `serialize` back
into a genuine object.
#### registerDeserializer(klass)
You need to call the global `registerDeserializer` method with your class in
order to make it available to the deserialization system. Now you can call the
global `deserialize` method with state returned from `serialize`, and your
class's `deserialize` method will be selected automatically.
### Versioning
```coffee-script
class MyObject
@version: 2
@deserialize: (state) -> ...
serialize: -> { version: MyObject.version, ... }
```
Your serializable class can optionally have a class-level `@version` property
and include a `version` key in its serialized state. When deserializing, Atom
will only attempt to call deserialize if the two versions match, and otherwise
return undefined. We plan on implementing a migration system in the future, but
this at least protects you from improperly deserializing old state. If you find
yourself in dire need of the migration system, let us know.
### Deferred Package Deserializers
If your package defers loading on startup with an `activationEvents` property in
its `package.cson`, your deserializers won't be loaded until your package is
activated. If you want to deserialize an object from your package on startup,
this could be a problem.
The solution is to also supply a `deferredDeserializers` array in your
`package.cson` with the names of all your deserializers. When Atom attempts to
deserialize some state whose `deserializer` matches one of these names, it will
load your package first so it can register any necessary deserializers before
proceeding.
For example, the markdown preview package doesn't fully load until a preview is
triggered. But if you refresh a window with a preview pane, it loads the
markdown package early so Atom can deserialize the view correctly.
```coffee-script
# markdown-preview/package.cson
'activationEvents': 'markdown-preview:toggle': '.editor'
'deferredDeserializers': ['MarkdownPreviewView']
...
```

View File

@ -18,6 +18,7 @@ class AtomCefClient;
+ (CefSettings)createCefSettings;
+ (NSDictionary *)parseArguments:(char **)argv count:(int)argc;
- (void)open:(NSString *)path;
- (void)openDev:(NSString *)path;
- (void)open:(NSString *)path pidToKillWhenWindowCloses:(NSNumber *)pid;
- (IBAction)runSpecs:(id)sender;
- (IBAction)runBenchmarks:(id)sender;

View File

@ -13,18 +13,14 @@ void activateOpenApp();
BOOL isAppAlreadyOpen();
int AtomMain(int argc, char* argv[]) {
{
// See if we're being run as a secondary process.
CefMainArgs main_args(argc, argv);
CefRefPtr<CefApp> app(new AtomCefApp);
int exitCode = CefExecuteProcess(main_args, app);
if (exitCode >= 0)
return exitCode;
}
// Check if we're being run as a secondary process.
CefMainArgs main_args(argc, argv);
CefRefPtr<CefApp> app(new AtomCefApp);
int exitCode = CefExecuteProcess(main_args, app);
if (exitCode >= 0)
return exitCode;
// We're the main process.
@autoreleasepool {
handleBeingOpenedAgain(argc, argv);
@ -33,7 +29,7 @@ int AtomMain(int argc, char* argv[]) {
NSString *mainNibName = [infoDictionary objectForKey:@"NSMainNibFile"];
NSNib *mainNib = [[NSNib alloc] initWithNibNamed:mainNibName bundle:[NSBundle bundleWithIdentifier:@"com.github.atom.framework"]];
[mainNib instantiateNibWithOwner:application topLevelObjects:nil];
[mainNib instantiateWithOwner:application topLevelObjects:nil];
CefRunMessageLoop();
}

View File

@ -16,6 +16,8 @@
"plist": "git://github.com/nathansobo/node-plist.git"
},
"private": true,
"scripts": {
"preinstall": "true"
}

View File

@ -3,18 +3,31 @@ AtomPackage = require 'atom-package'
fs = require 'fs-utils'
describe "AtomPackage", ->
[packageMainModule, pack] = []
beforeEach ->
pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-activation-events'))
pack.load()
describe ".load()", ->
describe "if the package's metadata has a `deferredDeserializers` array", ->
it "requires the package's main module attempting to use deserializers named in the array", ->
expect(pack.mainModule).toBeNull()
object = deserialize(deserializer: 'Foo', data: "Hello")
expect(object.constructor.name).toBe 'Foo'
expect(object.data).toBe 'Hello'
expect(pack.mainModule).toBeDefined()
expect(pack.mainModule.activateCallCount).toBe 0
describe ".activate()", ->
beforeEach ->
window.rootView = new RootView
packageMainModule = require 'fixtures/packages/package-with-activation-events/main'
spyOn(packageMainModule, 'activate').andCallThrough()
describe "when the package metadata includes activation events", ->
[packageMainModule, pack] = []
beforeEach ->
pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-activation-events'))
packageMainModule = require 'fixtures/packages/package-with-activation-events/main'
spyOn(packageMainModule, 'activate').andCallThrough()
pack.load()
pack.activate()
it "defers activating the package until an activation event bubbles to the root view", ->
expect(packageMainModule.activate).not.toHaveBeenCalled()
@ -44,12 +57,13 @@ describe "AtomPackage", ->
expect(packageMainModule.activate).not.toHaveBeenCalled()
pack.load()
pack.activate()
expect(packageMainModule.activate).toHaveBeenCalled()
describe "when the package doesn't have an index.coffee", ->
it "does not throw an exception or log an error", ->
spyOn(console, "error")
spyOn(console, "warn")
spyOn(console, "warn").andCallThrough()
pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-keymaps-manifest'))
expect(-> pack.load()).not.toThrow()

View File

@ -85,26 +85,26 @@ describe "the `atom` global", ->
describe "activation", ->
it "calls activate on the package main with its previous state", ->
pack = window.loadPackage('package-with-module')
spyOn(pack.packageMain, 'activate')
spyOn(pack.mainModule, 'activate')
serializedState = rootView.serialize()
rootView.deactivate()
RootView.deserialize(serializedState)
window.loadPackage('package-with-module')
expect(pack.packageMain.activate).toHaveBeenCalledWith(someNumber: 1)
expect(pack.mainModule.activate).toHaveBeenCalledWith(someNumber: 1)
describe "deactivation", ->
it "deactivates and removes the package module from the package module map", ->
pack = window.loadPackage('package-with-module')
expect(atom.activatedAtomPackages.length).toBe 1
spyOn(pack.packageMain, "deactivate").andCallThrough()
spyOn(pack.mainModule, "deactivate").andCallThrough()
atom.deactivateAtomPackages()
expect(pack.packageMain.deactivate).toHaveBeenCalled()
expect(pack.mainModule.deactivate).toHaveBeenCalled()
expect(atom.activatedAtomPackages.length).toBe 0
describe "serialization", ->
it "uses previous serialization state on unactivated packages", ->
it "uses previous serialization state on packages whose activation has been deferred", ->
atom.atomPackageStates['package-with-activation-events'] = {previousData: 'exists'}
unactivatedPackage = window.loadPackage('package-with-activation-events')
activatedPackage = window.loadPackage('package-with-module')
@ -116,7 +116,8 @@ describe "the `atom` global", ->
'previousData': 'exists'
# ensure serialization occurs when the packageis activated
unactivatedPackage.activatePackageMain()
unactivatedPackage.deferActivation = false
unactivatedPackage.activate()
expect(atom.serializeAtomPackages()).toEqual
'package-with-module':
'someNumber': 1
@ -125,8 +126,8 @@ describe "the `atom` global", ->
it "absorbs exceptions that are thrown by the package module's serialize methods", ->
spyOn(console, 'error')
window.loadPackage('package-with-module')
window.loadPackage('package-with-serialize-error', activateImmediately: true)
window.loadPackage('package-with-module', activateImmediately: true)
window.loadPackage('package-with-serialize-error', activateImmediately: true)
packageStates = atom.serializeAtomPackages()
expect(packageStates['package-with-module']).toEqual someNumber: 1

View File

@ -133,3 +133,21 @@ describe "Config", ->
expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-light-ui/package.cson'))).toBeTruthy()
expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-dark-syntax.css'))).toBeTruthy()
expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-light-syntax.css'))).toBeTruthy()
describe "when the config file is not parseable", ->
beforeEach ->
config.configDirPath = '/tmp/dot-atom-dir'
config.configFilePath = fs.join(config.configDirPath, "config.cson")
expect(fs.exists(config.configDirPath)).toBeFalsy()
afterEach ->
fs.remove('/tmp/dot-atom-dir') if fs.exists('/tmp/dot-atom-dir')
it "logs an error to the console and does not overwrite the config file", ->
config.save.reset()
spyOn(console, 'error')
fs.write(config.configFilePath, "{{{{{")
config.loadUserConfig()
config.set("hair", "blonde") # trigger a save
expect(console.error).toHaveBeenCalled()
expect(config.save).not.toHaveBeenCalled()

View File

@ -136,6 +136,43 @@ describe "PaneContainer", ->
for item in pane.getItems()
expect(item.saved).toBeTruthy()
describe ".confirmClose()", ->
it "resolves the returned promise after modified files are saved", ->
pane1.itemAtIndex(0).isModified = -> true
pane2.itemAtIndex(0).isModified = -> true
spyOn(atom, "confirm").andCallFake (a, b, c, d, e, f, g, noSaveFn) -> noSaveFn()
promiseHandler = jasmine.createSpy("promiseHandler")
failedPromiseHandler = jasmine.createSpy("failedPromiseHandler")
promise = container.confirmClose()
promise.done promiseHandler
promise.fail failedPromiseHandler
waitsFor ->
promiseHandler.wasCalled
runs ->
expect(failedPromiseHandler).not.toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
it "rejects the returned promise if the user cancels saving", ->
pane1.itemAtIndex(0).isModified = -> true
pane2.itemAtIndex(0).isModified = -> true
spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancelFn, f, g) -> cancelFn()
promiseHandler = jasmine.createSpy("promiseHandler")
failedPromiseHandler = jasmine.createSpy("failedPromiseHandler")
promise = container.confirmClose()
promise.done promiseHandler
promise.fail failedPromiseHandler
waitsFor ->
failedPromiseHandler.wasCalled
runs ->
expect(promiseHandler).not.toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
describe "serialization", ->
it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", ->
newContainer = deserialize(container.serialize())
@ -146,3 +183,10 @@ describe "PaneContainer", ->
newContainer.height(200).width(300).attachToDom()
expect(newContainer.find('.row > :contains(1)').width()).toBe 150
expect(newContainer.find('.row > .column > :contains(2)').height()).toBe 100
it "removes empty panes on deserialization", ->
# only deserialize pane 1's view successfully
TestView.deserialize = ({name}) -> new TestView(name) if name is '1'
newContainer = deserialize(container.serialize())
expect(newContainer.find('.row, .column')).not.toExist()
expect(newContainer.find('> :contains(1)')).toExist()

View File

@ -450,6 +450,14 @@ describe "Pane", ->
pane.remove()
expect(rootView.focus).not.toHaveBeenCalled()
describe ".getNextPane()", ->
it "returns the next pane if one exists, wrapping around from the last pane to the first", ->
pane.showItem(editSession1)
expect(pane.getNextPane()).toBeUndefined
pane2 = pane.splitRight()
expect(pane.getNextPane()).toBe pane2
expect(pane2.getNextPane()).toBe pane
describe "when the pane is focused", ->
it "focuses the active item view", ->
focusHandler = jasmine.createSpy("focusHandler")

View File

@ -134,8 +134,8 @@ describe "Project", ->
project.getFilePaths().done (foundPaths) -> paths = foundPaths
runs ->
expect(paths).not.toContain('a')
expect(paths).toContain('b')
expect(paths).not.toContain(project.resolve('a'))
expect(paths).toContain(project.resolve('b'))
describe "when config.core.hideGitIgnoredFiles is true", ->
it "ignores files that are present in .gitignore if the project is a git repo", ->

View File

@ -20,8 +20,21 @@ describe "@load(name)", ->
expect($(".editor").css("background-color")).toBe("rgb(20, 20, 20)")
describe "AtomTheme", ->
describe "when the theme is a file", ->
it "loads and applies css", ->
expect($(".editor").css("padding-bottom")).not.toBe "1234px"
themePath = project.resolve('themes/theme-stylesheet.css')
theme = Theme.load(themePath)
expect($(".editor").css("padding-top")).toBe "1234px"
it "parses, loads and applies less", ->
expect($(".editor").css("padding-bottom")).not.toBe "1234px"
themePath = project.resolve('themes/theme-stylesheet.less')
theme = Theme.load(themePath)
expect($(".editor").css("padding-top")).toBe "4321px"
describe "when the theme contains a package.json file", ->
it "loads and applies css from package.json in the correct order", ->
it "loads and applies stylesheets from package.json in the correct order", ->
expect($(".editor").css("padding-top")).not.toBe("101px")
expect($(".editor").css("padding-right")).not.toBe("102px")
expect($(".editor").css("padding-bottom")).not.toBe("103px")
@ -32,16 +45,8 @@ describe "@load(name)", ->
expect($(".editor").css("padding-right")).toBe("102px")
expect($(".editor").css("padding-bottom")).toBe("103px")
describe "when the theme is a CSS file", ->
it "loads and applies the stylesheet", ->
expect($(".editor").css("padding-bottom")).not.toBe "1234px"
themePath = project.resolve('themes/theme-stylesheet.css')
theme = Theme.load(themePath)
expect($(".editor").css("padding-top")).toBe "1234px"
describe "when the theme does not contain a package.json file and is a directory", ->
it "loads all CSS files in the directory", ->
it "loads all stylesheet files in the directory", ->
expect($(".editor").css("padding-top")).not.toBe "10px"
expect($(".editor").css("padding-right")).not.toBe "20px"
expect($(".editor").css("padding-bottom")).not.toBe "30px"

View File

@ -1,5 +1,6 @@
$ = require 'jquery'
fs = require 'fs-utils'
{less} = require 'less'
describe "Window", ->
projectPath = null
@ -34,16 +35,31 @@ describe "Window", ->
$(window).trigger 'focus'
expect($("body")).not.toHaveClass("is-blurred")
describe ".close()", ->
it "is triggered by the 'core:close' event", ->
spyOn window, 'close'
$(window).trigger 'core:close'
expect(window.close).toHaveBeenCalled()
describe "window:close event", ->
describe "when no pane items are modified", ->
it "calls window.close", ->
spyOn window, 'close'
$(window).trigger 'window:close'
expect(window.close).toHaveBeenCalled()
it "is triggered by the 'window:close event'", ->
spyOn window, 'close'
$(window).trigger 'window:close'
expect(window.close).toHaveBeenCalled()
describe "when pane items are are modified", ->
it "prompts user to save and and calls window.close", ->
spyOn(window, 'close')
spyOn(atom, "confirm").andCallFake (a, b, c, d, e, f, g, noSave) -> noSave()
editSession = rootView.open("sample.js")
editSession.insertText("I look different, I feel different.")
$(window).trigger 'window:close'
expect(window.close).toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
it "prompts user to save and aborts if dialog is canceled", ->
spyOn(window, 'close')
spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancel) -> cancel()
editSession = rootView.open("sample.js")
editSession.insertText("I look different, I feel different.")
$(window).trigger 'window:close'
expect(window.close).not.toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
describe ".reload()", ->
beforeEach ->
@ -62,22 +78,46 @@ describe "Window", ->
expect(atom.confirm).toHaveBeenCalled()
describe "requireStylesheet(path)", ->
it "synchronously loads the stylesheet at the given path and installs a style tag for it in the head", ->
$('head style[id*="atom.css"]').remove()
it "synchronously loads css at the given path and installs a style tag for it in the head", ->
cssPath = project.resolve('css.css')
lengthBefore = $('head style').length
requireStylesheet('atom.css')
requireStylesheet(cssPath)
expect($('head style').length).toBe lengthBefore + 1
styleElt = $('head style[id*="atom.css"]')
fullPath = require.resolve('atom.css')
expect(styleElt.attr('id')).toBe fullPath
expect(styleElt.text()).toBe fs.read(fullPath)
element = $('head style[id*="css.css"]')
expect(element.attr('id')).toBe cssPath
expect(element.text()).toBe fs.read(cssPath)
# doesn't append twice
requireStylesheet('atom.css')
requireStylesheet(cssPath)
expect($('head style').length).toBe lengthBefore + 1
$('head style[id*="css.css"]').remove()
it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", ->
lessPath = project.resolve('sample.less')
lengthBefore = $('head style').length
requireStylesheet(lessPath)
expect($('head style').length).toBe lengthBefore + 1
element = $('head style[id*="sample.less"]')
expect(element.attr('id')).toBe lessPath
expect(element.text()).toBe """
#header {
color: #4d926f;
}
h2 {
color: #4d926f;
}
"""
# doesn't append twice
requireStylesheet(lessPath)
expect($('head style').length).toBe lengthBefore + 1
$('head style[id*="sample.less"]').remove()
describe ".disableStyleSheet(path)", ->
it "removes styling applied by given stylesheet path", ->
cssPath = require.resolve(fs.join("fixtures", "css.css"))

View File

@ -0,0 +1,3 @@
## File.markdown
:cool:

View File

@ -1,7 +1,14 @@
class Foo
registerDeserializer(this)
@deserialize: ({data}) -> new Foo(data)
constructor: (@data) ->
module.exports =
activateCallCount: 0
activationEventCallCount: 0
activate: ->
@activateCallCount++
rootView.getActiveView()?.command 'activation-event', =>
@activationEventCallCount++

View File

@ -1,2 +1,3 @@
'activationEvents': ['activation-event']
'deferredDeserializers': ['Foo']
'main': 'main'

1
spec/fixtures/sample-with-error.less vendored Normal file
View File

@ -0,0 +1 @@
#header {

8
spec/fixtures/sample.less vendored Normal file
View File

@ -0,0 +1,8 @@
@color: #4D926F;
#header {
color: @color;
}
h2 {
color: @color;
}

View File

@ -0,0 +1,5 @@
@padding: 4321px;
.editor {
padding-top: @padding;
}

View File

@ -1,3 +1,3 @@
{
"stylesheets": ["first.css", "second.css", "last.css"]
"stylesheets": ["first.css", "second.less", "last.css"]
}

View File

@ -1,5 +0,0 @@
.editor {
/* padding-top: 102px;*/
padding-right: 102px;
padding-bottom: 102px;
}

View File

@ -0,0 +1,7 @@
@number: 102px;
.editor {
/* padding-top: 102px;*/
padding-right: @number;
padding-bottom: @number;
}

View File

@ -1,3 +0,0 @@
.editor {
padding-bottom: 30px;
}

View File

@ -0,0 +1,5 @@
@number: 30px;
.editor {
padding-bottom: @number;
}

View File

@ -15,7 +15,7 @@ TokenizedBuffer = require 'tokenized-buffer'
fs = require 'fs-utils'
RootView = require 'root-view'
Git = require 'git'
requireStylesheet "jasmine.css"
requireStylesheet "jasmine.less"
fixturePackagesPath = fs.resolveOnLoadPath('fixtures/packages')
keymap.loadBundledKeymaps()
[bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []
@ -67,6 +67,8 @@ beforeEach ->
spyOn($native, 'writeToPasteboard').andCallFake (text) -> pasteboardContent = text
spyOn($native, 'readFromPasteboard').andCallFake -> pasteboardContent
addCustomMatchers(this)
afterEach ->
keymap.bindingSets = bindingSetsToRestore
keymap.bindingSetsByFirstKeystrokeToRestore = bindingSetsByFirstKeystrokeToRestore
@ -85,12 +87,14 @@ afterEach ->
atom.presentingModal = false
waits(0) # yield to ui thread to make screen update more frequently
window.loadPackage = (name, options) ->
window.loadPackage = (name, options={}) ->
Package = require 'package'
packagePath = _.find atom.getPackagePaths(), (packagePath) -> fs.base(packagePath) == name
if pack = Package.build(packagePath)
pack.load(options)
atom.loadedPackages.push(pack)
pack.deferActivation = false if options.activateImmediately
pack.activate()
pack
# Specs rely on TextMate bundles (but not atom packages)
@ -120,6 +124,23 @@ jasmine.unspy = (object, methodName) ->
throw new Error("Not a spy") unless object[methodName].originalValue?
object[methodName] = object[methodName].originalValue
addCustomMatchers = (spec) ->
spec.addMatchers
toBeInstanceOf: (expected) ->
notText = if @isNot then " not" else ""
this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class"
@actual instanceof expected
toHaveLength: (expected) ->
notText = if @isNot then " not" else ""
this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}"
@actual.length == expected
toExistOnDisk: (expected) ->
notText = this.isNot and " not" or ""
@message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
fs.exists(@actual)
window.keyIdentifierForKey = (key) ->
if key.length > 1 # named key
key

View File

@ -86,7 +86,7 @@ describe "fs", ->
it "calls fn for every path in the tree at the given path", ->
paths = []
onPath = (path) ->
paths.push(fs.join(fixturesDir, path))
paths.push(path)
true
fs.traverseTreeSync fixturesDir, onPath, onPath
expect(paths).toEqual fs.listTree(fixturesDir)
@ -106,14 +106,16 @@ describe "fs", ->
expect(path).not.toMatch /\/dir\//
it "returns entries if path is a symlink", ->
symlinkPath = fs.join(fixturesDir, 'symlink-to-dir')
symlinkPaths = []
onSymlinkPath = (path) -> symlinkPaths.push(path)
onSymlinkPath = (path) -> symlinkPaths.push(path.substring(symlinkPath.length + 1))
regularPath = fs.join(fixturesDir, 'dir')
paths = []
onPath = (path) -> paths.push(path)
onPath = (path) -> paths.push(path.substring(regularPath.length + 1))
fs.traverseTreeSync(fs.join(fixturesDir, 'symlink-to-dir'), onSymlinkPath, onSymlinkPath)
fs.traverseTreeSync(fs.join(fixturesDir, 'dir'), onPath, onPath)
fs.traverseTreeSync(symlinkPath, onSymlinkPath, onSymlinkPath)
fs.traverseTreeSync(regularPath, onPath, onPath)
expect(symlinkPaths).toEqual(paths)

View File

@ -1,4 +1,5 @@
$ = require 'jquery'
_ = require 'underscore'
{View, $$} = require 'space-pen'
describe 'jQuery extensions', ->
@ -41,7 +42,7 @@ describe 'jQuery extensions', ->
element.trigger 'foo'
expect(events).toEqual [2,1,3]
describe "$.fn.events() and $.fn.document", ->
describe "$.fn.events() and $.fn.document(...)", ->
it "returns a list of all events being listened for on the target node or its ancestors, along with their documentation string", ->
view = $$ ->
@div id: 'a', =>
@ -49,20 +50,18 @@ describe 'jQuery extensions', ->
@div id: 'c'
@div id: 'd'
view.document
'a1': "This is event A2"
'b2': "This is event b2"
view.document 'a1', "This is event A2"
view.document 'b2', "This is event b2"
view.document 'a1': "A1: Waste perfectly-good steak"
view.document 'a1', "A1: Waste perfectly-good steak"
view.on 'a1', ->
view.on 'a2', ->
view.on 'b1', -> # should not appear as a duplicate
divB = view.find('#b')
divB.document
'b1': "B1: Super-sonic bomber"
'b2': "B2: Looks evil. Kinda is."
divB.document 'b1', "B1: Super-sonic bomber"
divB.document 'b2', "B2: Looks evil. Kinda is."
divB.on 'b1', ->
divB.on 'b2', ->
@ -76,6 +75,80 @@ describe 'jQuery extensions', ->
'a1': "A1: Waste perfectly-good steak"
'a2': null
describe "$.fn.command(eventName, [selector, options,] handler)", ->
[view, handler] = []
beforeEach ->
view = $$ ->
@div class: 'a', =>
@div class: 'b'
@div class: 'c'
handler = jasmine.createSpy("commandHandler")
it "binds the handler to the given event / selector for all argument combinations", ->
view.command 'test:foo', handler
view.trigger 'test:foo'
expect(handler).toHaveBeenCalled()
handler.reset()
view.command 'test:bar', '.b', handler
view.find('.b').trigger 'test:bar'
view.find('.c').trigger 'test:bar'
expect(handler.callCount).toBe 1
handler.reset()
view.command 'test:baz', doc: 'Spaz', handler
view.trigger 'test:baz'
expect(handler).toHaveBeenCalled()
handler.reset()
view.command 'test:quux', '.c', doc: 'Lorem', handler
view.find('.b').trigger 'test:quux'
view.find('.c').trigger 'test:quux'
expect(handler.callCount).toBe 1
it "passes the 'data' option through when binding the event handler", ->
view.command 'test:foo', data: "bar", handler
view.trigger 'test:foo'
expect(handler.argsForCall[0][0].data).toBe 'bar'
it "sets a custom docstring if the 'doc' option is specified", ->
view.command 'test:foo', doc: "Foo!", handler
expect(view.events()).toEqual 'test:foo': 'Test: Foo!'
it "capitalizes the 'github' prefix how we like it", ->
view.command 'github:spelling', handler
expect(view.events()).toEqual 'github:spelling': 'GitHub: Spelling'
describe "$.fn.scrollUp/Down/ToTop/ToBottom", ->
it "scrolls the element in the specified way if possible", ->
view = $$ -> @div => _.times 20, => @div('A')
view.css(height: 100, width: 100, overflow: 'scroll')
view.attachToDom()
view.scrollUp()
expect(view.scrollTop()).toBe 0
view.scrollDown()
expect(view.scrollTop()).toBeGreaterThan 0
previousScrollTop = view.scrollTop()
view.scrollDown()
expect(view.scrollTop()).toBeGreaterThan previousScrollTop
view.scrollToBottom()
expect(view.scrollTop()).toBe view.prop('scrollHeight') - 100
previousScrollTop = view.scrollTop()
view.scrollDown()
expect(view.scrollTop()).toBe previousScrollTop
view.scrollUp()
expect(view.scrollTop()).toBeLessThan previousScrollTop
previousScrollTop = view.scrollTop()
view.scrollUp()
expect(view.scrollTop()).toBeLessThan previousScrollTop
view.scrollToTop()
expect(view.scrollTop()).toBe 0
describe "Event.prototype", ->
class GrandchildView extends View
@content: -> @div class: 'grandchild'

View File

@ -7,70 +7,22 @@ CSON = require 'cson'
module.exports =
class AtomPackage extends Package
metadata: null
packageMain: null
mainModule: null
deferActivation: false
load: ({activateImmediately}={}) ->
load: ->
try
@loadMetadata()
@loadKeymaps()
@loadStylesheets()
if @metadata.activationEvents and not activateImmediately
@subscribeToActivationEvents()
if @deferActivation = @metadata.activationEvents?
@registerDeferredDeserializers()
else
@activatePackageMain()
@requireMainModule()
catch e
console.warn "Failed to load package named '#{@name}'", e.stack
this
disableEventHandlersOnBubblePath: (event) ->
bubblePathEventHandlers = []
disabledHandler = ->
element = $(event.target)
while element.length
if eventHandlers = element.data('events')?[event.type]
for eventHandler in eventHandlers
eventHandler.disabledHandler = eventHandler.handler
eventHandler.handler = disabledHandler
bubblePathEventHandlers.push(eventHandler)
element = element.parent()
bubblePathEventHandlers
restoreEventHandlersOnBubblePath: (eventHandlers) ->
for eventHandler in eventHandlers
eventHandler.handler = eventHandler.disabledHandler
delete eventHandler.disabledHandler
unsubscribeFromActivationEvents: (activateHandler) ->
if _.isArray(@metadata.activationEvents)
rootView.off(event, activateHandler) for event in @metadata.activationEvents
else
rootView.off(event, selector, activateHandler) for event, selector of @metadata.activationEvents
subscribeToActivationEvents: () ->
activateHandler = (event) =>
bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event)
@activatePackageMain()
$(event.target).trigger(event)
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
@unsubscribeFromActivationEvents(activateHandler)
if _.isArray(@metadata.activationEvents)
rootView.command(event, activateHandler) for event in @metadata.activationEvents
else
rootView.command(event, selector, activateHandler) for event, selector of @metadata.activationEvents
activatePackageMain: ->
mainPath = @path
mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
try
mainPath = require.resolve(mainPath)
catch e
return
if fs.isFile(mainPath)
@packageMain = require(mainPath)
config.setDefaults(@name, @packageMain.configDefaults)
atom.activateAtomPackage(this)
loadMetadata: ->
if metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json'])
@metadata = CSON.readObject(metadataPath)
@ -90,3 +42,68 @@ class AtomPackage extends Package
stylesheetDirPath = fs.join(@path, 'stylesheets')
for stylesheetPath in fs.list(stylesheetDirPath)
requireStylesheet(stylesheetPath)
activate: ->
if @deferActivation
@subscribeToActivationEvents()
else
try
if @requireMainModule()
config.setDefaults(@name, @mainModule.configDefaults)
atom.activateAtomPackage(this)
catch e
console.warn "Failed to activate package named '#{@name}'", e.stack
requireMainModule: ->
return @mainModule if @mainModule
mainPath =
if @metadata.main
fs.join(@path, @metadata.main)
else
fs.join(@path, 'index')
mainPath = fs.resolveExtension(mainPath, ["", _.keys(require.extensions)...])
@mainModule = require(mainPath) if fs.isFile(mainPath)
registerDeferredDeserializers: ->
for deserializerName in @metadata.deferredDeserializers ? []
registerDeferredDeserializer deserializerName, => @requireMainModule()
subscribeToActivationEvents: () ->
return unless @metadata.activationEvents?
activateHandler = (event) =>
bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event)
@deferActivation = false
@activate()
$(event.target).trigger(event)
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
@unsubscribeFromActivationEvents(activateHandler)
if _.isArray(@metadata.activationEvents)
rootView.command(event, activateHandler) for event in @metadata.activationEvents
else
rootView.command(event, selector, activateHandler) for event, selector of @metadata.activationEvents
unsubscribeFromActivationEvents: (activateHandler) ->
if _.isArray(@metadata.activationEvents)
rootView.off(event, activateHandler) for event in @metadata.activationEvents
else
rootView.off(event, selector, activateHandler) for event, selector of @metadata.activationEvents
disableEventHandlersOnBubblePath: (event) ->
bubblePathEventHandlers = []
disabledHandler = ->
element = $(event.target)
while element.length
if eventHandlers = element.data('events')?[event.type]
for eventHandler in eventHandlers
eventHandler.disabledHandler = eventHandler.handler
eventHandler.handler = disabledHandler
bubblePathEventHandlers.push(eventHandler)
element = element.parent()
bubblePathEventHandlers
restoreEventHandlersOnBubblePath: (eventHandlers) ->
for eventHandler in eventHandlers
eventHandler.handler = eventHandler.disabledHandler
delete eventHandler.disabledHandler

View File

@ -6,10 +6,10 @@ module.exports =
class AtomTheme extends Theme
loadStylesheet: (stylesheetPath)->
@stylesheets[stylesheetPath] = fs.read(stylesheetPath)
@stylesheets[stylesheetPath] = window.loadStylesheet(stylesheetPath)
load: ->
if fs.extension(@path) is '.css'
if fs.extension(@path) in ['.css', '.less']
@loadStylesheet(@path)
else
metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json'])
@ -18,6 +18,6 @@ class AtomTheme extends Theme
if stylesheetNames
@loadStylesheet(fs.join(@path, name)) for name in stylesheetNames
else
@loadStylesheet(stylesheetPath) for stylesheetPath in fs.list(@path, ['.css'])
@loadStylesheet(stylesheetPath) for stylesheetPath in fs.list(@path, ['.css', '.less'])
super

View File

@ -24,10 +24,10 @@ _.extend atom,
activateAtomPackage: (pack) ->
@activatedAtomPackages.push(pack)
pack.packageMain.activate(@atomPackageStates[pack.name] ? {})
pack.mainModule.activate(@atomPackageStates[pack.name] ? {})
deactivateAtomPackages: ->
pack.packageMain.deactivate?() for pack in @activatedAtomPackages
pack.mainModule.deactivate?() for pack in @activatedAtomPackages
@activatedAtomPackages = []
serializeAtomPackages: ->
@ -35,7 +35,7 @@ _.extend atom,
for pack in @loadedPackages
if pack in @activatedAtomPackages
try
packageStates[pack.name] = pack.packageMain.serialize?()
packageStates[pack.name] = pack.mainModule.serialize?()
catch e
console?.error("Exception serializing '#{pack.name}' package's module\n", e.stack)
else
@ -61,6 +61,9 @@ _.extend atom,
new LoadTextMatePackagesTask(textMatePackages).start() if textMatePackages.length > 0
activatePackages: ->
pack.activate() for pack in @loadedPackages
getLoadedPackages: ->
_.clone(@loadedPackages)

View File

@ -19,6 +19,7 @@ class Config
userPackagesDirPath: userPackagesDirPath
defaultSettings: null
settings: null
configFileHasErrors: null
constructor: ->
@defaultSettings =
@ -34,18 +35,17 @@ class Config
fs.makeDirectory(@configDirPath)
templateConfigDirPath = fs.resolve(window.resourcePath, 'dot-atom')
onConfigDirFile = (path) =>
templatePath = fs.join(templateConfigDirPath, path)
configPath = fs.join(@configDirPath, path)
fs.write(configPath, fs.read(templatePath))
relativePath = path.substring(templateConfigDirPath.length + 1)
configPath = fs.join(@configDirPath, relativePath)
fs.write(configPath, fs.read(path))
fs.traverseTreeSync(templateConfigDirPath, onConfigDirFile, (path) -> true)
configThemeDirPath = fs.join(@configDirPath, 'themes')
onThemeDirFile = (path) ->
templatePath = fs.join(bundledThemesDirPath, path)
configPath = fs.join(configThemeDirPath, path)
fs.write(configPath, fs.read(templatePath))
relativePath = path.substring(bundledThemesDirPath.length + 1)
configPath = fs.join(configThemeDirPath, relativePath)
fs.write(configPath, fs.read(path))
fs.traverseTreeSync(bundledThemesDirPath, onThemeDirFile, (path) -> true)
load: ->
@ -54,8 +54,13 @@ class Config
loadUserConfig: ->
if fs.exists(@configFilePath)
userConfig = CSON.readObject(@configFilePath)
_.extend(@settings, userConfig)
try
userConfig = CSON.readObject(@configFilePath)
_.extend(@settings, userConfig)
catch e
@configFileHasErrors = true
console.error "Failed to load user config '#{@configFilePath}'", e.message
console.error e.stack
get: (keyPath) ->
_.valueForKeyPath(@settings, keyPath) ?
@ -91,6 +96,7 @@ class Config
subscription
update: ->
return if @configFileHasErrors
@save()
@trigger 'updated'

View File

@ -61,7 +61,7 @@ class Editor extends View
else
{editSession, @mini} = (editSessionOrOptions ? {})
requireStylesheet 'editor.css'
requireStylesheet 'editor.less'
@id = Editor.nextEditorId++
@lineCache = []

View File

@ -9,6 +9,7 @@ class PaneContainer extends View
@deserialize: ({root}) ->
container = new PaneContainer
container.append(deserialize(root)) if root
container.removeEmptyPanes()
container
@content: ->
@ -61,6 +62,23 @@ class PaneContainer extends View
saveAll: ->
pane.saveItems() for pane in @getPanes()
confirmClose: ->
deferred = $.Deferred()
modifiedItems = []
for pane in @getPanes()
modifiedItems.push(item) for item in pane.getItems() when item.isModified?()
cancel = => deferred.reject()
saveNextModifiedItem = =>
if modifiedItems.length == 0
deferred.resolve()
else
item = modifiedItems.pop()
@paneAtIndex(0).promptToSaveItem item, saveNextModifiedItem, cancel
saveNextModifiedItem()
deferred.promise()
getPanes: ->
@find('.pane').views()
@ -93,5 +111,9 @@ class PaneContainer extends View
root.css(width: '100%', height: '100%', top: 0, left: 0)
root.adjustDimensions()
removeEmptyPanes: ->
for pane in @getPanes() when pane.getItems().length == 0
pane.remove()
afterAttach: ->
@adjustPaneDimensions()

View File

@ -11,7 +11,8 @@ class Pane extends View
@div class: 'item-views', outlet: 'itemViews'
@deserialize: ({items, focused, activeItemUri}) ->
pane = new Pane(items.map((item) -> deserialize(item))...)
deserializedItems = _.compact(items.map((item) -> deserialize(item)))
pane = new Pane(deserializedItems...)
pane.showItemForUri(activeItemUri) if activeItemUri
pane.focusOnAttach = true if focused
pane
@ -21,7 +22,7 @@ class Pane extends View
initialize: (@items...) ->
@viewsByClassName = {}
@showItem(@items[0])
@showItem(@items[0]) if @items.length > 0
@command 'core:close', @destroyActiveItem
@command 'core:save', @saveActiveItem
@ -46,7 +47,7 @@ class Pane extends View
@command 'pane:split-down', => @splitDown()
@command 'pane:close', => @destroyItems()
@command 'pane:close-other-items', => @destroyInactiveItems()
@on 'focus', => @activeView.focus(); false
@on 'focus', => @activeView?.focus(); false
@on 'focusin', => @makeActive()
@on 'focusout', => @autosaveActiveItem()
@ -72,6 +73,12 @@ class Pane extends View
isActive: ->
@hasClass('active')
getNextPane: ->
panes = @getContainer()?.getPanes()
return unless panes.length > 1
nextIndex = (panes.indexOf(this) + 1) % panes.length
panes[nextIndex]
getItems: ->
new Array(@items...)
@ -152,13 +159,13 @@ class Pane extends View
destroyInactiveItems: ->
@destroyItem(item) for item in @getItems() when item isnt @activeItem
promptToSaveItem: (item, nextAction) ->
promptToSaveItem: (item, nextAction, cancelAction) ->
uri = item.getUri()
atom.confirm(
"'#{item.getTitle()}' has changes, do you want to save them?"
"'#{item.getTitle?() ? item.getUri()}' has changes, do you want to save them?"
"Your changes will be lost if close this item without saving."
"Save", => @saveItem(item, nextAction)
"Cancel", null
"Cancel", cancelAction
"Don't Save", nextAction
)

View File

@ -75,6 +75,9 @@ class RootView extends View
panes: @panes.serialize()
packages: atom.serializeAtomPackages()
confirmClose: ->
@panes.confirmClose()
handleFocus: (e) ->
if @getActivePane()
@getActivePane().focus()
@ -115,7 +118,7 @@ class RootView extends View
updateTitle: ->
if projectPath = project.getPath()
if item = @getActivePaneItem()
@setTitle("#{item.getTitle()} - #{projectPath}")
@setTitle("#{item.getTitle?() ? 'untitled'} - #{projectPath}")
else
@setTitle(projectPath)
else

View File

@ -20,7 +20,7 @@ class SelectList extends View
cancelling: false
initialize: ->
requireStylesheet 'select-list.css'
requireStylesheet 'select-list.less'
@miniEditor.getBuffer().on 'changed', => @schedulePopulateList()
@miniEditor.on 'focusout', => @cancel() unless @cancelling

View File

@ -32,6 +32,8 @@ class TextMatePackage extends Package
console.warn "Failed to load package at '#{@path}'", e.stack
this
activate: -> # no-op
getGrammars: -> @grammars
readGrammars: ->

View File

@ -11,7 +11,7 @@ class Theme
if fs.exists(name)
path = name
else
path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme', '.css'])
path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme', '.css', 'less'])
throw new Error("No theme exists named '#{name}'") unless path

View File

@ -1,11 +1,13 @@
fs = require 'fs-utils'
$ = require 'jquery'
ChildProcess = require 'child_process'
{less} = require 'less'
{spawn} = require 'child_process'
require 'jquery-extensions'
require 'underscore-extensions'
require 'space-pen-extensions'
deserializers = {}
deferredDeserializers = {}
# This method is called in any window needing a general environment, including specs
window.setUpEnvironment = ->
@ -23,17 +25,17 @@ window.setUpEnvironment = ->
$(document).on 'keydown', keymap.handleKeyEvent
keymap.bindDefaultKeys()
requireStylesheet 'reset.css'
requireStylesheet 'atom.css'
requireStylesheet 'tabs.css'
requireStylesheet 'tree-view.css'
requireStylesheet 'status-bar.css'
requireStylesheet 'command-panel.css'
requireStylesheet 'fuzzy-finder.css'
requireStylesheet 'overlay.css'
requireStylesheet 'popover-list.css'
requireStylesheet 'notification.css'
requireStylesheet 'markdown.css'
requireStylesheet 'reset.less'
requireStylesheet 'atom.less'
requireStylesheet 'tabs.less'
requireStylesheet 'tree-view.less'
requireStylesheet 'status-bar.less'
requireStylesheet 'command-panel.less'
requireStylesheet 'fuzzy-finder.less'
requireStylesheet 'overlay.less'
requireStylesheet 'popover-list.less'
requireStylesheet 'notification.less'
requireStylesheet 'markdown.less'
if nativeStylesheetPath = require.resolve("#{platform}.css")
requireStylesheet(nativeStylesheetPath)
@ -52,10 +54,11 @@ window.startup = ->
handleWindowEvents()
config.load()
atom.loadTextPackage()
buildProjectAndRootView()
keymap.loadBundledKeymaps()
atom.loadThemes()
atom.loadPackages()
buildProjectAndRootView()
atom.activatePackages()
keymap.loadUserKeymaps()
atom.requireUserInitScript()
$(window).on 'beforeunload', -> shutdown(); false
@ -81,14 +84,13 @@ window.installAtomCommand = (commandPath) ->
bundledCommandPath = fs.resolve(window.resourcePath, 'atom.sh')
if bundledCommandPath?
fs.write(commandPath, fs.read(bundledCommandPath))
ChildProcess.exec("chmod u+x '#{commandPath}'")
spawn("chmod u+x '#{commandPath}'")
window.handleWindowEvents = ->
$(window).on 'core:close', => window.close()
$(window).command 'window:close', => window.close()
$(window).command 'window:toggle-full-screen', => atom.toggleFullScreen()
$(window).on 'focus', -> $("body").removeClass('is-blurred')
$(window).on 'blur', -> $("body").addClass('is-blurred')
$(window).command 'window:close', => confirmClose()
window.buildProjectAndRootView = ->
RootView = require 'root-view'
@ -115,10 +117,21 @@ window.stylesheetElementForId = (id) ->
window.requireStylesheet = (path) ->
if fullPath = require.resolve(path)
window.applyStylesheet(fullPath, fs.read(fullPath))
unless fullPath
content = window.loadStylesheet(fullPath)
window.applyStylesheet(fullPath, content)
else
console.log "bad", path
throw new Error("Could not find a file at path '#{path}'")
window.loadStylesheet = (path) ->
content = fs.read(path)
if fs.extension(path) == '.less'
(new less.Parser).parse content, (e, tree) ->
throw new Error(e.message, file, e.line) if e
content = tree.toCSS()
content
window.removeStylesheet = (path) ->
unless fullPath = require.resolve(path)
throw new Error("Could not find a file at path '#{path}'")
@ -151,6 +164,9 @@ window.registerDeserializers = (args...) ->
window.registerDeserializer = (klass) ->
deserializers[klass.name] = klass
window.registerDeferredDeserializer = (name, fn) ->
deferredDeserializers[name] = fn
window.unregisterDeserializer = (klass) ->
delete deserializers[klass.name]
@ -160,7 +176,11 @@ window.deserialize = (state) ->
deserializer.deserialize(state)
window.getDeserializer = (state) ->
deserializers[state?.deserializer]
name = state?.deserializer
if deferredDeserializers[name]
deferredDeserializers[name]()
delete deferredDeserializers[name]
deserializers[name]
window.measure = (description, fn) ->
start = new Date().getTime()
@ -175,3 +195,6 @@ window.profile = (description, fn) ->
value = fn()
console.profileEnd(description)
value
confirmClose = ->
rootView.confirmClose().done -> window.close()

View File

@ -7,7 +7,7 @@ describe "CommandLogger", ->
beforeEach ->
window.rootView = new RootView
rootView.open('sample.js')
commandLogger = window.loadPackage('command-logger').packageMain
commandLogger = window.loadPackage('command-logger').mainModule
commandLogger.eventLog = {}
editor = rootView.getActiveView()

View File

@ -115,7 +115,7 @@ class CommandPanelView extends View
escapedCommand: ->
@miniEditor.getText()
execute: (command=@escapedCommand())->
execute: (command=@escapedCommand()) ->
@loadingMessage.show()
@errorMessages.empty()

View File

@ -16,7 +16,7 @@ class SelectAllMatchesInProject extends Command
promise = project.scan @regex, ({path, range}) ->
operations.push(new Operation(
project: project
buffer: project.bufferForPath(path)
path: path
bufferRange: range
))

View File

@ -4,7 +4,7 @@ module.exports =
class OperationView extends View
@content: ({operation} = {}) ->
{prefix, suffix, match, range} = operation.preview()
@li 'data-index': operation.index, class: 'operation', =>
@li class: 'operation', =>
@span range.start.row + 1, class: 'line-number'
@span class: 'preview', =>
@span prefix

View File

@ -1,22 +1,30 @@
module.exports =
class Operation
constructor: ({@project, @buffer, bufferRange, @newText, @preserveSelection, @errorMessage}) ->
@buffer.retain()
@marker = @buffer.markRange(bufferRange)
constructor: ({@project, @path, @buffer, @bufferRange, @newText, @preserveSelection, @errorMessage}) ->
if @buffer?
@buffer.retain()
@getMarker()
getMarker: ->
@marker ?= @getBuffer().markRange(@bufferRange)
getBuffer: ->
@buffer ?= @project.bufferForPath(@path).retain()
getPath: ->
@project.relativize(@buffer.getPath())
path = @path ? @getBuffer().getPath()
@project.relativize(path)
getBufferRange: ->
@buffer.getMarkerRange(@marker)
@getBuffer().getMarkerRange(@getMarker())
execute: (editSession) ->
@buffer.change(@getBufferRange(), @newText) if @newText
@getBuffer().change(@getBufferRange(), @newText) if @newText
@getBufferRange() unless @preserveSelection
preview: ->
range = @buffer.getMarkerRange(@marker)
line = @buffer.lineForRow(range.start.row)
range = @getBuffer().getMarkerRange(@getMarker())
line = @getBuffer().lineForRow(range.start.row)
prefix = line[0...range.start.column]
match = line[range.start.column...range.end.column]
suffix = line[range.end.column..]
@ -24,5 +32,5 @@ class Operation
{prefix, suffix, match, range}
destroy: ->
@buffer.destroyMarker(@marker)
@buffer.release()
@buffer?.destroyMarker(@marker) if @marker?
@buffer?.release()

View File

@ -5,16 +5,14 @@ $ = require 'jquery'
module.exports =
class PathView extends View
@content: ({path, operations, previewList} = {}) ->
@content: ({path, 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'
@span outlet: 'description', 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)
@ -27,6 +25,10 @@ class PathView extends View
@toggle(true)
false
addOperation: (operation) ->
@matches.append new OperationView({operation, @previewList})
@description.text("(#{@matches.find('li').length})")
isSelected: ->
@hasClass('selected') or @find('.selected').length

View File

@ -11,12 +11,17 @@ class PreviewList extends ScrollView
@ol class: 'preview-list', tabindex: -1
operations: null
viewsForPath: null
pixelOverdraw: 100
lastRenderedOperationIndex: null
initialize: ->
super
@on 'core:move-down', => @selectNextOperation(); false
@on 'core:move-up', => @selectPreviousOperation(); false
@on 'scroll', =>
@renderOperations() if @scrollBottom() >= (@prop('scrollHeight'))
@command 'command-panel:collapse-all', => @collapseAllPaths()
@command 'command-panel:expand-all', => @expandAllPaths()
@ -25,6 +30,7 @@ class PreviewList extends ScrollView
@children().each (index, element) -> $(element).view().expand()
collapseAllPaths: ->
@renderOperations(renderAll: true)
@children().each (index, element) -> $(element).view().collapse()
destroy: ->
@ -35,23 +41,31 @@ class PreviewList extends ScrollView
populate: (operations) ->
@destroyOperations() if @operations
@operations = operations
@lastRenderedOperationIndex = 0
@empty()
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})
@viewsForPath = {}
@show()
@find('.operation:first').addClass('selected')
@setLineNumberWidth()
@renderOperations()
setLineNumberWidth: ->
lineNumbers = @find('.line-number')
maxWidth = 0
lineNumbers.each (index, element) ->
maxWidth = Math.max($(element).outerWidth(), maxWidth)
lineNumbers.width(maxWidth)
@find('.operation:first').addClass('selected')
renderOperations: ({renderAll}={}) ->
renderAll ?= false
startingScrollHeight = @prop('scrollHeight')
for operation in @operations[@lastRenderedOperationIndex..]
pathView = @pathViewForPath(operation.getPath())
pathView.addOperation(operation)
@lastRenderedOperationIndex++
break if not renderAll and @prop('scrollHeight') >= startingScrollHeight + @pixelOverdraw and @prop('scrollHeight') > @height() + @pixelOverdraw
pathViewForPath: (path) ->
pathView = @viewsForPath[path]
if not pathView
pathView = new PathView({path: path, previewList: this})
@viewsForPath[path] = pathView
@append(pathView)
pathView
selectNextOperation: ->
selectedView = @find('.selected').view()

View File

@ -11,7 +11,7 @@ describe "CommandPanel", ->
rootView.enableKeymap()
editSession = rootView.getActivePaneItem()
buffer = editSession.buffer
commandPanelMain = window.loadPackage('command-panel', activateImmediately: true).packageMain
commandPanelMain = window.loadPackage('command-panel', activateImmediately: true).mainModule
commandPanel = commandPanelMain.commandPanelView
commandPanel.history = []
commandPanel.historyIndex = 0

View File

@ -0,0 +1,45 @@
RootView = require 'root-view'
CommandPanelView = require 'command-panel/lib/command-panel-view'
_ = require 'underscore'
describe "Preview List", ->
[previewList, commandPanelMain, commandPanelView] = []
beforeEach ->
window.rootView = new RootView()
rootView.attachToDom()
commandPanelMain = window.loadPackage('command-panel', activateImmediately: true).mainModule
commandPanelView = commandPanelMain.commandPanelView
previewList = commandPanelView.previewList
rootView.trigger 'command-panel:toggle'
describe "when the list is scrollable", ->
it "adds more operations to the DOM when `scrollBottom` nears the `pixelOverdraw`", ->
waitsForPromise ->
commandPanelView.execute('X x/so/')
runs ->
expect(previewList.prop('scrollHeight')).toBeGreaterThan previewList.height()
previousScrollHeight = previewList.prop('scrollHeight')
previousOperationCount = previewList.find("li").length
previewList.scrollTop(previewList.pixelOverdraw / 2)
previewList.trigger('scroll') # Not sure why scroll event isn't being triggered on it's own
expect(previewList.prop('scrollHeight')).toBe previousScrollHeight
expect(previewList.find("li").length).toBe previousOperationCount
previewList.scrollToBottom()
previewList.trigger('scroll') # Not sure why scroll event isn't being triggered on it's own
expect(previewList.prop('scrollHeight')).toBeGreaterThan previousScrollHeight
expect(previewList.find("li").length).toBeGreaterThan previousOperationCount
it "renders all operations if the preview items are collapsed", ->
waitsForPromise ->
commandPanelView.execute('X x/so/')
runs ->
expect(previewList.prop('scrollHeight')).toBeGreaterThan previewList.height()
previousScrollHeight = previewList.prop('scrollHeight')
previousOperationCount = previewList.find("li").length
previewList.collapseAllPaths()
expect(previewList.find("li").length).toBeGreaterThan previousOperationCount

View File

@ -17,7 +17,7 @@ describe "EditorStats", ->
beforeEach ->
window.rootView = new RootView
rootView.open('sample.js')
editorStats = window.loadPackage('editor-stats').packageMain.stats
editorStats = window.loadPackage('editor-stats').mainModule.stats
describe "when a keyup event is triggered", ->
beforeEach ->

View File

@ -1,45 +0,0 @@
.editor-stats-wrapper {
padding: 5px;
box-sizing: border-box;
border-top: 1px solid rgba(255, 255, 255, 0.05);
z-index: 9999;
}
.editor-stats {
height: 50px;
width: 100%;
background: #1d1f21;
border: 1px solid rgba(0, 0, 0, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.editor-stats rect.bar {
fill: rgba(255, 255, 255, 0.2);
shape-rendering: crispedges;
}
.editor-stats rect.bar.max {
fill: rgba(0, 163, 255, 1);
}
.editor-stats text {
font-size: 10px;
fill: rgba(255, 255, 255, 0.2);
font-family: Courier;
}
.editor-stats .minor text {
display: none;
}
.editor-stats line {
stroke: #ccc;
stroke-opacity: 0.05;
stroke-width: 1px;
shape-rendering: crispedges;
}
.editor-stats path.domain {
fill: none;
}

View File

@ -0,0 +1,45 @@
.editor-stats-wrapper {
padding: 5px;
box-sizing: border-box;
border-top: 1px solid rgba(255, 255, 255, 0.05);
z-index: 9999;
}
.editor-stats {
height: 50px;
width: 100%;
background: #1d1f21;
border: 1px solid rgba(0, 0, 0, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-right: 1px solid rgba(255, 255, 255, 0.1);
.bar {
fill: rgba(255, 255, 255, 0.2);
shape-rendering: crispedges;
&.max {
fill: rgba(0, 163, 255, 1);
}
}
text {
font-size: 10px;
fill: rgba(255, 255, 255, 0.2);
font-family: Courier;
}
.minor text {
display: none;
}
line {
stroke: #ccc;
stroke-opacity: 0.05;
stroke-width: 1px;
shape-rendering: crispedges;
}
path.domain {
display: none;
}
}

View File

@ -35,9 +35,10 @@ module.exports =
createView: ->
unless @fuzzyFinderView
@loadPathsTask?.abort()
FuzzyFinderView = require 'fuzzy-finder/lib/fuzzy-finder-view'
@fuzzyFinderView = new FuzzyFinderView()
if @projectPaths? and not @fuzzyFinderView.projectPaths?
if @projectPaths?.length > 0 and not @fuzzyFinderView.projectPaths?
@fuzzyFinderView.projectPaths = @projectPaths
@fuzzyFinderView.reloadProjectPaths = false
@fuzzyFinderView

View File

@ -13,7 +13,7 @@ describe 'FuzzyFinder', ->
window.rootView = new RootView
rootView.open('sample.js')
rootView.enableKeymap()
finderView = window.loadPackage("fuzzy-finder").packageMain.createView()
finderView = window.loadPackage("fuzzy-finder").mainModule.createView()
describe "file-finder behavior", ->
describe "toggling", ->

View File

@ -1,5 +1,2 @@
'.editor':
'ctrl-m': 'markdown-preview:toggle'
'.markdown-preview':
'ctrl-m': 'markdown-preview:toggle'
'ctrl-m': 'markdown-preview:show'

View File

@ -1,49 +1,36 @@
ScrollView = require 'scroll-view'
fs = require 'fs-utils'
$ = require 'jquery'
ScrollView = require 'scroll-view'
{$$$} = require 'space-pen'
module.exports =
class MarkdownPreviewView extends ScrollView
@activate: ->
@instance = new MarkdownPreviewView
registerDeserializer(this)
@deserialize: ({path}) ->
new MarkdownPreviewView(project.bufferForPath(path))
@content: ->
@div class: 'markdown-preview', tabindex: -1, =>
@div class: 'markdown-body', outlet: 'markdownBody'
@div class: 'markdown-preview', tabindex: -1
initialize: ->
initialize: (@buffer) ->
super
@fetchRenderedMarkdown()
@on 'core:move-up', => @scrollUp()
@on 'core:move-down', => @scrollDown()
rootView.command 'markdown-preview:toggle', => @toggle()
@on 'blur', => @detach() unless document.activeElement is this[0]
@command 'core:cancel', => @detach()
serialize: ->
deserializer: 'MarkdownPreviewView'
path: @buffer.getPath()
toggle: ->
if @hasParent()
@detach()
else
@attach()
getTitle: ->
"Markdown Preview #{@buffer.getBaseName()}"
attach: ->
return unless @isMarkdownEditor()
rootView.append(this)
@markdownBody.html(@getLoadingHtml())
@loadHtml()
@focus()
getUri: ->
"markdown-preview:#{@buffer.getPath()}"
detach: ->
return if @detaching
@detaching = true
super
rootView.focus()
@detaching = false
getActiveText: ->
rootView.getActiveView()?.getText()
getErrorHtml: (error) ->
$$$ ->
setErrorHtml: ->
@html $$$ ->
@h2 'Previewing Markdown Failed'
@h3 'Possible Reasons'
@ul =>
@ -52,29 +39,18 @@ class MarkdownPreviewView extends ScrollView
@a 'github.com', href: 'https://github.com'
@span '.'
getLoadingHtml: ->
$$$ ->
@div class: 'markdown-spinner', 'Loading Markdown...'
setLoading: ->
@html($$$ -> @div class: 'markdown-spinner', 'Loading Markdown...')
loadHtml: (text) ->
payload =
mode: 'markdown'
text: @getActiveText()
request =
fetchRenderedMarkdown: (text) ->
@setLoading()
$.ajax
url: 'https://api.github.com/markdown'
type: 'POST'
dataType: 'html'
contentType: 'application/json; charset=UTF-8'
data: JSON.stringify(payload)
success: (html) => @setHtml(html)
error: (jqXhr, error) => @setHtml(@getErrorHtml(error))
$.ajax(request)
setHtml: (html) ->
@markdownBody.html(html) if @hasParent()
isMarkdownEditor: (path) ->
editor = rootView.getActiveView()
return unless editor?
return true if editor.getGrammar().scopeName is 'source.gfm'
path and fs.isMarkdownExtension(fs.extension(path))
data: JSON.stringify
mode: 'markdown'
text: @buffer.getText()
success: (html) => @html(html)
error: => @setErrorHtml()

View File

@ -0,0 +1,25 @@
EditSession = require 'edit-session'
MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view'
module.exports =
activate: ->
rootView.command 'markdown-preview:show', '.editor', => @show()
show: ->
activePane = rootView.getActivePane()
item = activePane.activeItem
if not item instanceof EditSession
console.warn("Can not render markdown for #{item.getUri()}")
return
editSession = item
if nextPane = activePane.getNextPane()
if preview = nextPane.itemForUri("markdown-preview:#{editSession.getPath()}")
nextPane.showItem(preview)
preview.fetchRenderedMarkdown()
else
nextPane.showItem(new MarkdownPreviewView(editSession.buffer))
else
activePane.splitRight(new MarkdownPreviewView(editSession.buffer))
activePane.focus()

View File

@ -1,3 +1,4 @@
'main': 'lib/markdown-preview-view'
'main': 'lib/markdown-preview'
'activationEvents':
'markdown-preview:toggle': '.editor'
'markdown-preview:show': '.editor'
'deferredDeserializers': ['MarkdownPreviewView']

View File

@ -1,84 +1,67 @@
$ = require 'jquery'
RootView = require 'root-view'
MarkdownPreview = require 'markdown-preview/lib/markdown-preview-view'
_ = require 'underscore'
MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view'
{$$} = require 'space-pen'
describe "MarkdownPreview", ->
describe "MarkdownPreview package", ->
beforeEach ->
project.setPath(project.resolve('markdown'))
window.rootView = new RootView
window.loadPackage("markdown-preview")
spyOn(MarkdownPreview.prototype, 'loadHtml')
window.loadPackage("markdown-preview", activateImmediately: true)
spyOn(MarkdownPreviewView.prototype, 'fetchRenderedMarkdown')
describe "markdown-preview:toggle event", ->
it "toggles on/off a preview for a .md file", ->
rootView.open('file.md')
editor = rootView.getActiveView()
expect(rootView.find('.markdown-preview')).not.toExist()
editor.trigger('markdown-preview:toggle')
describe "markdown-preview:show", ->
beforeEach ->
rootView.open("file.markdown")
markdownPreviewView = rootView.find('.markdown-preview')?.view()
expect(rootView.find('.markdown-preview')).toExist()
expect(markdownPreviewView.loadHtml).toHaveBeenCalled()
markdownPreviewView.trigger('markdown-preview:toggle')
expect(rootView.find('.markdown-preview')).not.toExist()
describe "when the active item is an edit session", ->
beforeEach ->
rootView.attachToDom()
it "displays a preview for a .markdown file", ->
rootView.open('file.markdown')
editor = rootView.getActiveView()
expect(rootView.find('.markdown-preview')).not.toExist()
editor.trigger('markdown-preview:toggle')
expect(rootView.find('.markdown-preview')).toExist()
markdownPreviewView = rootView.find('.markdown-preview')?.view()
expect(markdownPreviewView.loadHtml).toHaveBeenCalled()
describe "when a preview item has not been created for the edit session's uri", ->
describe "when there is more than one pane", ->
it "shows a markdown preview for the current buffer on the next pane", ->
rootView.getActivePane().splitRight()
[pane1, pane2] = rootView.getPanes()
pane1.focus()
it "displays a preview for a file with the source.gfm grammar scope", ->
gfmGrammar = _.find syntax.grammars, (grammar) -> grammar.scopeName is 'source.gfm'
rootView.open('file.js')
editor = rootView.getActiveView()
project.addGrammarOverrideForPath(editor.getPath(), gfmGrammar)
editor.reloadGrammar()
expect(rootView.find('.markdown-preview')).not.toExist()
editor.trigger('markdown-preview:toggle')
expect(rootView.find('.markdown-preview')).toExist()
markdownPreviewView = rootView.find('.markdown-preview')?.view()
expect(markdownPreviewView.loadHtml).toHaveBeenCalled()
rootView.getActiveView().trigger 'markdown-preview:show'
it "does not display a preview for non-markdown file", ->
rootView.open('file.js')
editor = rootView.getActiveView()
expect(rootView.find('.markdown-preview')).not.toExist()
editor.trigger('markdown-preview:toggle')
expect(rootView.find('.markdown-preview')).not.toExist()
expect(MarkdownPreview.prototype.loadHtml).not.toHaveBeenCalled()
preview = pane2.activeItem
expect(preview).toBeInstanceOf(MarkdownPreviewView)
expect(preview.buffer).toBe rootView.getActivePaneItem().buffer
expect(pane1).toMatchSelector(':has(:focus)')
describe "core:cancel event", ->
it "removes markdown preview", ->
rootView.open('file.md')
editor = rootView.getActiveView()
expect(rootView.find('.markdown-preview')).not.toExist()
editor.trigger('markdown-preview:toggle')
describe "when there is only one pane", ->
it "splits the current pane to the right with a markdown preview for the current buffer", ->
expect(rootView.getPanes()).toHaveLength 1
markdownPreviewView = rootView.find('.markdown-preview')?.view()
expect(markdownPreviewView).toExist()
markdownPreviewView.trigger('core:cancel')
expect(rootView.find('.markdown-preview')).not.toExist()
rootView.getActiveView().trigger 'markdown-preview:show'
describe "when the editor receives focus", ->
it "removes the markdown preview view", ->
rootView.attachToDom()
rootView.open('file.md')
editor = rootView.getActiveView()
expect(rootView.find('.markdown-preview')).not.toExist()
editor.trigger('markdown-preview:toggle')
expect(rootView.getPanes()).toHaveLength 2
[pane1, pane2] = rootView.getPanes()
markdownPreviewView = rootView.find('.markdown-preview')
editor.focus()
expect(markdownPreviewView).toExist()
expect(rootView.find('.markdown-preview')).not.toExist()
expect(pane2.items).toHaveLength 1
preview = pane2.activeItem
expect(preview).toBeInstanceOf(MarkdownPreviewView)
expect(preview.buffer).toBe rootView.getActivePaneItem().buffer
expect(pane1).toMatchSelector(':has(:focus)')
describe "when no editor is open", ->
it "does not attach", ->
expect(rootView.getActiveView()).toBeFalsy()
rootView.trigger('markdown-preview:toggle')
expect(rootView.find('.markdown-preview')).not.toExist()
describe "when a preview item has already been created for the edit session's uri", ->
it "updates and shows the existing preview item if it isn't displayed", ->
rootView.getActiveView().trigger 'markdown-preview:show'
[pane1, pane2] = rootView.getPanes()
pane2.focus()
expect(rootView.getActivePane()).toBe pane2
preview = pane2.activeItem
expect(preview).toBeInstanceOf(MarkdownPreviewView)
rootView.open()
expect(pane2.activeItem).not.toBe preview
pane1.focus()
preview.fetchRenderedMarkdown.reset()
rootView.getActiveView().trigger 'markdown-preview:show'
expect(preview.fetchRenderedMarkdown).toHaveBeenCalled()
expect(rootView.getPanes()).toHaveLength 2
expect(pane2.getItems()).toHaveLength 2
expect(pane2.activeItem).toBe preview
expect(pane1).toMatchSelector(':has(:focus)')

View File

@ -0,0 +1,39 @@
MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view'
$ = require 'jquery'
{$$$} = require 'space-pen'
describe "MarkdownPreviewView", ->
[buffer, preview] = []
beforeEach ->
spyOn($, 'ajax')
project.setPath(project.resolve('markdown'))
buffer = project.bufferForPath('file.markdown')
preview = new MarkdownPreviewView(buffer)
afterEach ->
buffer.release()
describe "on construction", ->
ajaxArgs = null
beforeEach ->
ajaxArgs = $.ajax.argsForCall[0][0]
it "shows a loading spinner and fetches the rendered markdown", ->
expect(preview.find('.markdown-spinner')).toExist()
expect($.ajax).toHaveBeenCalled()
expect(JSON.parse(ajaxArgs.data).text).toBe buffer.getText()
ajaxArgs.success($$$ -> @div "WWII", class: 'private-ryan')
expect(preview.find(".private-ryan")).toExist()
it "shows an error message on error", ->
ajaxArgs.error()
expect(preview.text()).toContain "Failed"
describe "serialization", ->
it "reassociates with the same buffer when deserialized", ->
newPreview = deserialize(preview.serialize())
expect(newPreview.buffer).toBe buffer

View File

@ -1,438 +0,0 @@
.markdown-preview {
font-family: "Helvetica Neue", Helvetica, sans-serif;
font-size: 14px;
line-height: 1.6;
position: absolute;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
background-color: #fff;
overflow: auto;
z-index: 3;
box-sizing: border-box;
padding: 20px;
}
.markdown-body {
min-width: 680px;
}
.markdown-body pre,
.markdown-body code,
.markdown-body tt {
font-size: 12px;
font-family: Consolas, "Liberation Mono", Courier, monospace;
}
.markdown-body a {
color: #4183c4;
}
.markdown-body ol > li {
list-style-type: decimal;
}
.markdown-body ul > li {
list-style-type: disc;
}
.markdown-spinner {
margin: auto;
background-image: url(images/octocat-spinner-128.gif);
background-repeat: no-repeat;
background-size: 64px;
background-position: top center;
padding-top: 70px;
text-align: center;
}
/* this code below was copied from https://github.com/assets/stylesheets/primer/components/markdown.css */
/* we really need to get primer in here somehow. */
.markdown-body {
font-size: 14px;
line-height: 1.6;
overflow: hidden; }
.markdown-body > *:first-child {
margin-top: 0 !important; }
.markdown-body > *:last-child {
margin-bottom: 0 !important; }
.markdown-body a.absent {
color: #c00; }
.markdown-body a.anchor {
display: block;
padding-left: 30px;
margin-left: -30px;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0; }
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
margin: 20px 0 10px;
padding: 0;
font-weight: bold;
-webkit-font-smoothing: antialiased;
cursor: text;
position: relative; }
.markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link {
display: none;
color: #000; }
.markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor {
text-decoration: none;
line-height: 1;
padding-left: 0;
margin-left: -22px;
top: 15%; }
.markdown-body h1:hover a.anchor .mini-icon-link, .markdown-body h2:hover a.anchor .mini-icon-link, .markdown-body h3:hover a.anchor .mini-icon-link, .markdown-body h4:hover a.anchor .mini-icon-link, .markdown-body h5:hover a.anchor .mini-icon-link, .markdown-body h6:hover a.anchor .mini-icon-link {
display: inline-block; }
.markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code {
font-size: inherit; }
.markdown-body h1 {
font-size: 28px;
color: #000; }
.markdown-body h2 {
font-size: 24px;
border-bottom: 1px solid #ccc;
color: #000; }
.markdown-body h3 {
font-size: 18px; }
.markdown-body h4 {
font-size: 16px; }
.markdown-body h5 {
font-size: 14px; }
.markdown-body h6 {
color: #777;
font-size: 14px; }
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul, .markdown-body ol, .markdown-body dl,
.markdown-body table,
.markdown-body pre {
margin: 15px 0; }
.markdown-body hr {
background: transparent url("https://a248.e.akamai.net/assets.github.com/assets/primer/markdown/dirty-shade-0e7d81b119cc9beae17b0c98093d121fa0050a74.png") repeat-x 0 0;
border: 0 none;
color: #ccc;
height: 4px;
padding: 0; }
.markdown-body > h2:first-child, .markdown-body > h1:first-child, .markdown-body > h1:first-child + h2, .markdown-body > h3:first-child, .markdown-body > h4:first-child, .markdown-body > h5:first-child, .markdown-body > h6:first-child {
margin-top: 0;
padding-top: 0; }
.markdown-body a:first-child h1, .markdown-body a:first-child h2, .markdown-body a:first-child h3, .markdown-body a:first-child h4, .markdown-body a:first-child h5, .markdown-body a:first-child h6 {
margin-top: 0;
padding-top: 0; }
.markdown-body h1 + p,
.markdown-body h2 + p,
.markdown-body h3 + p,
.markdown-body h4 + p,
.markdown-body h5 + p,
.markdown-body h6 + p {
margin-top: 0; }
.markdown-body li p.first {
display: inline-block; }
.markdown-body ul, .markdown-body ol {
padding-left: 30px; }
.markdown-body ul.no-list, .markdown-body ol.no-list {
list-style-type: none;
padding: 0; }
.markdown-body ul li > :first-child,
.markdown-body ul li ul:first-of-type, .markdown-body ol li > :first-child,
.markdown-body ol li ul:first-of-type {
margin-top: 0px; }
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-bottom: 0; }
.markdown-body dl {
padding: 0; }
.markdown-body dl dt {
font-size: 14px;
font-weight: bold;
font-style: italic;
padding: 0;
margin: 15px 0 5px; }
.markdown-body dl dt:first-child {
padding: 0; }
.markdown-body dl dt > :first-child {
margin-top: 0px; }
.markdown-body dl dt > :last-child {
margin-bottom: 0px; }
.markdown-body dl dd {
margin: 0 0 15px;
padding: 0 15px; }
.markdown-body dl dd > :first-child {
margin-top: 0px; }
.markdown-body dl dd > :last-child {
margin-bottom: 0px; }
.markdown-body blockquote {
border-left: 4px solid #DDD;
padding: 0 15px;
color: #777; }
.markdown-body blockquote > :first-child {
margin-top: 0px; }
.markdown-body blockquote > :last-child {
margin-bottom: 0px; }
.markdown-body table th {
font-weight: bold; }
.markdown-body table th, .markdown-body table td {
border: 1px solid #ccc;
padding: 6px 13px; }
.markdown-body table tr {
border-top: 1px solid #ccc;
background-color: #fff; }
.markdown-body table tr:nth-child(2n) {
background-color: #f8f8f8; }
.markdown-body img {
max-width: 100%;
-moz-box-sizing: border-box;
box-sizing: border-box; }
.markdown-body span.frame {
display: block;
overflow: hidden; }
.markdown-body span.frame > span {
border: 1px solid #ddd;
display: block;
float: left;
overflow: hidden;
margin: 13px 0 0;
padding: 7px;
width: auto; }
.markdown-body span.frame span img {
display: block;
float: left; }
.markdown-body span.frame span span {
clear: both;
color: #333;
display: block;
padding: 5px 0 0; }
.markdown-body span.align-center {
display: block;
overflow: hidden;
clear: both; }
.markdown-body span.align-center > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: center; }
.markdown-body span.align-center span img {
margin: 0 auto;
text-align: center; }
.markdown-body span.align-right {
display: block;
overflow: hidden;
clear: both; }
.markdown-body span.align-right > span {
display: block;
overflow: hidden;
margin: 13px 0 0;
text-align: right; }
.markdown-body span.align-right span img {
margin: 0;
text-align: right; }
.markdown-body span.float-left {
display: block;
margin-right: 13px;
overflow: hidden;
float: left; }
.markdown-body span.float-left span {
margin: 13px 0 0; }
.markdown-body span.float-right {
display: block;
margin-left: 13px;
overflow: hidden;
float: right; }
.markdown-body span.float-right > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: right; }
.markdown-body code, .markdown-body tt {
margin: 0 2px;
padding: 0px 5px;
border: 1px solid #eaeaea;
background-color: #f8f8f8;
border-radius: 3px; }
.markdown-body code {
white-space: nowrap; }
.markdown-body pre > code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent; }
.markdown-body .highlight pre, .markdown-body pre {
background-color: #f8f8f8;
border: 1px solid #ccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px; }
.markdown-body pre code, .markdown-body pre tt {
margin: 0;
padding: 0;
background-color: transparent;
border: none; }
/* this code was copied from https://github.com/assets/stylesheets/primer/components/pygments.css */
/* the .markdown-body class was then added to all rules */
.markdown-body .highlight {
background: #ffffff; }
.markdown-body .highlight .c {
color: #999988;
font-style: italic; }
.markdown-body .highlight .err {
color: #a61717;
background-color: #e3d2d2; }
.markdown-body .highlight .k {
font-weight: bold; }
.markdown-body .highlight .o {
font-weight: bold; }
.markdown-body .highlight .cm {
color: #999988;
font-style: italic; }
.markdown-body .highlight .cp {
color: #999999;
font-weight: bold; }
.markdown-body .highlight .c1 {
color: #999988;
font-style: italic; }
.markdown-body .highlight .cs {
color: #999999;
font-weight: bold;
font-style: italic; }
.markdown-body .highlight .gd {
color: #000000;
background-color: #ffdddd; }
.markdown-body .highlight .gd .x {
color: #000000;
background-color: #ffaaaa; }
.markdown-body .highlight .ge {
font-style: italic; }
.markdown-body .highlight .gr {
color: #aa0000; }
.markdown-body .highlight .gh {
color: #999999; }
.markdown-body .highlight .gi {
color: #000000;
background-color: #ddffdd; }
.markdown-body .highlight .gi .x {
color: #000000;
background-color: #aaffaa; }
.markdown-body .highlight .go {
color: #888888; }
.markdown-body .highlight .gp {
color: #555555; }
.markdown-body .highlight .gs {
font-weight: bold; }
.markdown-body .highlight .gu {
color: #800080;
font-weight: bold; }
.markdown-body .highlight .gt {
color: #aa0000; }
.markdown-body .highlight .kc {
font-weight: bold; }
.markdown-body .highlight .kd {
font-weight: bold; }
.markdown-body .highlight .kn {
font-weight: bold; }
.markdown-body .highlight .kp {
font-weight: bold; }
.markdown-body .highlight .kr {
font-weight: bold; }
.markdown-body .highlight .kt {
color: #445588;
font-weight: bold; }
.markdown-body .highlight .m {
color: #009999; }
.markdown-body .highlight .s {
color: #d14; }
.markdown-body .highlight .na {
color: #008080; }
.markdown-body .highlight .nb {
color: #0086B3; }
.markdown-body .highlight .nc {
color: #445588;
font-weight: bold; }
.markdown-body .highlight .no {
color: #008080; }
.markdown-body .highlight .ni {
color: #800080; }
.markdown-body .highlight .ne {
color: #990000;
font-weight: bold; }
.markdown-body .highlight .nf {
color: #990000;
font-weight: bold; }
.markdown-body .highlight .nn {
color: #555555; }
.markdown-body .highlight .nt {
color: #000080; }
.markdown-body .highlight .nv {
color: #008080; }
.markdown-body .highlight .ow {
font-weight: bold; }
.markdown-body .highlight .w {
color: #bbbbbb; }
.markdown-body .highlight .mf {
color: #009999; }
.markdown-body .highlight .mh {
color: #009999; }
.markdown-body .highlight .mi {
color: #009999; }
.markdown-body .highlight .mo {
color: #009999; }
.markdown-body .highlight .sb {
color: #d14; }
.markdown-body .highlight .sc {
color: #d14; }
.markdown-body .highlight .sd {
color: #d14; }
.markdown-body .highlight .s2 {
color: #d14; }
.markdown-body .highlight .se {
color: #d14; }
.markdown-body .highlight .sh {
color: #d14; }
.markdown-body .highlight .si {
color: #d14; }
.markdown-body .highlight .sx {
color: #d14; }
.markdown-body .highlight .sr {
color: #009926; }
.markdown-body .highlight .s1 {
color: #d14; }
.markdown-body .highlight .ss {
color: #990073; }
.markdown-body .highlight .bp {
color: #999999; }
.markdown-body .highlight .vc {
color: #008080; }
.markdown-body .highlight .vg {
color: #008080; }
.markdown-body .highlight .vi {
color: #008080; }
.markdown-body .highlight .il {
color: #009999; }
.markdown-body .highlight .gc {
color: #999;
background-color: #EAF2F5; }
.type-csharp .markdown-body .highlight .k {
color: #0000FF; }
.type-csharp .markdown-body .highlight .kt {
color: #0000FF; }
.type-csharp .markdown-body .highlight .nf {
color: #000000;
font-weight: normal; }
.type-csharp .markdown-body .highlight .nc {
color: #2B91AF; }
.type-csharp .markdown-body .highlight .nn {
color: #000000; }
.type-csharp .markdown-body .highlight .s {
color: #A31515; }
.type-csharp .markdown-body .highlight .sc {
color: #A31515; }

View File

@ -0,0 +1,403 @@
.markdown-preview {
font-family: "Helvetica Neue", Helvetica, sans-serif;
font-size: 14px;
line-height: 1.6;
background-color: #fff;
overflow: scroll;
box-sizing: border-box;
padding: 20px;
}
.markdown-spinner {
margin: auto;
background-image: url(images/octocat-spinner-128.gif);
background-repeat: no-repeat;
background-size: 64px;
background-position: top center;
padding-top: 70px;
text-align: center;
}
// This is styling for generic markdownized text. Anything you put in a
// container with .markdown-preview on it should render generally well. It also
// includes some GitHub Flavored Markdown specific styling (like @mentions)
.markdown-preview {
pre,
code,
tt {
font-size: 12px;
font-family: Consolas, "Liberation Mono", Courier, monospace;
}
a {
color: #4183c4;
}
ol > li {
list-style-type: decimal;
}
ul > li {
list-style-type: disc;
}
& > *:first-child {
margin-top: 0 !important;
}
& > *:last-child {
margin-bottom: 0 !important;
}
// Link Colors
a.absent {
color: #c00;
}
a.anchor {
display: block;
padding-left: 30px;
margin-left: -30px;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0;
}
// Headings
h1, h2, h3, h4, h5, h6 {
margin: 20px 0 10px;
padding: 0;
font-weight: bold;
-webkit-font-smoothing: antialiased;
cursor: text;
position: relative;
.mini-icon-link {
display: none;
color: #000;
}
&:hover a.anchor {
text-decoration: none;
line-height: 1;
padding-left: 0;
margin-left: -22px;
top: 15%;
.mini-icon-link {
display: inline-block;
}
}
tt, code {
font-size: inherit;
}
}
h1 {
font-size: 28px;
color: #000;
}
h2 {
font-size: 24px;
border-bottom: 1px solid #ccc;
color: #000;
}
h3 {
font-size: 18px;
}
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
color: #777;
font-size: 14px;
}
p,
blockquote,
ul, ol, dl,
table,
pre {
margin: 15px 0;
}
hr {
background: transparent;
border: 0 none;
color: #ccc;
height: 4px;
padding: 0;
}
& > h2:first-child,
& > h1:first-child,
& > h1:first-child + h2,
& > h3:first-child,
& > h4:first-child,
& > h5:first-child,
& > h6:first-child {
margin-top: 0;
padding-top: 0;
}
// fixes margin on shit like:
// <a name='the-heading'>
// <h1>The Heading</h1></a>
a:first-child {
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
padding-top: 0;
}
}
h1 + p,
h2 + p,
h3 + p,
h4 + p,
h5 + p,
h6 + p {
margin-top: 0;
}
// ReST first graf in nested list
li p.first {
display: inline-block;
}
// Lists, Blockquotes & Such
ul, ol {
padding-left: 30px;
&.no-list {
list-style-type: none;
padding: 0;
}
li > :first-child,
li ul:first-of-type {
margin-top: 0px;
}
}
ul ul,
ul ol,
ol ol,
ol ul {
margin-bottom: 0;
}
dl {
padding: 0;
}
dl dt {
font-size: 14px;
font-weight: bold;
font-style: italic;
padding: 0;
margin: 15px 0 5px;
&:first-child {
padding: 0;
}
& > :first-child {
margin-top: 0px;
}
& > :last-child {
margin-bottom: 0px;
}
}
dl dd {
margin: 0 0 15px;
padding: 0 15px;
& > :first-child {
margin-top: 0px;
}
& > :last-child {
margin-bottom: 0px;
}
}
blockquote {
border-left: 4px solid #DDD;
padding: 0 15px;
color: #777;
& > :first-child {
margin-top: 0px;
}
& > :last-child {
margin-bottom: 0px;
}
}
// Tables
table {
th {
font-weight: bold;
}
th, td {
border: 1px solid #ccc;
padding: 6px 13px;
}
tr {
border-top: 1px solid #ccc;
background-color: #fff;
&:nth-child(2n) {
background-color: #f8f8f8;
}
}
}
// Images & Stuff
img {
max-width: 100%;
@include box-sizing();
}
// Gollum Image Tags
// Framed
span.frame {
display: block;
overflow: hidden;
& > span {
border: 1px solid #ddd;
display: block;
float: left;
overflow: hidden;
margin: 13px 0 0;
padding: 7px;
width: auto;
}
span img {
display: block;
float: left;
}
span span {
clear: both;
color: #333;
display: block;
padding: 5px 0 0;
}
}
span.align-center {
display: block;
overflow: hidden;
clear: both;
& > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: center;
}
span img {
margin: 0 auto;
text-align: center;
}
}
span.align-right {
display: block;
overflow: hidden;
clear: both;
& > span {
display: block;
overflow: hidden;
margin: 13px 0 0;
text-align: right;
}
span img {
margin: 0;
text-align: right;
}
}
span.float-left {
display: block;
margin-right: 13px;
overflow: hidden;
float: left;
span {
margin: 13px 0 0;
}
}
span.float-right {
display: block;
margin-left: 13px;
overflow: hidden;
float: right;
& > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: right;
}
}
// Inline code snippets
code, tt {
margin: 0 2px;
padding: 0px 5px;
border: 1px solid #eaeaea;
background-color: #f8f8f8;
border-radius:3px;
}
code { white-space: nowrap; }
// Code tags within code blocks (<pre>s)
pre > code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent;
}
.highlight pre, pre {
background-color: #f8f8f8;
border: 1px solid #ccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius:3px;
}
pre code, pre tt {
margin: 0;
padding: 0;
background-color: transparent;
border: none;
}
}

View File

@ -0,0 +1,201 @@
.highlight {
background: #ffffff;
// Comment
.c { color: #999988; font-style: italic }
// Error
.err { color: #a61717; background-color: #e3d2d2 }
// Keyword
.k { font-weight: bold }
// Operator
.o { font-weight: bold }
// Comment.Multiline
.cm { color: #999988; font-style: italic }
// Comment.Preproc
.cp { color: #999999; font-weight: bold }
// Comment.Single
.c1 { color: #999988; font-style: italic }
// Comment.Special
.cs { color: #999999; font-weight: bold; font-style: italic }
// Generic.Deleted
.gd { color: #000000; background-color: #ffdddd }
// Generic.Deleted.Specific
.gd .x { color: #000000; background-color: #ffaaaa }
// Generic.Emph
.ge { font-style: italic }
// Generic.Error
.gr { color: #aa0000 }
// Generic.Heading
.gh { color: #999999 }
// Generic.Inserted
.gi { color: #000000; background-color: #ddffdd }
// Generic.Inserted.Specific
.gi .x { color: #000000; background-color: #aaffaa }
// Generic.Output
.go { color: #888888 }
// Generic.Prompt
.gp { color: #555555 }
// Generic.Strong
.gs { font-weight: bold }
// Generic.Subheading
.gu { color: #800080; font-weight: bold; }
// Generic.Traceback
.gt { color: #aa0000 }
// Keyword.Constant
.kc { font-weight: bold }
// Keyword.Declaration
.kd { font-weight: bold }
// Keyword.Namespace
.kn { font-weight: bold }
// Keyword.Pseudo
.kp { font-weight: bold }
// Keyword.Reserved
.kr { font-weight: bold }
// Keyword.Type
.kt { color: #445588; font-weight: bold }
// Literal.Number
.m { color: #009999 }
// Literal.String
.s { color: #d14 }
// Name
.n { color: #333333 }
// Name.Attribute
.na { color: #008080 }
// Name.Builtin
.nb { color: #0086B3 }
// Name.Class
.nc { color: #445588; font-weight: bold }
// Name.Constant
.no { color: #008080 }
// Name.Entity
.ni { color: #800080 }
// Name.Exception
.ne { color: #990000; font-weight: bold }
// Name.Function
.nf { color: #990000; font-weight: bold }
// Name.Namespace
.nn { color: #555555 }
// Name.Tag
.nt { color: #000080 }
// Name.Variable
.nv { color: #008080 }
// Operator.Word
.ow { font-weight: bold }
// Text.Whitespace
.w { color: #bbbbbb }
// Literal.Number.Float
.mf { color: #009999 }
// Literal.Number.Hex
.mh { color: #009999 }
// Literal.Number.Integer
.mi { color: #009999 }
// Literal.Number.Oct
.mo { color: #009999 }
// Literal.String.Backtick
.sb { color: #d14 }
// Literal.String.Char
.sc { color: #d14 }
// Literal.String.Doc
.sd { color: #d14 }
// Literal.String.Double
.s2 { color: #d14 }
// Literal.String.Escape
.se { color: #d14 }
// Literal.String.Heredoc
.sh { color: #d14 }
// Literal.String.Interpol
.si { color: #d14 }
// Literal.String.Other
.sx { color: #d14 }
// Literal.String.Regex
.sr { color: #009926 }
// Literal.String.Single
.s1 { color: #d14 }
// Literal.String.Symbol
.ss { color: #990073 }
// Name.Builtin.Pseudo
.bp { color: #999999 }
// Name.Variable.Class
.vc { color: #008080 }
// Name.Variable.Global
.vg { color: #008080 }
// Name.Variable.Instance
.vi { color: #008080 }
// Literal.Number.Integer.Long
.il { color: #009999 }
.gc {
color: #999;
background-color: #EAF2F5;
}
}
.type-csharp .highlight {
.k { color: #0000FF }
.kt { color: #0000FF }
.nf { color: #000000; font-weight: normal }
.nc { color: #2B91AF }
.nn { color: #000000 }
.s { color: #A31515 }
.sc { color: #A31515 }
}

View File

@ -62,6 +62,7 @@ class PackageGeneratorView extends View
for path in fs.listTree(templatePath)
relativePath = path.replace(templatePath, "")
relativePath = relativePath.replace(/^\//, '')
relativePath = relativePath.replace(/\.template$/, '')
relativePath = @replacePackageNamePlaceholders(relativePath, packageName)
sourcePath = fs.join(@getPackagePath(), relativePath)

View File

@ -37,12 +37,6 @@ describe 'Package Generator', ->
packagePath = "/tmp/atom-packages/#{packageName}"
fs.remove(packagePath) if fs.exists(packagePath)
@addMatchers
toExistOnDisk: (expected) ->
notText = this.isNot and " not" or ""
@message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
fs.exists(@actual)
afterEach ->
fs.remove(packagePath) if fs.exists(packagePath)

View File

@ -7,7 +7,7 @@ describe "Spell check", ->
window.rootView = new RootView
rootView.open('sample.js')
config.set('spell-check.grammars', [])
window.loadPackage('spell-check')
window.loadPackage('spell-check', activateImmediately: true)
rootView.attachToDom()
editor = rootView.getActiveView()

View File

@ -127,8 +127,10 @@ class TreeView extends ScrollView
@root = null
selectActiveFile: ->
activeFilePath = rootView.getActiveView()?.getPath()
@selectEntryForPath(activeFilePath) if activeFilePath
if activeFilePath = rootView.getActiveView()?.getPath?()
@selectEntryForPath(activeFilePath)
else
@deselect()
revealActiveFile: ->
@attach()
@ -298,9 +300,12 @@ class TreeView extends ScrollView
return false unless entry.get(0)
entry = entry.view() unless entry instanceof View
@selectedPath = entry.getPath()
@treeViewList.find('.selected').removeClass('selected')
@deselect()
entry.addClass('selected')
deselect: ->
@treeViewList.find('.selected').removeClass('selected')
scrollTop: (top) ->
if top
@treeViewList.scrollTop(top)

View File

@ -1,4 +1,5 @@
$ = require 'jquery'
{$$} = require 'space-pen'
_ = require 'underscore'
TreeView = require 'tree-view/lib/tree-view'
RootView = require 'root-view'
@ -49,7 +50,7 @@ describe "TreeView", ->
rootView.deactivate()
window.rootView = new RootView()
rootView.open()
treeView = window.loadPackage("tree-view").packageMain.createView()
treeView = window.loadPackage("tree-view").mainModule.createView()
it "does not attach to the root view or create a root node when initialized", ->
expect(treeView.hasParent()).toBeFalsy()
@ -75,13 +76,13 @@ describe "TreeView", ->
rootView.deactivate()
window.rootView = new RootView
rootView.open('tree-view.js')
treeView = window.loadPackage("tree-view").packageMain.createView()
treeView = window.loadPackage("tree-view").mainModule.createView()
expect(treeView.hasParent()).toBeFalsy()
expect(treeView.root).toExist()
describe "when the root view is opened to a directory", ->
it "attaches to the root view", ->
treeView = window.loadPackage("tree-view").packageMain.createView()
treeView = window.loadPackage("tree-view").mainModule.createView()
expect(treeView.hasParent()).toBeTruthy()
expect(treeView.root).toExist()
@ -301,18 +302,25 @@ describe "TreeView", ->
expect(subdir).toHaveClass 'expanded'
expect(rootView.getActiveView().isFocused).toBeFalsy()
describe "when a new file is opened in the active editor", ->
it "selects the file in the tree view if the file's entry visible", ->
sampleJs.click()
rootView.open(fs.resolveOnLoadPath('fixtures/tree-view/tree-view.txt'))
describe "when the active item changes on the active pane", ->
describe "when the item has a path", ->
it "selects the entry with that path in the tree view if it is visible", ->
sampleJs.click()
rootView.open(require.resolve('fixtures/tree-view/tree-view.txt'))
expect(sampleTxt).toHaveClass 'selected'
expect(treeView.find('.selected').length).toBe 1
expect(sampleTxt).toHaveClass 'selected'
expect(treeView.find('.selected').length).toBe 1
it "selects the file's parent dir if the file's entry is not visible", ->
rootView.open('dir1/sub-dir1/sub-file1')
dirView = treeView.root.find('.directory:contains(dir1)').view()
expect(dirView).toHaveClass 'selected'
it "selects the path's parent dir if its entry is not visible", ->
rootView.open('dir1/sub-dir1/sub-file1')
dirView = treeView.root.find('.directory:contains(dir1)').view()
expect(dirView).toHaveClass 'selected'
describe "when the item has no path", ->
it "deselects the previously selected entry", ->
sampleJs.click()
rootView.getActivePane().showItem($$ -> @div('hello'))
expect(rootView.find('.selected')).not.toExist()
describe "when a different editor becomes active", ->
it "selects the file in that is open in that editor", ->

View File

@ -1,57 +0,0 @@
.tree-view-wrapper {
position: relative;
height: 100%;
cursor: default;
-webkit-user-select: none;
min-width: 50px;
z-index: 2;
}
.tree-view {
position: relative;
cursor: default;
-webkit-user-select: none;
overflow: auto;
height: 100%;
}
.tree-view-wrapper .tree-view-resizer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 10px;
cursor: col-resize;
z-index: 3;
}
.tree-view .entry {
text-wrap: none;
white-space: nowrap;
}
.tree-view .entry > .header,
.tree-view .entry > .name {
z-index: 1;
position: relative;
display: inline-block;
}
.tree-view .selected > .highlight {
position: absolute;
left: 0;
right: 0;
height: 24px;
}
.tree-view .disclosure-arrow {
display: inline-block;
}
.tree-view-dialog {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
}

View File

@ -0,0 +1,57 @@
.tree-view-wrapper {
position: relative;
height: 100%;
cursor: default;
-webkit-user-select: none;
min-width: 50px;
z-index: 2;
.tree-view-resizer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 10px;
cursor: col-resize;
z-index: 3;
}
}
.tree-view {
position: relative;
cursor: default;
-webkit-user-select: none;
overflow: auto;
height: 100%;
.entry {
text-wrap: none;
white-space: nowrap;
& > .header,
> .name {
z-index: 1;
position: relative;
display: inline-block;
}
}
.selected > .highlight {
position: absolute;
left: 0;
right: 0;
height: 24px;
}
.disclosure-arrow {
display: inline-block;
}
}
.tree-view-dialog {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
}

View File

@ -1,3 +1,4 @@
require 'underscore-extensions'
_ = require 'underscore'
fs = require 'fs'
fsUtils = require 'fs-utils'

View File

@ -86,11 +86,11 @@ module.exports =
paths = []
if extensions
onPath = (path) =>
paths.push(@join(rootPath, path)) if _.contains(extensions, @extension(path))
paths.push(path) if _.contains(extensions, @extension(path))
false
else
onPath = (path) =>
paths.push(@join(rootPath, path))
paths.push(path)
false
@traverseTreeSync(rootPath, onPath, onPath)
paths
@ -98,7 +98,7 @@ module.exports =
listTree: (rootPath) ->
paths = []
onPath = (path) =>
paths.push(@join(rootPath, path))
paths.push(path)
true
@traverseTreeSync(rootPath, onPath, onPath)
paths
@ -162,9 +162,9 @@ module.exports =
absolutePath = @join(rootPath, file)
stats = fs.statSync(absolutePath)
if stats.isDirectory()
traverse(absolutePath, relativePath, onFile, onDirectory) if onDirectory(relativePath)
traverse(absolutePath, relativePath, onFile, onDirectory) if onDirectory(absolutePath)
else if stats.isFile()
onFile(relativePath)
onFile(absolutePath)
traverse(rootPath, '', onFile, onDirectory)

View File

@ -7,6 +7,12 @@ $.fn.scrollBottom = (newValue) ->
else
@scrollTop() + @height()
$.fn.scrollDown = ->
@scrollTop(@scrollTop() + $(window).height() / 20)
$.fn.scrollUp = ->
@scrollTop(@scrollTop() - $(window).height() / 20)
$.fn.scrollToTop = ->
@scrollTop(0)
@ -53,7 +59,9 @@ $.fn.trueHeight = ->
$.fn.trueWidth = ->
this[0].getBoundingClientRect().width
$.fn.document = (eventDescriptions) ->
$.fn.document = (eventName, docString) ->
eventDescriptions = {}
eventDescriptions[eventName] = docString
@data('documentation', {}) unless @data('documentation')
_.extend(@data('documentation'), eventDescriptions)
@ -69,12 +77,20 @@ $.fn.events = ->
else
events
$.fn.command = (args...) ->
eventName = args[0]
documentation = {}
documentation[eventName] = _.humanizeEventName(eventName)
@document(documentation)
@on(args...)
$.fn.command = (eventName, selector, options, handler) ->
if not options?
handler = selector
selector = null
else if not handler?
handler = options
options = null
if selector? and typeof(selector) is 'object'
options = selector
selector = null
@document(eventName, _.humanizeEventName(eventName, options?["doc"]))
@on(eventName, selector, options?['data'], handler)
$.fn.iconSize = (size) ->
@width(size).height(size).css('font-size', size)

View File

@ -52,16 +52,20 @@ _.mixin
regex = RegExp('[' + specials.join('\\') + ']', 'g')
string.replace(regex, "\\$&");
humanizeEventName: (eventName) ->
if /:/.test(eventName)
[namespace, name] = eventName.split(':')
return "#{@humanizeEventName(namespace)}: #{@humanizeEventName(name)}"
humanizeEventName: (eventName, eventDoc) ->
[namespace, event] = eventName.split(':')
return _.capitalize(namespace) unless event?
words = eventName.split('-')
words.map(_.capitalize).join(' ')
namespaceDoc = _.undasherize(namespace)
eventDoc ?= _.undasherize(event)
"#{namespaceDoc}: #{eventDoc}"
capitalize: (word) ->
word[0].toUpperCase() + word[1..]
if word.toLowerCase() is 'github'
'GitHub'
else
word[0].toUpperCase() + word[1..]
pluralize: (count=0, singular, plural=singular+'s') ->
if count is 1
@ -80,6 +84,9 @@ _.mixin
else
"-"
undasherize: (string) ->
string.split('-').map(_.capitalize).join(' ')
underscore: (string) ->
string = string[0].toLowerCase() + string[1..]
string.replace /([A-Z])|(-)/g, (m, letter, dash) ->

View File

@ -1,81 +0,0 @@
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
#root-view {
height: 100%;
overflow: hidden;
position: relative;
}
#root-view #horizontal {
display: -webkit-flex;
height: 100%;
}
#root-view #vertical {
display: -webkit-flex;
-webkit-flex: 1;
-webkit-flex-flow: column;
}
#panes {
position: relative;
-webkit-flex: 1;
}
#panes .column {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-y: hidden;
}
#panes .row {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-x: hidden;
}
#panes .pane {
position: absolute;
display: -webkit-flex;
-webkit-flex-flow: column;
top: 0;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
}
#panes .pane .item-views {
-webkit-flex: 1;
display: -webkit-flex;
-webkit-flex-flow: column;
}
@font-face {
font-family: 'Octicons Regular';
src: url("octicons-regular-webfont.woff") format("woff");
font-weight: normal;
font-style: normal;
}
.is-loading {
background-image: url(images/spinner.svg);
background-repeat: no-repeat;
width: 14px;
height: 14px;
opacity: 0.5;
background-size: contain;
position: relative;
display: inline-block;
padding-left: 19px;
}

86
static/atom.less Normal file
View File

@ -0,0 +1,86 @@
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
#root-view {
height: 100%;
overflow: hidden;
position: relative;
#horizontal {
display: -webkit-flex;
height: 100%;
}
#vertical {
display: -webkit-flex;
-webkit-flex: 1;
-webkit-flex-flow: column;
}
}
#panes {
position: relative;
-webkit-flex: 1;
.column {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-y: hidden;
}
.row {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-x: hidden;
}
.pane {
position: absolute;
display: -webkit-flex;
-webkit-flex-flow: column;
top: 0;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
}
.pane .item-views {
-webkit-flex: 1;
display: -webkit-flex;
-webkit-flex-flow: column;
}
.pane .item-views > * {
-webkit-flex: 1;
min-height: 0;
}
}
@font-face {
font-family: 'Octicons Regular';
src: url("octicons-regular-webfont.woff") format("woff");
font-weight: normal;
font-style: normal;
}
.is-loading {
background-image: url(images/spinner.svg);
background-repeat: no-repeat;
width: 14px;
height: 14px;
opacity: 0.5;
background-size: contain;
position: relative;
display: inline-block;
padding-left: 19px;
}

View File

@ -1,134 +0,0 @@
.command-panel {
position: relative;
padding: 0;
}
.command-panel .is-loading {
display: block;
margin: 0 auto 10px auto;
width: 100px;
background-color: #111;
background-size: auto;
background-position: 5px 5px;
padding: 5px 5px 10px 30px;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-top: 1px solid rgba(0, 0, 0, 1);
border-left: 1px solid rgba(0, 0, 0, 1);
}
.command-panel .preview-count {
display: inline-block;
margin-top: 4px;
font-size: 11px;
}
.command-panel .preview-list {
max-height: 300px;
overflow: auto;
margin: 0 0 10px 0;
position: relative;
cursor: default;
}
.command-panel .header:after {
content: ".";
display: block;
visibility: hidden;
clear: both;
height: 0;
}
.command-panel .expand-collapse {
float: right;
}
.command-panel .expand-collapse li {
display: inline-block;
cursor: pointer;
font-size: 11px;
margin-left: 5px;
padding: 5px 10px;
border-radius: 3px;
}
.command-panel .preview-count,
.command-panel .expand-collapse {
-webkit-user-select: none;
}
.command-panel .preview-list .path {
position: relative;
-webkit-user-select: none;
}
.command-panel .preview-list .path-details:before {
font-family: 'Octicons Regular';
font-size: 12px;
width: 12px;
height: 12px;
margin-right: 5px;
margin-left: 5px;
-webkit-font-smoothing: antialiased;
content: "\f05b";
position: relative;
top: 0;
}
.command-panel .preview-list .is-collapsed .path-details:before {
content: "\f05a";
}
.command-panel .preview-list .path-name:before {
font-family: 'Octicons Regular';
font-size: 16px;
width: 16px;
height: 16px;
margin-right: 5px;
-webkit-font-smoothing: antialiased;
content: "\f011";
position: relative;
top: 1px;
}
.command-panel .preview-list .path.readme .path-name:before {
content: "\f007";
}
.command-panel .preview-list .operation {
padding-top: 2px;
padding-bottom: 2px;
padding-left: 10px;
}
.command-panel .preview-list .line-number {
margin-right: 1ex;
text-align: right;
display: inline-block;
}
.command-panel .preview-list .path-match-number {
padding-left: 8px;
}
.command-panel .preview-list .preview {
word-break: break-all;
}
.command-panel .preview-list .preview .match {
-webkit-border-radius: 2px;
padding: 1px;
}
.command-panel .prompt-and-editor .editor {
position: relative;
}
.command-panel .prompt-and-editor {
display: -webkit-flex;
}
.error-messages {
padding: 5px 1em;
color: white;
}

132
static/command-panel.less Normal file
View File

@ -0,0 +1,132 @@
.command-panel {
position: relative;
padding: 0;
.is-loading {
display: block;
margin: 0 auto 10px auto;
width: 100px;
background-color: #111111;
background-size: auto;
background-position: 5px 5px;
padding: 5px 5px 10px 30px;
border-radius: 3px;
border: 1px solid rgba(255,255,255,0.1);
border-top: 1px solid rgba(0,0,0,1);
border-left: 1px solid rgba(0,0,0,1);
}
.preview-count {
display: inline-block;
margin-top: 4px;
font-size: 11px;
-webkit-user-select: none;
}
.preview-list {
max-height: 300px;
overflow: auto;
margin: 0 0 10px 0;
position: relative;
cursor: default;
.path {
position: relative;
-webkit-user-select: none;
}
.path-details:before {
font-family: 'Octicons Regular';
font-size: 12px;
width: 12px;
height: 12px;
margin-right: 5px;
margin-left: 5px;
-webkit-font-smoothing: antialiased;
content: "\f05b";
position: relative;
top: 0;
}
.is-collapsed .path-details:before {
content: "\f05a";
}
.path-name:before {
font-family: 'Octicons Regular';
font-size: 16px;
width: 16px;
height: 16px;
margin-right: 5px;
-webkit-font-smoothing: antialiased;
content: "\f011";
position: relative;
top: 1px;
}
.path.readme .path-name:before {
content: "\f007";
}
.operation {
padding-top: 2px;
padding-bottom: 2px;
padding-left: 10px;
}
.line-number {
margin-right: 1ex;
text-align: right;
display: inline-block;
}
.path-match-number {
padding-left: 8px;
}
.preview {
word-break: break-all;
.match {
-webkit-border-radius: 2px;
padding: 1px;
}
}
}
.header:after {
content: ".";
display: block;
visibility: hidden;
clear: both;
height: 0;
}
.expand-collapse {
float: right;
-webkit-user-select: none;
li {
display: inline-block;
cursor: pointer;
font-size: 11px;
margin-left: 5px;
padding: 5px 10px;
border-radius: 3px;
}
}
.prompt-and-editor {
display: -webkit-flex;
.editor {
position: relative;
-webkit-flex: 1;
}
}
}
.error-messages {
padding: 5px 1em;
color: white;
}

View File

@ -6,7 +6,6 @@
z-index: 0;
font-family: Inconsolata, Monaco, Courier;
line-height: 1.3;
-webkit-flex: 1;
}
.editor.mini {

View File

@ -1,23 +0,0 @@
.source.gfm {
-webkit-font-smoothing: antialiased;
}
.gfm .markup.heading {
font-weight: bold;
}
.gfm .bold {
font-weight: bold;
}
.gfm .italic {
font-style: italic;
}
.gfm .comment.quote {
font-style: italic;
}
.gfm .raw {
-webkit-font-smoothing: subpixel-antialiased;
}

25
static/markdown.less Normal file
View File

@ -0,0 +1,25 @@
.source {
.gfm {
-webkit-font-smoothing: antialiased;
.markup.heading {
font-weight: bold;
}
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.comment.quote {
font-style: italic;
}
.raw {
-webkit-font-smoothing: subpixel-antialiased;
}
}
}

Some files were not shown because too many files have changed in this diff Show More