Merge branch 'reboot'

This commit is contained in:
Chris Wanstrath
2011-11-14 20:25:48 -08:00
85 changed files with 5927 additions and 1170 deletions

View File

@@ -9,7 +9,4 @@
- (AtomController *)createController:(NSString *)path;
- (void)removeController:(AtomController *)controller;
- (id)storageGet:(NSString *)keyPath defaultValue:(id)defaultValue;
- (id)storageSet:(NSString *)keyPath value:(id)value;
@end

View File

@@ -6,35 +6,20 @@
#define ATOM_USER_PATH ([[NSString stringWithString:@"~/.atomicity/"] stringByStandardizingPath])
#define ATOM_STORAGE_PATH ([ATOM_USER_PATH stringByAppendingPathComponent:@".app-storage"])
#define WEB_STORAGE_PATH ([ATOM_USER_PATH stringByAppendingPathComponent:@".web-storage"])
@implementation AtomApp
@synthesize controllers;
- (AtomController *)createController:(NSString *)path {
if (path) {
NSMutableArray *openedPaths = [self storageGet:@"app.openedPaths" defaultValue:[NSMutableArray array]];
if (![openedPaths containsObject:path]) {
[openedPaths addObject:path];
[self storageSet:@"app.openedPaths" value:openedPaths];
}
}
AtomController *controller = [[AtomController alloc] initWithPath:path];
AtomController *controller = [[AtomController alloc] initWithURL:path];
[controllers addObject:controller];
// window.coffee will set the window size
[[controller window] setFrame:NSMakeRect(0, 0, 0, 0) display:YES animate:NO];
return controller;
}
- (void)removeController:(AtomController *)controller {
[controllers removeObject:controller];
NSMutableArray *openedPaths = [self storageGet:@"app.openedPaths" defaultValue:[NSMutableArray array]];
[openedPaths removeObject:controller.path];
[self storageSet:@"app.openedPaths" value:openedPaths];
[controllers removeObject:controller];
}
- (void)open:(NSString *)path {
@@ -45,16 +30,8 @@
path = [[[panel URLs] lastObject] path];
}
for (AtomController *controller in controllers) {
JSValueRef value = [controller.jscocoa callJSFunctionNamed:@"canOpen" withArguments:path, nil];
if ([controller.jscocoa toBool:value]) {
[controller.jscocoa callJSFunctionNamed:@"open" withArguments:path, nil];
return;
}
}
[self createController:path];
[self createController:path];
}
// Events in the "app:*" namespace get sent to all controllers
@@ -70,7 +47,7 @@
BOOL handeled = NO;
AtomController *controller = [[self keyWindow] windowController];
// The keyWindow could be a Cocoa Dialog or something, ignore that.
// The keyWindow could be a Cocoa Dialog or something, ignore those.
if ([controller isKindOfClass:[AtomController class]]) {
JSValueRef value = [controller.jscocoa callJSFunctionNamed:@"handleKeyEvent" withArguments:event, nil];
handeled = [controller.jscocoa toBool:value];
@@ -95,74 +72,12 @@
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification {
self.controllers = [NSMutableArray array];
// Hack to make localStorage work
WebPreferences* prefs = [WebPreferences standardPreferences];
[prefs _setLocalStorageDatabasePath:WEB_STORAGE_PATH];
[prefs setLocalStorageEnabled:YES];
NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], @"WebKitDeveloperExtras",
nil];
NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], @"WebKitDeveloperExtras", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSError *error = nil;
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:ATOM_USER_PATH withIntermediateDirectories:YES attributes:nil error:&error];
if (!success || error) {
[NSException raise:@"Atom: Failed to open storage path at '%@'. %@" format:ATOM_USER_PATH, [error localizedDescription]];
}
NSArray *openedPaths = [self storageGet:@"app.openedPaths" defaultValue:[NSMutableArray array]];
if (openedPaths.count == 0) {
[self createController:NULL];
}
else {
for (NSString *path in openedPaths) {
[self createController:path];
}
}
}
// Helper Methods that should probably go elsewhere
- (id)storage {
id storage = [NSMutableDictionary dictionaryWithContentsOfFile:ATOM_STORAGE_PATH];
if (!storage) storage = [NSMutableDictionary dictionary];
return storage;
}
- (id)storageGet:(NSString *)keyPath defaultValue:(id)defaultValue {
id storage = [NSMutableDictionary dictionaryWithContentsOfFile:ATOM_STORAGE_PATH];
if (!storage) storage = [NSMutableDictionary dictionary];
id value = [storage valueForKeyPath:keyPath];
if (!value) value = defaultValue;
return value;
}
- (id)storageSet:(NSString *)keyPath value:(id)value {
id storage = [NSMutableDictionary dictionaryWithContentsOfFile:ATOM_STORAGE_PATH];
if (!storage) storage = [NSMutableDictionary dictionary];
NSArray *keys = [keyPath componentsSeparatedByString:@"."];
id parent = storage;
for (int i = 0; i < keys.count - 1; i++) {
NSString *key = [keys objectAtIndex:i];
id newParent = [parent valueForKey:key];
if (!newParent) {
newParent = [NSMutableDictionary dictionary];
[parent setValue:newParent forKey:key];
}
parent = newParent;
}
[storage setValue:value forKeyPath:keyPath];
[storage writeToFile:ATOM_STORAGE_PATH atomically:YES];
return value;
[self createController:nil];
}
@end

View File

@@ -7,9 +7,9 @@
}
@property (retain) WebView *webView;
@property (retain, readonly) NSString *path;
@property (retain, readonly) NSString *url;
@property (retain) JSCocoa *jscocoa;
- (AtomController *)initWithPath:(NSString *)aPath;
- (AtomController *)initWithURL:(NSString *)url;
@end

View File

@@ -4,11 +4,10 @@
#import "JSCocoa.h"
#import <WebKit/WebKit.h>
#import <stdio.h>
@implementation AtomController
@synthesize webView, path, jscocoa;
@synthesize webView, url, jscocoa;
- (void)dealloc {
[jscocoa unlinkAllReferences];
@@ -16,25 +15,23 @@
[jscocoa release]; jscocoa = nil;
[webView release];
[path release];
[url release];
[super dealloc];
}
- (id)initWithPath:(NSString *)aPath {
aPath = aPath ? aPath : @"/tmp";
- (id)initWithURL:(NSString *)_url {
self = [super initWithWindowNibName:@"AtomWindow"];
path = [[aPath stringByStandardizingPath] retain];
url = [[_url stringByStandardizingPath] retain];
[self.window makeKeyWindow];
return self;
}
- (void)windowDidLoad {
[super windowDidLoad];
[[webView inspector] showConsole:self];
[self.window setDelegate:self];
[self.window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
@@ -49,18 +46,7 @@
NSURL *resourceURL = [[NSBundle mainBundle] resourceURL];
NSURL *indexURL = [resourceURL URLByAppendingPathComponent:@"index.html"];
NSURLRequest *request = [NSURLRequest requestWithURL:indexURL];
[[webView mainFrame] loadRequest:request];
}
// Helper methods that should go elsewhere
- (NSString *)tempfile {
char *directory = "/tmp";
char *prefix = "temp-file";
char *tmpPath = tempnam(directory, prefix);
NSString *tmpPathString = [NSString stringWithUTF8String:tmpPath];
free(tmpPath);
return tmpPathString;
[[webView mainFrame] loadRequest:request];
}
// WebUIDelegate
@@ -73,5 +59,5 @@
[(AtomApp *)NSApp removeController:self];
return YES;
}
@end

View File

@@ -46,6 +46,11 @@ static JSClassRef hashObjectClass = NULL;
static void throwException(JSContextRef ctx, JSValueRef* exception, NSString* reason);
BOOL isUsingStret(id argumentEncodings);
JSValueRef valueFromExternalContext(JSContextRef externalCtx, JSValueRef value, JSContextRef ctx);
void* getObjCCallAddress(id argumentEncodings);
JSValueRef boxedValueFromExternalContext(JSContextRef externalCtx, JSValueRef value, JSContextRef ctx);
// iPhone specifics
#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE
const JSClassDefinition kJSClassDefinitionEmpty = { 0, 0,

View File

@@ -10,6 +10,8 @@
#import "JSCocoaController.h"
#include <sys/mman.h> // for mmap()
void closure_function(ffi_cif* cif, void* resp, void** args, void* userdata);
@implementation JSCocoaFFIClosure

View File

@@ -102,7 +102,7 @@ void UKFileSubscriptionProc(FNMessage message, OptionBits flags, void *refcon
kNilOptions, &subscription );
if( err != noErr )
{
NSLog( @"UKFNSubscribeFileWatcher addPath: %@ failed due to error ID=%ld.", path, err );
NSLog( @"UKFNSubscribeFileWatcher addPath: %@ failed due to error ID=%d.", path, err );
return;
}

View File

@@ -269,7 +269,7 @@ static UKKQueue * gUKKQueueSharedQueueSingleton = nil;
-(void) removePathFromQueue: (NSString*)path
{
int index = 0;
NSUInteger index = 0;
int fd = -1;
AT_SYNCHRONIZED( self )

View File

@@ -1,4 +1,4 @@
![](https://img.skitch.com/20110828-e6a2sk5mqewpfnxb3eeuef112d.png)
# Atom — Futuristic Text Editing
## Informative Links

8
docs/chris-wants.txt Normal file
View File

@@ -0,0 +1,8 @@
Want
- open raw HTML in browser tab
- cmd-shift-p: preview markdown as HTML
- cmd-t file finder uses project.settings.extraURLs
- atom.process.run "shell-command", -> stdoutCallback()

15
docs/corey-wants.txt Normal file
View File

@@ -0,0 +1,15 @@
Intent
- open a folder
- child urls of folder open in same window
- open/save/close on project
Want
- cmd-shift-t: reopen last closed tab
- an extension that shows all available key commands
- stdlib should be indepentent of atom (no events in stdlib)

30
docs/extensions.md Normal file
View File

@@ -0,0 +1,30 @@
* Project
find in project
find file in project
issues
gem scraper
npm scraper
rake runner
test runner
rails extension
ctags
treeview
tabs
* Resource
gist resource
* Window
gist creator/browser
thunderhorse
align
git
console
* Editor
debugger
markdown viewer
(coffee -> java)script
surround
snippets

View File

@@ -1,36 +0,0 @@
# Keybinding ideas
# ----------------
# Are ctrl-v and ctrl-V different?
# Nested commands? Use timeout?
# Optional Regex, is that fucking crazy?
# Command/Control/Option or cmd/ctrl/alt
# How should we deal with scope?
keymap:
# Take some method found in the keymap scope?
'cmd-c': 'copyText'
# Take a block
'cmd-v': -> paste.someText()
# Can take a regex
# * how would this work with timeouts
# * how can this not look so hackish
# * do we even need this?
'cmd-/(\d+)/': (number) ->
window.switchTo(parseInt(number))
# Nested commands
'cmd-ctrl-r':
'r': -> run.something()
't': -> test.something()
# Switch modes? I don't like this syntax
'mode(normal):esc':
'j': 'moveDown'
'k': 'moveUp'
'a': ->
goto.endOfLine()
keybindingMode('normal')
'mode(insert):i':
'delete': 'delete'

166
extensions/docs/cdoc.coffee Normal file
View File

@@ -0,0 +1,166 @@
# cdoc is a bite sized library which scans CoffeeScript
# source code and returns a js object containing class, module
# and method names, along with the comments and line numbers
# associated with them.
#
# It pairs finely with TomDoc, but really doesn't care which
# documentation format you use. As long as your class, module, and
# method definitions are preceded by a comment, cdoc will do its job.
module.exports = cdoc =
# Parses CoffeeScript source code into an object describing
# the class names and modules defined therein. Method names,
# comments, and line numbers are also included.
#
# text - The String CoffeeScript source code to parse into a js object.
#
# Returns an object like this:
#
# [
# name: "App"
# comment:"Singleton object representing the application.",
# line: 5,
# methods: [
# name: "setTitle"
# signature: "setTitle: (title) ->"
# params: [
# name: "title"
# ]
# comment: "Sets the title of the app."
# line: 7
# ,
# name: "title"
# signature: "title: ->"
# params: []
# comment: "Returns the String title of the app."
# line: 11
# ]
# name: "class Robot"
# comment: "A Robot that can walk and talk, just like a real boy."
# line: 15
# methods: [
# name: "constructor"
# signature: "constructor: (path) ->"
# params: [
# name: "path"
# ]
# comment: "Robots receive messages from a..."
# line: 20
# ]
# ]
#
# In the above, App is an object while Robot is a class.
parse: (text) ->
scopes = []
lastComment = ''
methodIndent = null
moduleIsFn = false
text.split("\n").forEach (line, lineno) =>
return if moduleIsFn
lineno++
# Skip empty lines and lines containing only a comment symbol.
return if not line.trim()
# If this line is a comment, record it and move on.
if line.trim()[0] is '#'
lastComment += line.trim().match(/^\s*\#\s?(.*)$/)[1] + "\n"
return
# detect:
# 1. module.exports = (params) ->
# 2. module.exports = (params) =>
if match = line.match /^\s*module.exports\s*=\s*((?:\((.+)\))?\s*(-|=)>)/
moduleIsFn = true
[ signature, params ] = match[1..2]
params = for param in params?.split(',') or []
{ name: param.trim() }
scopes.push
name: 'module'
signature: signature
params: params
comment: lastComment.trim()
line: lineno
# detect:
# 1. module.exports =
# 2. module.exports = MyModule =
# 3. module.exports = class MyClass
# 4. exports.MyClass = class MyClass
else if match = line.match /^\s*(?:module.exports|exports\.[\w\.]+)\s*=\s*(?:(class [\w\.]+)(?:\s+extends [\w.]+)?|([\w\.]+)\s*=)\s*$/
methodIndent = null
scopes.push
name: match[1] or match[2] or 'module'
comment: lastComment.trim()
line: lineno
methods: []
# detect: class Window
else if newScope = line.match(/^\s*class ([\w\.]+)/)?[0]
methodIndent = null
scopes.push
name: newScope
comment: lastComment.trim()
line: lineno
methods: []
# detect:
# 1. hear: ->
# 2. hear: (regex, args...) ->
# 3. hear = ->
# 4. hear = (regex, args...) ->
# also detect `@hear` and `=>` forms of the above
# as well as `exports.head = (regex, args...) ->` form
else if match = line.match /^\s*(@|exports\.)?(([\w\.]+)\s*(:|=)\s*(?:\((.*?)\))?\s*(-|=)>)\s*/
# If the indentation of this method is deeper than the
# previous method definition, and we're still in the same
# scope, bail out. Probably an inner function.
indent = line.match(/^(\s*)/)[0].length
return if methodIndent? and methodIndent < indent
methodIndent = indent
[ prefix, fn, name, type, params ] = match[1..-1]
# floating function
if type is '=' and prefix isnt 'exports.'
return
# Normalize the method signature.
# 1. exports.method = (param) ->
# exports.method: (param) ->
# 2. method: () ->
# method: ->
fn = fn.trim()
fn = fn.replace /\s+=\s+((\(|-|=))/, ': $1'
fn = fn.replace /\s*\(\)/, ''
params = for param in params?.split(',') or []
{ name: param.trim() }
if prefix is 'exports.'
modules = scopes.filter (scope) -> scope?.name is 'module'
scope = modules[0]
if not scope
scopes.push scope =
name: 'module'
comment: ""
line: 0
methods: []
else
scope = scopes[scopes.length - 1]
scope.methods ?= []
scope.methods.push
name: name
signature: fn
params: params
comment: lastComment.trim()
line: lineno
# Clear the last comment, we're done with it.
lastComment = ''
scopes

View File

@@ -0,0 +1,23 @@
fs = require 'fs'
tdoc = require 'docs/tdoc'
Browser = require 'browser'
module.exports =
class Docs extends Browser
window.resourceTypes.push this
running: true
constructor: ->
atom.keybinder.load require.resolve "docs/key-bindings.coffee"
open: (url) ->
return false if not url
if match = url.match /^docs:\/\/(.+)/
@url = url
code = fs.read match[1]
@show tdoc.html code
true

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
hbar = require './handlebars'
{Showdown} = require './showdown'
converter = new Showdown.converter
hbar.registerHelper 'format', (comment) ->
comment = comment+''
# param - info => <b>param:</b> info
comment = comment.replace /(?:^\s*(\S+?)\s+-\s+(.+)$)+/img,
'<br>**$1:** $2'
comment = comment.replace '<br>**', '<br><br>**'
# markdownize
comment = converter.makeHtml comment
# <pre><code>code</code></pre> => <pre>code</pre>
comment = comment.replace /<pre><code>((?:.|\n)+)<\/code><\/pre>/img,
'<pre>$1</pre>'
new hbar.SafeString comment

View File

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

View File

@@ -0,0 +1,2 @@
editor:
'cmd-shift-d': (editor) -> window.open "docs://#{editor.url}"

1302
extensions/docs/showdown.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,82 @@
# tdoc is a bite sized library for generating HTML documentation
# from CoffeeScript source code and Markdown files.
#
# It pairs finely with TomDoc, but really doesn't care which
# documentation format you use. As long as your class, module, and
# method definitions are preceded by a comment, tdoc will do its job.
File = require 'fs'
cdoc = require './cdoc'
hbar = require './handlebars'
require './helpers'
# The tdoc module is our main interface. Using it we can turn CoffeeScript
# or Markdown into HTML using a template.
module.exports = tdoc =
# Theme to use. Set using setTheme()
theme: 'default'
# Turns code into HTML docs.
#
# code - String of source code.
# options -
# path: The path to the file this code is from.
#
# Returns a String of HTML.
html: (code, options={}) ->
options.path ?= ''
options.paths = options.paths ? []
options.sourceURL = "https://github.com/github/hubot/tree/master/"
options.code = code
if /\.(md|markdown|mdown|txt)$/.test options.path
@render @template('markdown'), options
else if options.path is '' or /\.coffee$/.test options.path
context = cdoc.parse code
context[key] = value for key, value of options
@render @template(), context
else
"Don't know how to parse #{options.path}."
# Sets the current theme.
#
# theme - String name of the theme you want to use.
#
# Returns nothing.
setTheme: (name) ->
@theme = name
# Finds a template.
#
# name - String name of the template you want.
#
# Returns a String template
template: (name='module') ->
# lame require.resolve hack :(
layout = @read require.resolve "docs/themes/#{@theme}/layout.html"
file = @read require.resolve "docs/themes/#{@theme}/#{name}.html"
layout.replace /{{{\s*yield\s*}}}/, file
# Renders a template using Handlebars.js.
#
# template - String template to render.
# context - Object to use as context of the template.
#
# Returns the fully rendered template.
render: (template, context) ->
compiled = hbar.compile template
compiled context
# Reads a file synchronously using either CommonJS or node.
#
# path - String path to the file you want to read.
# encoding - String encoding to use when reading the file.
#
# Returns a String.
read: (path, encoding="utf8") ->
if File.readFileSync?
File.readFileSync path, encoding
else if File.read?
File.read path

View File

@@ -0,0 +1,11 @@
<h1>Hubot</h1>
<h3>A simple helpful Robot for your Company</h3>
<p>
<ul class="unstyled">
<li><a href="#">Creator</a></li>
<li><a href="#">Robot</a></li>
<li><a href="#">Robot.Response</a></li>
</ul>
</p>

View File

@@ -0,0 +1,32 @@
<html>
<head>
<link rel="stylesheet" href="http://twitter.github.com/bootstrap/assets/css/bootstrap-1.2.0.min.css">
<style>
p { color: #404040; margin-bottom: 20px; font-size: 14px; line-height: 20px; }
ul { list-style-type: none; }
li { color: #404040; }
h5 { border-bottom: 1px dotted #404040; line-height: 20px; margin-bottom: 8px; }
h5 a { color: inherit; }
h5 a:hover { text-decoration: none; }
pre { background-color: #FEE9CC; }
#main { width: 610px; margin:0 auto; padding-top: 10px; }
#sidebar { float: left; padding-top: 12px; }
</style>
</head>
<body>
<div class="container">
<div id="sidebar">
<ul>
{{# paths}}
<li><a href="{{this}}.html">{{this}}</a></li>
{{/ paths}}
</ul>
</div>
<div id="main">
{{{yield}}}
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
{{format code}}

View File

@@ -0,0 +1,19 @@
{{# this}}
<h3>{{name}}</h3>
<ul>
{{#if comment}}
<li><p>{{comment}}</p></li>
{{/if}}
{{# methods}}
<li>
{{#if ../../sourceURL}}
<h5><a href="{{../../../sourceURL}}{{../../../path}}#L{{line}}" target="_blank">{{signature}}</a></h5>
{{else}}
<h5>{{signature}}</h5>
{{/if}}
{{format comment}}
</li>
{{/ methods}}
</ul>
{{/ this}}

View File

@@ -1,99 +0,0 @@
$ = require 'jquery'
_ = require 'underscore'
File = require 'fs'
Pane = require 'pane'
jQuery = $
Modal = require 'modal'
require 'filefinder/stringscore'
module.exports =
class FilefinderPane extends Pane
html: require "filefinder/filefinder.html"
constructor: (@filefinder) ->
$('#filefinder input').live 'keydown', @onKeydown
@modal = new Modal @html
onKeydown: (e) =>
keys = up: 38, down: 40, enter: 13
if e.keyCode is keys.enter
@openSelected()
false
else if e.keyCode is keys.up
@moveUp()
else if e.keyCode is keys.down
@moveDown()
else
@filterFiles()
toggle: ->
if @modal.showing
@modal.hide()
else
@showFinder()
paths: ->
_paths = []
for dir in File.list window.path
continue if /\.git|Cocoa/.test dir
_paths.push File.listDirectoryTree dir
_.reject _.flatten(_paths), (dir) -> File.isDirectory dir
showFinder: ->
@modal.show()
@files = []
for file in @paths()
@files.push file.replace "#{window.path}/", ''
@filterFiles()
findMatchingFiles: (query) ->
return [] if not query
results = []
for file in @files
score = file.score query
if score > 0
# Basename matches count for more.
if not query.match '/'
if name.match '/'
score += name.replace(/^.*\//, '').score query
else
score *= 2
results.push [score, file]
sorted = results.sort (a, b) -> b[0] - a[0]
_.map sorted, (el) -> el[1]
filterFiles: ->
if query = $('#filefinder input').val()
files = @findMatchingFiles query
else
files = @files
$('#filefinder ul').empty()
for file in files[0..10]
$('#filefinder ul').append "<li>#{file}</li>"
$('#filefinder input').focus()
$('#filefinder li:first').addClass 'selected'
openSelected: ->
dir = window.path
file = $('#filefinder .selected').text()
window.open "#{dir}/#{file}"
@toggle()
moveUp: ->
selected = $('#filefinder .selected')
if selected.prev().length
selected.prev().addClass 'selected'
selected.removeClass 'selected'
moveDown: ->
selected = $('#filefinder .selected')
if selected.next().length
selected.next().addClass 'selected'
selected.removeClass 'selected'

View File

@@ -1,15 +1,18 @@
_ = require 'underscore'
fs = require 'fs'
Extension = require 'extension'
KeyBinder = require 'key-binder'
Event = require 'event'
FilefinderPane = require 'filefinder/filefinder-pane'
ModalSelector = require 'modal-selector'
module.exports =
class Filefinder extends Extension
constructor: ->
KeyBinder.register "filefinder", @
KeyBinder.load require.resolve "filefinder/key-bindings.coffee"
atom.keybinder.load require.resolve "filefinder/key-bindings.coffee"
atom.on 'project:open', @startup
@pane = new FilefinderPane @
startup: (@project) =>
@pane = new ModalSelector => _.reject @project.allURLs(), ({url}) ->
fs.isDirectory url
toggle: ->
@pane.toggle()
@pane?.toggle()

View File

@@ -1,65 +0,0 @@
<style>
#modal .content {
background: #ededed;
padding: 0;
}
#modal .close {
display: none;
}
#filefinder .filelist {
height: 100px;
overflow: hidden;
padding: 10px 0;
}
#filefinder input[type=search] {
width: 95%;
margin: 10px;
}
#modal .content {
min-width: 200px;
height: 100%;
background-color: #DDE3EA;
color: #000;
border-right: 1px solid #B4B4B4;
cursor: default;
-webkit-user-select: none;
overflow: auto;
}
#modal .content .cwd {
padding-top: 5px;
padding-left: 5px;
font-weight: bold;
color: #708193;
text-transform: uppercase;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
#modal .content ul {
margin: 0;
padding-top: 2px;
list-style-type: none;
}
#modal .content li {
padding: 0;
padding-left: 5px;
line-height: 20px;
font-size: 14px;
}
#modal .content li.selected {
background-image: -webkit-gradient(linear,0% 0,0% 100%,from(#BCCBEB),to(#8094BB));
border-top: 1px solid #A0AFCD;
color: #fff;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
}
</style>
<div id='filefinder'>
<input type='search'>
<br>
<ul class='filelist'>
</ul>
</div>

View File

@@ -3,32 +3,39 @@ _ = require 'underscore'
fs = require 'fs'
Extension = require 'extension'
KeyBinder = require 'key-binder'
Event = require 'event'
Watcher = require 'watcher'
ModalSelector = require 'modal-selector'
module.exports =
class Gemfile extends Extension
constructor: ->
Event.on 'extensions:loaded', @addRubyGemsDir
atom.keybinder.load require.resolve "gemfile/key-bindings.coffee"
atom.on 'project:open', @startup
addRubyGemsDir: =>
paths = window.extensions.Tree.paths
gemfile = _.detect paths, ({path}) -> /Gemfile/i.test path
startup: (@project) =>
urls = @project.urls()
gemfile = _.detect urls, ({url}) -> /Gemfile/i.test url
{url} = gemfile if gemfile
gems = @gems url if url
if gemfile
paths.push
label: "RubyGems"
path: "http://rubygems.org/"
paths: @gemsFromGemFile gemfile.path
window.extensions.Tree.reload()
if url and gems.length > 0
@project.settings.extraURLs[@project.url] = [
name: "RubyGems"
url: "http://rubygems.org/"
type: 'dir'
]
@project.settings.extraURLs["http://rubygems.org/"] = gems
@pane = new ModalSelector -> gems
gemsFromGemFile: (path) ->
file = fs.read path
toggle: ->
@pane?.toggle()
gems: (url) ->
file = fs.read url
gems = []
for line in file.split "\n"
if gem = line.match(/^\s*gem ['"](.+?)['"]/)?[1]
gems.push label: gem, path: "https://rubygems.org/gems/#{gem}"
gems.push type: 'file', name: gem, url: "https://rubygems.org/gems/#{gem}"
gems

View File

@@ -0,0 +1,2 @@
gemfile:
'cmd-ctrl-g': (gemfile) -> gemfile.toggle()

144
extensions/gist/base64.js Normal file
View File

@@ -0,0 +1,144 @@
/**
*
* Base64 encode / decode
* http://www.webtoolkit.info/
*
**/
var Base64 = {
// private property
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
// public method for encoding
encode : function (input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = Base64._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
}
return output;
},
// public method for decoding
decode : function (input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
output = Base64._utf8_decode(output);
return output;
},
// private method for UTF-8 encoding
_utf8_encode : function (string) {
string = string.replace(/\r\n/g,"\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
},
// private method for UTF-8 decoding
_utf8_decode : function (utftext) {
var string = "";
var i = 0;
var c = c1 = c2 = 0;
while ( i < utftext.length ) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
}
else if((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i+1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
}
else {
c2 = utftext.charCodeAt(i+1);
c3 = utftext.charCodeAt(i+2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
}
}
module.exports = Base64;

View File

@@ -0,0 +1,64 @@
_ = require 'underscore'
$ = require 'jquery'
fs = require 'fs'
Extension = require 'resource'
ModalSelector = require 'modal-selector'
Editor = require 'editor'
Base64 = require 'gist/base64'
module.exports =
class Gist extends Editor
window.resourceTypes.push this
open: (url) ->
return if not url
if match = url.match /^https?:\/\/gist\.github\.com\/([^\/]+)\/?/
super()
@setCode "Loading Gist..."
@url = url
@id = match[1]
$.ajax
url: "https://api.github.com/gists/#{@id}"
error: => @setCode "Loading Gist failed."
success: (data) =>
# only one file for now
@filename = _.first _.keys data.files
@metadata = _.first _.values data.files
@setModeForURL @filename
@setCode @metadata.content
true
save: ->
user = GitHub?.username
pass = GitHub?.password
if not user or not pass
console.error "Please set GitHub.username and GitHub.password to save."
return
# Can't get this to work yet. 500ing
if @id
files = {}
files[@filename] = content: @code()
$.ajax
# Needed for CORS, otherwise we send an unparseable Origin
headers: { origin: null }
url: "https://api.github.com/gists/#{@id}"
type: 'patch'
contentType: 'application/json'
data: JSON.stringify { files }
error: -> console.error "Saving Gist failed."
success: (data) =>
atom.native.writeToPasteboard @url
console.log 'it worked'
beforeSend: (req) =>
req.setRequestHeader 'Authorization', @authorization user, pass
true
# basic auth
authorization: (user, pass) ->
token = "#{user}:#{pass}"
hash = Base64.encode token
"Basic #{hash}"

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
editor:
'cmd-ctrl-p': (editor) -> window.open "markdown:#{editor.url}"

View File

@@ -0,0 +1,37 @@
fs = require 'fs'
tdoc = require 'docs/tdoc'
Browser = require 'browser'
{Showdown} = require './showdown'
converter = new Showdown.converter
module.exports =
class Markdownpreview extends Browser
window.resourceTypes.push this
running: true
constructor: ->
atom.keybinder.load require.resolve "markdownpreview/key-bindings.coffee"
open: (url) ->
return false if not url
if match = url.match /^markdown:(.+)/
@url = url
html = '''
<link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">
<style>
body { padding:10px; }
code { line-height:16px; }
</style>
'''
html += converter.makeHtml fs.read match[1]
@show html
true

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,2 @@
projectfinder:
'cmd-shift-p': (projectfinder) => projectfinder.toggle()

View File

@@ -0,0 +1,24 @@
_ = require 'underscore'
fs = require 'fs'
Extension = require 'extension'
ModalSelector = require 'modal-selector'
module.exports =
class Projectfinder extends Extension
settings:
root: "~/Code"
constructor: ->
atom.keybinder.load require.resolve "projectfinder/key-bindings.coffee"
atom.on 'window:load', @startup
startup: (@project) =>
@pane = new ModalSelector =>
_.compact _.map (fs.list @settings.root), (url) =>
return if fs.isFile url
name = url.replace "#{fs.absolute @settings.root}/", ''
{ name, url }
toggle: ->
@pane?.toggle()

View File

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

View File

@@ -0,0 +1,2 @@
showkeybindings:
'cmd-shift-k': -> window.open 'atom://keybindings'

View File

@@ -0,0 +1,41 @@
_ = require 'underscore'
$ = require 'jquery'
fs = require 'fs'
Browser = require 'browser'
module.exports =
class Showkeybindings extends Browser
window.resourceTypes.push this
constructor: ->
atom.keybinder.load require.resolve "showkeybindings/key-bindings.coffee"
@running = true
open: (url) ->
return if not url
if url is 'atom://keybindings'
@title = 'Keybindings'
@url = url
@show()
true
innerHTML: ->
html = '''
<link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">
<style>body { padding:10px; }</style>
'''
html += '<h1>Keybindings</h1>'
for name, bindings of atom.keybinder.keymaps
html += "<h3>#{name}</h3>"
html += "<ul>"
for binding, method of bindings
html += """
<li>#{atom.keybinder.bindingFromAscii(binding)} - #{method}</li>
"""
html += "</ul>"
html
show: ->
super @innerHTML()

View File

@@ -2,7 +2,6 @@ $ = require 'jquery'
_ = require 'underscore'
Pane = require 'pane'
Browser = require 'browser'
module.exports =
class TabsPane extends Pane
@@ -42,12 +41,7 @@ class TabsPane extends Pane
existing = $("#tabs [data-path='#{path}']")
return @switchToTab existing if existing.length
name = if not path
"untitled"
else if Browser.isPathUrl path
path.match(/(\w+:\/\/)([^\/]+)?/)[2]
else
_.last path.split '/'
name = _.last (path or 'untitled').split '/'
$("#tabs ul .active").removeClass()
$("#tabs ul").append """
@@ -55,6 +49,20 @@ class TabsPane extends Pane
"""
$("#tabs ul li:last").addClass 'active'
closeActiveTab: ->
tabsLength = $('#tabs ul li').length
activePath = $('#tabs ul .active').data 'path'
if tabsLength is 1
@removeTab activePath
$('#main-container').children().css 'display', 'none !important'
window.setTitle window.resource.title()
else if tabsLength > 0
@removeTab activePath
@prevTab()
else
window.close()
removeTab: (path) ->
tab = $("#tabs li[data-path='#{path}']")
if tab.hasClass("active")

View File

@@ -1,37 +1,26 @@
$ = require 'jquery'
Extension = require 'extension'
KeyBinder = require 'key-binder'
Event = require 'event'
TabsPane = require 'tabs/tabs-pane'
fs = require 'fs'
Extension = require 'extension'
TabsPane = require 'tabs/tabs-pane'
module.exports =
class Tabs extends Extension
constructor: () ->
KeyBinder.register "tabs", @
KeyBinder.load require.resolve "tabs/key-bindings.coffee"
project: null
@pane = new TabsPane @
constructor: ->
atom.keybinder.load require.resolve "tabs/key-bindings.coffee"
Event.on 'editor:bufferAdd', (e) =>
path = e.details
@pane.addTab path
atom.on 'project:open', @startup
atom.on 'project:resource:active', @focus
Event.on 'editor:bufferFocus', (e) =>
path = e.details
@pane.addTab path
Event.on 'editor:bufferRemove', (e) =>
path = e.details
@pane.removeTab path
Event.on 'browser:focus', (e) =>
path = e.details
@pane.addTab path
startup: ->
startup: (@project) =>
@pane = new TabsPane this
@pane.show()
for path, buffer of window.editor.buffers
@pane.addTab path
super
shutdown: ->
@pane.remove()
super
focus: (project, resource) =>
@pane?.addTab resource.url

View File

@@ -13,7 +13,7 @@ class TinyTest extends Extension
'Command-Ctrl-T': 'runTests'
runTests: ->
_.map File.list(@window.path + '/test'), @runTest
_.map File.list(window.url + '/test'), @runTest
runTest: (path) ->
# Even though we already have the path, run it

View File

@@ -14,53 +14,39 @@ class TreePane extends Pane
html: $ require "tree/tree.html"
constructor: (@tree) ->
@reload()
@render()
$('#tree li').live 'click', (event) =>
return true if event.__treeClicked__
$('#tree .active').removeClass 'active'
el = $(event.currentTarget)
path = decodeURIComponent el.data 'path'
url = decodeURIComponent el.data 'url'
if el.hasClass 'dir'
if el.hasClass 'open'
@tree.hideDir path
el.removeClass 'open'
el.children("ul").remove()
el.find('ul').remove()
else
@tree.showDir path
el.addClass 'open'
list = @createList @tree.findPath(path).paths
list = @createList @tree.urls url
el.append list
else
el.addClass 'active'
window.open path
window.open url
# HACK I need the event to propogate beyond the tree pane,
# but I need the tree pane to ignore it. Need somehting
# cleaner here.
event.__treeClicked__ = true
false
true
reload: ->
@html.children('#tree .cwd').text _.last window.path.split '/'
fileList = @createList @tree.paths
render: ->
@html.children('#tree .cwd').text _.last window.url.split '/'
fileList = @createList @tree.urls()
fileList.addClass 'files'
@html.children('#tree .files').replaceWith fileList
createList: (root) ->
shownDirs = @tree.shownDirs()
createList: (urls) ->
list = $('<ul>')
for {label, path, paths} in root
type = if paths then 'dir' else 'file'
encodedPath = encodeURIComponent path
listItem = $("<li class='#{type}' data-path='#{encodedPath}'>#{label}</li>")
if path in shownDirs and type is 'dir'
listItem.append @createList paths
listItem.addClass "open"
for {name, url, type} in urls
encodedURL = encodeURIComponent url
listItem = $("<li class='#{type}' data-url='#{encodedURL}'>#{name}</li>")
list.append listItem
list

View File

@@ -1,91 +1,26 @@
_ = require 'underscore'
Event = require 'event'
Extension = require 'extension'
KeyBinder = require 'key-binder'
Storage = require 'storage'
TreePane = require 'tree/tree-pane'
Watcher = require 'watcher'
fs = require 'fs'
module.exports =
class Tree extends Extension
ignorePattern: /\.git|\.xcodeproj|\.DS_Store/
# a path is an object with three keys: label, path, and paths.
# paths is an optional Array of other path objects.
paths: []
project: null
constructor: ->
KeyBinder.register "tree", @
KeyBinder.load require.resolve "tree/key-bindings.coffee"
atom.keybinder.load require.resolve "tree/key-bindings.coffee"
atom.on 'project:open', @startup
# watch the root dir
Watcher.watch window.path, @watchDir
# Hide dirs that no longer exist, watch dirs that do.
for dir in @shownDirs()
if not fs.exists dir
@hideDir dir
else
Watcher.watch dir, @watchDir
@paths = @findPaths window.path
@pane = new TreePane @
startup: ->
startup: (@project) =>
@pane = new TreePane this
@pane.show()
super
shutdown: ->
@unwatchDir window.path
@unwatchDir dir for dir in @shownDirs()
@pane.remove()
super
reload: ->
@pane.reload()
shownDirStorageKey: ->
@.constructor.name + ":" + window.path + ":shownDirs"
watchDir: (changeType, dir) =>
# Update the paths.
@paths = @findPaths window.path
@pane.reload()
unwatchDir: (dir) ->
Watcher.unwatch dir, @watchDir
shownDirs: ->
Storage.get @shownDirStorageKey(), []
showDir: (dir) ->
dirs = @shownDirs().concat dir
Storage.set @shownDirStorageKey(), dirs
Watcher.watch dir, @watchDir
hideDir: (dir) ->
dirs = _.without @shownDirs(), dir
Storage.set @shownDirStorageKey(), dirs
@unwatchDir dir, @watchDir
findPath: (searchPath, paths=@paths) ->
found = null
for obj in paths
return found if found
if obj.path is searchPath
found = obj
else if obj.paths
found = @findPath searchPath, obj.paths
found
findPaths: (root) ->
paths = []
for path in fs.list root
continue if @ignorePattern.test path
paths.push
label: _.last path.split '/'
path: path
paths: @findPaths path if fs.isDirectory path
paths
urls: (root=@project.url) ->
@project.urls root

View File

@@ -1,72 +1,16 @@
<html>
<head>
<style>
* {
margin: 0;
padding: 0;
}
body {
font-family: Lucida Grande;
font-size: 12px;
overflow: hidden;
}
#app-horizontal {
background-color: orange;
display: -webkit-box;
min-height: 100%;
-webkit-box-orient: horizontal;
}
#app-vertical {
background-color: #AFEEEE;
display: -webkit-box;
-webkit-box-flex: 1;
-webkit-box-orient: vertical;
}
.main {
-webkit-box-flex: 1;
}
.pane {
display: -webkit-box;
-webkit-box-orient: vertical;
}
.left {
background-color: gray;
}
.right {
background-color: green;
}
.top {
background-color: purple;
}
.bottom {
background-color: blue;
}
#ace-editor {
position: relative;
-webkit-box-flex: 1;
font: 18px Inconsolata, Monaco, Courier;
}
</style>
<link href="static/atom.css" media="screen" rel="stylesheet" type="text/css" />
</head>
<body>
<div id='app-horizontal'>
<div id='app-vertical'>
<div id='main-container' class='main pane'></div>
<div id='main'></div>
</div>
</div>
</body>
<script src='src/require.js'></script>
<script src='src/stdlib/require.js'></script>
<script>
window.onerror = function() {
window.showConsole(true)

View File

@@ -1,15 +0,0 @@
KeyBinder = require 'key-binder'
fs = require 'fs'
require 'window'
module.exports =
class App
@startup: ->
KeyBinder.register "app", @
window.startup()
@open: (path) ->
OSX.NSApp.open path
@quit: ->
OSX.NSApp.terminate null

7
src/atom/app.coffee Normal file
View File

@@ -0,0 +1,7 @@
module.exports =
class App
open: (url) ->
OSX.NSApp.open url
quit: ->
OSX.NSApp.terminate null

36
src/atom/browser.coffee Normal file
View File

@@ -0,0 +1,36 @@
$ = require 'jquery'
Resource = require 'resource'
module.exports =
class Browser extends Resource
window.resourceTypes.push this
url: null
open: (url) ->
return false if not /^https?:/.test url
@url = url
@show()
true
# innerHTML - Optional String to set as iframe's content.
show: (innerHTML=null) ->
style = "width:100%;height:100%;background-color:#fff;border:none"
@html = "<iframe src='#{@url}' style='#{style}'></iframe>"
super
iframe = @pane.find('iframe')[0]
if innerHTML
iframe.contentWindow.document.body.innerHTML = innerHTML
if @title
window.setTitle @title
else
window.setTitle iframe.contentWindow.document.title
$(iframe).bind 'load', (e) =>
window.setTitle e.target.contentWindow.document.title

View File

@@ -0,0 +1,41 @@
$ = require 'jquery'
_ = require 'underscore'
fs = require 'fs'
Pane = require 'pane'
ace = require 'ace/ace'
module.exports =
class EditorPane extends Pane
id: null
html: null
position: 'main'
editor: null
constructor: ->
@id = _.uniqueId 'editor-'
@html = $ "<div id='#{@id}'></div>"
@show()
@ace = ace.edit @id
# This stuff should all be grabbed from the .atomicity dir
@ace.setTheme require "ace/theme/twilight"
@ace.getSession().setUseSoftTabs true
@ace.getSession().setTabSize 2
@ace.setShowInvisibles true
@ace.setPrintMarginColumn 78
# Resize editor when panes are added/removed
el = document.body
el.addEventListener 'DOMNodeInsertedIntoDocument', => @resize()
el.addEventListener 'DOMNodeRemovedFromDocument', => @resize()
resize: (timeout=1) ->
setTimeout =>
@ace.focus()
@ace.resize()
, timeout

165
src/atom/editor.coffee Normal file
View File

@@ -0,0 +1,165 @@
$ = require 'jquery'
_ = require 'underscore'
fs = require 'fs'
Resource = require 'resource'
EditorPane = require 'editor-pane'
{EditSession} = require 'ace/edit_session'
{UndoManager} = require 'ace/undomanager'
# Events:
# editor:open (editor) -> Called when an editor is opened.
module.exports =
class Editor extends Resource
window.resourceTypes.push this
dirty: false
url: null
pane: null
modeMap:
js: 'javascript'
c: 'c_cpp'
cpp: 'c_cpp'
h: 'c_cpp'
m: 'c_cpp'
md: 'markdown'
cs: 'csharp'
rb: 'ruby'
ru: 'ruby'
gemspec: 'ruby'
modeFileMap:
Gemfile: 'ruby'
Rakefile: 'ruby'
modeForURL: (url) ->
return if not url
if not modeName = @modeFileMap[ _.last url.split '/' ]
language = _.last url.split '.'
language = language.toLowerCase()
modeName = @modeMap[language] or language
try
require("ace/mode/#{modeName}").Mode
catch e
console.error e
require("ace/mode/text").Mode
setModeForURL: (url) ->
@ace.session.setMode new (@modeForURL url)
title: ->
if @url then _.last @url.split '/' else 'untitled'
show: ->
@pane.show()
@ace.resize()
open: (url) ->
if url
return false if not fs.isFile url
return false if @url
window.setTitle @title()
@pane ?= new EditorPane
@ace = @pane.ace
@url = url
@session = new EditSession code = if @url then fs.read @url else ''
@session.setValue code
@session.setUseSoftTabs useSoftTabs = @usesSoftTabs code
@session.setTabSize if useSoftTabs then @guessTabSize code else 8
@session.setUndoManager new UndoManager
@session.on 'change', => @dirty = true
@ace.setSession @session
@show()
@setModeForURL @url if @url
@dirty = false
atom.trigger 'editor:open', this
true
close: ->
if @dirty
detailedMessage = if @url
"#{@url} has changes."
else
"An untitled file has changes."
close = atom.native.alert "Do you want to save your changes?",
detailedMessage,
"Save": => @save()
"Cancel": => false
"Don't Save": => true
return if not close
super
save: ->
return @saveAs() if not @url
@removeTrailingWhitespace()
fs.write @url, @code()
@dirty = false
@url
saveAs: ->
if url = atom.native.savePanel()?.toString()
@url = url
@save url
code: ->
@ace.getSession().getValue()
setCode: (code) ->
@ace.getSession().setValue code
removeTrailingWhitespace: ->
return
@ace.replaceAll "",
needle: "[ \t]+$"
regExp: true
wrap: true
usesSoftTabs: (code) ->
not /^\t/m.test code or @code()
guessTabSize: (code) ->
# * ignores indentation of css/js block comments
match = /^( +)[^*]/im.exec code || @code()
match?[1].length or 2
copy: ->
text = @ace.getSession().doc.getTextRange @ace.getSelectionRange()
atom.native.writeToPasteboard text
cut: ->
text = @ace.getSession().doc.getTextRange @ace.getSelectionRange()
atom.native.writeToPasteboard text
@ace.session.remove @ace.getSelectionRange()
eval: -> eval @code()
toggleComment: -> @ace.toggleCommentLines()
outdent: -> @ace.blockOutdent()
indent: -> @ace.indent()
forwardWord: -> @ace.navigateWordRight()
backWord: -> @ace.navigateWordLeft()
deleteWord: -> @ace.removeWordRight()
home: -> @ace.navigateFileStart()
end: -> @ace.navigateFileEnd()
wordWrap: ->
@ace.getSession().setUseWrapMode true
consolelog: ->
@ace.insert 'console.log ""'
@ace.navigateLeft()

View File

@@ -0,0 +1,23 @@
fs = require 'fs'
module.exports =
class ExtensionManager
constructor: ->
@loadExtensions()
atom.on 'window:close', @unloadExtensions
loadExtensions: =>
extension.shutdown() for name, extension of atom.extensions
atom.extensions = {}
extensionPaths = fs.list require.resourcePath + "/extensions"
for extensionPath in extensionPaths when fs.isDirectory extensionPath
try
extension = require extensionPath
atom.extensions[extension.name] = new extension
catch error
console.warn "Loading Extension '#{fs.base extensionPath}' failed."
console.warn error
unloadExtensions: =>
extension.shutdown() for name, extension of atom.extensions

15
src/atom/extension.coffee Normal file
View File

@@ -0,0 +1,15 @@
# Extension subclasses must call super in all overridden methods.
module.exports =
class Extension
running: false
# `startup` should be called by you in Extension subclasses when they need
# to appear on the screen, attach themselves to a Resource, or otherwise become active.
startup: ->
@running = true
# `shutdown` shuold be called by you in Extension subclasses when they need
# to be remove from the screen, unattach themselves from a Resource, or otherwise become
# inactive.
shutdown: ->
@running = false

View File

@@ -1,14 +1,15 @@
app:
'cmd-q': (app) -> app.quit()
'cmd-o': (app) -> app.open()
'cmd-n': (app) -> atom.native.newWindow()
window:
'cmd-shift-i': (window) -> window.showConsole()
'cmd-shift-I': (window) -> window.showConsole()
'cmd-r': (window) -> window.reload()
'cmd-o': (window) -> window.open()
'cmd-s': (window) -> window.save()
editor:
'cmd-w': 'removeBuffer'
'cmd-s': 'save'
'cmd-w': 'close'
'cmd-shift-s': 'saveAs'
'cmd-c': 'copy'
'cmd-x': 'cut'

View File

@@ -0,0 +1,167 @@
$ = require 'jquery'
_ = require 'underscore'
Modal = require 'modal'
jQuery = $
require 'stringscore'
module.exports =
class ModalSelector extends Modal
selectorHTML: '''
<div id="modal-selector">
<input type="search">
<br>
<ul class="list">
</ul>
</div>
'''
showing: false
# A callback which should return the items to filter.
# Return should be an Array of {name, url} objects.
filterCallback: -> []
constructor: (@filterCallback) ->
super @selectorHTML
head = $('head')[0]
style = document.createElement 'style'
rules = document.createTextNode @selectorCSS
style.type = 'text/css'
style.appendChild rules
head.appendChild style
onKeydown: (e) =>
keys = up: 38, down: 40, enter: 13
if e.keyCode is keys.enter
@openSelected()
false
else if e.keyCode is keys.up
@moveUp()
else if e.keyCode is keys.down
@moveDown()
else
@filter()
show: ->
super
@list = @filterCallback()
@filter()
$('#modal-selector input').live 'keydown.modal-selector', @onKeydown
hide: ->
super
$(document).unbind '.modal-selector'
filter: ->
if query = $('#modal-selector input').val()
items = @findMatchingItems query
else
items = @list
$('#modal-selector ul').empty()
for {name} in items[0..10]
$('#modal-selector ul').append "<li>#{name}</li>"
$('#modal-selector input').focus()
$('#modal-selector li:first').addClass 'selected'
findMatchingItems: (query) ->
return [] if not query
results = []
for item in @list
{name, url} = item
score = name.score query
if score > 0
# Basename matches count for more.
if not query.match '/'
if name.match '/'
score += name.replace(/^.*\//, '').score query
else
score *= 2
results.push [score, item]
sorted = results.sort (a, b) -> b[0] - a[0]
_.map sorted, (el) -> el[1]
openSelected: ->
text = $('#modal-selector .selected').text()
{url} = _.find @list, (item) -> item.name is text
if typeof url is 'function'
url()
else
window.open url
@toggle()
moveUp: ->
selected = $('#modal-selector .selected')
if selected.prev().length
selected.prev().addClass 'selected'
selected.removeClass 'selected'
moveDown: ->
selected = $('#modal-selector .selected')
if selected.next().length
selected.next().addClass 'selected'
selected.removeClass 'selected'
selectorCSS: '''
#modal .content {
background: #ededed;
padding: 0;
}
#modal .close {
display: none;
}
#modal-selector .list {
height: 100px;
overflow: hidden;
padding: 10px 0;
}
#modal-selector input[type=search] {
width: 95%;
margin: 10px;
}
#modal .content {
min-width: 200px;
height: 100%;
background-color: #DDE3EA;
color: #000;
border-right: 1px solid #B4B4B4;
cursor: default;
-webkit-user-select: none;
overflow: auto;
}
#modal .content .cwd {
padding-top: 5px;
padding-left: 5px;
font-weight: bold;
color: #708193;
text-transform: uppercase;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
#modal .content ul {
margin: 0;
padding-top: 2px;
list-style-type: none;
}
#modal .content li {
padding: 0;
padding-left: 5px;
line-height: 20px;
font-size: 14px;
}
#modal .content li.selected {
background-image: -webkit-gradient(linear,0% 0,0% 100%,from(#BCCBEB),to(#8094BB));
border-top: 1px solid #A0AFCD;
color: #fff;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
}
'''

View File

@@ -1,10 +1,18 @@
$ = require 'jquery'
Event = require 'event'
# Events:
# modal:show (modal) -> Called when the modal is displayed on screen.
# modal:hide (modal) -> Called when the modal hides.
module.exports =
class Modal
template: '<div id="modal"><div class="popup"><div class="content"></div><a href="#" class="close inline">&nbsp;</a></div></div>'
template: '''
<div id="modal">
<div class="popup">
<div class="content"></div>
<a href="#" class="close inline">&nbsp;</a>
</div>
</div>
'''
showing: false
@@ -16,13 +24,13 @@ class Modal
style.appendChild rules
head.appendChild style
$(window).bind 'resize.modal', => @resize()
show: ->
@showing = true
$('body').append('<div id="modal-overlay"></div>')
$(window).bind 'resize.modal', => @resize()
$(document).bind 'keydown.modal', (e) =>
if e.keyCode is (esc = 27) then @hide(); false
@@ -37,7 +45,7 @@ class Modal
$('#modal input').focus()
@resize()
Event.trigger 'modal:show', @
atom.trigger 'modal:show', this
resize: ->
$('#modal').css
@@ -50,10 +58,14 @@ class Modal
$('#modal-overlay').fadeOut 200, -> $(this).remove()
$(document).unbind '.modal'
$(window).unbind '.modal'
Event.trigger 'modal:hide', @
atom.trigger 'modal:hide', this
toggle: ->
if @showing then @hide() else @show()
if @showing
@hide()
else
@show()
css: """
#modal {

View File

@@ -16,10 +16,8 @@ class Pane
switch @position
when 'main'
# ICK: There can be multiple 'main' panes, but only one can be active
# at at time.
$('#main-container').children().css 'display', 'none !important'
$('#main-container').append @pane
$('#main > div').addClass 'hidden'
$('#main').append @pane
when 'top'
verticalDiv.prepend @pane
when 'left'
@@ -29,19 +27,20 @@ class Pane
when 'right'
horizontalDiv.append @pane
else
throw "I DON'T KNOW HOW TO DEAL WITH #{@position}"
throw "pane position of #{this} can't be `#{@position}`"
showing: ->
@pane and not @pane.css('display').match /none/
@pane and not @pane.hasClass 'hidden'
show: ->
if @position == 'main'
$('#main-container').children().css 'display', 'none !important'
if not @pane then @add() else @pane.css 'display', '-webkit-box !important'
if not @pane
@add()
else
$('#main > div').addClass 'hidden' if @position == 'main'
@pane.removeClass 'hidden'
hide: ->
@pane.css 'display', 'none !important'
@pane.addClass 'hidden'
toggle: ->
if @showing()

97
src/atom/project.coffee Normal file
View File

@@ -0,0 +1,97 @@
$ = require 'jquery'
_ = require 'underscore'
fs = require 'fs'
Resource = require 'resource'
# Events:
# project:open (project) -> Called when a project is opened.
# project:resource:open (project, resource) ->
# Called when the project opens a resource.
# project:resource:active (project, resource) ->
# Called when a resource becomes active (i.e. the focal point)
# in a project.
module.exports =
class Project extends Resource
window.resourceTypes.push this
settings:
# Regexp used to ignore paths.
ignorePattern: /(\.git|\.xcodeproj|\.DS_Store|node_modules)/
# Arrays of { name, url, type } keyed by the root URL.
# Used when looking up a directory's contents by url
# to add metadata such as magic files or directories.
extraURLs: {}
resources: {}
activeResource: null
responder: ->
@activeResource or this
open: (url) ->
if not @url
# Can only open directories.
return false if not fs.isDirectory url
@url = url
window.setTitle @title()
atom.trigger 'project:open', this
true
else if @url
# Can't open directories once we have a URL.
if fs.isDirectory url
return false
# Ignore non-children files
if (fs.isFile url) and not @childURL url
return false
# Is this resource already open?
if @resources[url]
@activeResource = @resources[url]
atom.trigger 'project:resource:active', this, @activeResource
@activeResource.show()
true
else
# Try to open all others
for resourceType in window.resourceTypes
resource = new resourceType
break if success = resource.open url
if success
@resources[url] = @activeResource = resource
atom.trigger 'project:resource:open', this, resource
atom.trigger 'project:resource:active', this, resource
true
save: ->
@activeResource?.save()
title: ->
_.last @url.split '/'
# Determines if a passed URL is a child of @url.
# Returns a Boolean.
childURL: (url) ->
return false if not @url
parent = @url.replace /([^\/])$/, "$1/"
child = url.replace /([^\/])$/, "$1/"
child.match "^" + parent
urls: (root=@url) ->
_.compact _.map (fs.list root), (url) =>
return if @settings.ignorePattern.test url
type: if fs.isDirectory url then 'dir' else 'file'
name: url.replace(root, "").substring 1
url: url
.concat @settings.extraURLs[root] or []
# WARNING THIS IS PROBABLY SLOW
allURLs: ->
_.compact _.map (fs.listDirectoryTree @url), (url) =>
name = url.replace "#{window.url}/", ''
return if @settings.ignorePattern.test name
{ name, url }

19
src/atom/resource.coffee Normal file
View File

@@ -0,0 +1,19 @@
_ = require 'underscore'
Pane = require 'pane'
# When subclassing, call super() at the end of your
# constructor.
module.exports =
class Resource extends Pane
position: "main"
url: null
constructor: ->
# Can be used to delegate key events to another object, such as a pane.
responder: -> this
close: ->
window.close()
save: ->

55
src/atom/window.coffee Normal file
View File

@@ -0,0 +1,55 @@
fs = require 'fs'
_ = require 'underscore'
# This a weirdo file. We don't create a Window class, we just add stuff to
# the DOM window.
#
# Events:
# window:load - Same as window.onLoad. Final event of app startup.
windowAdditions =
resourceTypes: []
url: $atomController.url?.toString()
startup: ->
atom.trigger 'window:load', this
success = false
for resourceType in @resourceTypes.reverse()
@resource = new resourceType
break if success = @resource.open @url
throw "I DON'T KNOW ABOUT #{@url}" if not success
shutdown: ->
showConsole: ->
$atomController.webView.inspector.showConsole true
setTitle: (title) ->
$atomController.window.title = title
reload: ->
@close()
OSX.NSApp.createController @url
open: (url) ->
url = atom.native.openPanel() unless url
(@resource.open url) or atom.app.open url
save: ->
@resource.save()
close: (url) ->
@shutdown()
$atomController.close
handleKeyEvent: ->
atom.keybinder.handleEvent arguments...
triggerEvent: ->
atom.trigger arguments...
for key, value of windowAdditions
console.warn "DOMWindow already has a key named `#{key}`" if window[key]
window[key] = value

View File

@@ -1,28 +0,0 @@
$ = require 'jquery'
Event = require 'event'
Pane = require 'pane'
module.exports =
class Browser extends Pane
buffers: {}
html: $ "<div id='browser'></div>"
position: 'main'
@isPathUrl: (path) ->
/^https?:\/\//.test path
constructor: ->
Event.on "window:open", (e) =>
path = e.details
return unless @constructor.isPathUrl path
@buffers[path] ?= $ "<iframe src='#{path}' style='width:100%;height:100%'></iframe>"
@html.html @buffers[path]
@show()
Event.trigger "browser:focus", path

View File

@@ -1,232 +0,0 @@
$ = require 'jquery'
_ = require 'underscore'
fs = require 'fs'
ace = require 'ace/ace'
Event = require 'event'
KeyBinder = require 'key-binder'
Native = require 'native'
Storage = require 'storage'
Pane = require 'pane'
{EditSession} = require 'ace/edit_session'
{UndoManager} = require 'ace/undomanager'
module.exports =
class Editor extends Pane
activePath: null
buffers: {}
openPathsKey: "editor.openPaths.#{window.path}"
focusedPathKey: "editor.focusedPath.#{window.path}"
html: $ "<div id='ace-editor'></div>"
position: "main"
constructor: ->
KeyBinder.register "editor", @
@show()
@ace = ace.edit 'ace-editor'
# This stuff should all be grabbed from the .atomicity dir
@ace.setTheme require "ace/theme/twilight"
@ace.getSession().setUseSoftTabs true
@ace.getSession().setTabSize 2
@ace.setShowInvisibles(true)
@ace.setPrintMarginColumn 78
Event.on 'window:open', (e) =>
path = e.details
@addBuffer e.details if fs.isFile path
Event.on 'window:close', (e) => @removeBuffer e.details
Event.on 'editor:bufferFocus', (e) => @resize()
# Resize editor when panes are added/removed
el = document.body
el.addEventListener 'DOMNodeInsertedIntoDocument', => @resize()
el.addEventListener 'DOMNodeRemovedFromDocument', => @resize()
modeMap:
js: 'javascript'
c: 'c_cpp'
cpp: 'c_cpp'
h: 'c_cpp'
m: 'c_cpp'
md: 'markdown'
cs: 'csharp'
rb: 'ruby'
ru: 'ruby'
gemspec: 'ruby'
modeFileMap:
Gemfile: 'ruby'
Rakefile: 'ruby'
modeForPath: (path) ->
return null if not path
if not modeName = @modeFileMap[ _.last path.split '/' ]
language = _.last path.split '.'
language = language.toLowerCase()
modeName = @modeMap[language] or language
try
require("ace/mode/#{modeName}").Mode
catch e
null
restoreOpenBuffers: ->
openPaths = Storage.get @openPathsKey, []
focusedPath = Storage.get(@focusedPathKey)
@addBuffer path for path in openPaths
@focusBuffer focusedPath if focusedPath
addBuffer: (path) ->
throw "#{@constructor.name}: Cannot create buffer from a directory `#{path}`" if fs.isDirectory path
buffer = @buffers[path]
if not buffer
code = if path then fs.read path else ''
buffer = new EditSession code
buffer.setUndoManager new UndoManager
buffer.setUseSoftTabs useSoftTabs = @usesSoftTabs code
buffer.setTabSize if useSoftTabs then @guessTabSize code else 8
mode = @modeForPath path
buffer.setMode new mode if mode
@buffers[path] = buffer
openPaths = Storage.get @openPathsKey, []
unless path in openPaths
openPaths.push path
Storage.set @openPathsKey, openPaths
buffer.on 'change', -> buffer.$atom_dirty = true
Event.trigger "editor:bufferAdd", path
@focusBuffer path
removeBuffer: (path) ->
path ?= @activePath
return if not path
buffer = @buffers[path]
return if not buffer
if buffer.$atom_dirty
# This should be thrown into it's own method, but I can't think of a good
# name.
detailedMessage = if @activePath
"#{@activePath} has changes."
else
"An untitled file has changes."
canceled = Native.alert "Do you want to save the changes you made?", detailedMessage,
"Save": =>
path = @save()
not path # if save modal fails/cancels, consider it canceled
"Cancel": => true
"Don't Save": => false
return if canceled
delete @buffers[path]
openPaths = Storage.get @openPathsKey, []
Storage.set @openPathsKey, _.without openPaths, path
Event.trigger "editor:bufferRemove", path
if path is @activePath
newActivePath = Object.keys(@buffers)[0]
if newActivePath
@focusBuffer newActivePath
else
@ace.setSession new EditSession ''
focusBuffer: (path) ->
return if not path
@show()
@activePath = path
buffer = @buffers[path] or @addBuffer path
@ace.setSession buffer
Storage.set @focusedPathKey, path
Event.trigger "editor:bufferFocus", path
save: (path) ->
path ?= @activePath
return @saveAs() if not path
@removeTrailingWhitespace()
fs.write path, @code()
if @buffers[path]
@buffers[path].$atom_dirty = false
path
saveAs: ->
path = Native.savePanel()?.toString()
if path
@save path
@addBuffer path
path
code: ->
@ace.getSession().getValue()
removeTrailingWhitespace: ->
return
@ace.replaceAll "",
needle: "[ \t]+$"
regExp: true
wrap: true
usesSoftTabs: (code) ->
not /^\t/m.test code or @code()
guessTabSize: (code) ->
# * ignores indentation of css/js block comments
match = /^( +)[^*]/im.exec code || @code()
match?[1].length or 2
resize: (timeout=1) ->
setTimeout =>
@ace.focus()
@ace.resize()
, timeout
copy: ->
text = @ace.getSession().doc.getTextRange @ace.getSelectionRange()
Native.writeToPasteboard text
cut: ->
text = @ace.getSession().doc.getTextRange @ace.getSelectionRange()
Native.writeToPasteboard text
@ace.session.remove @ace.getSelectionRange()
eval: -> eval @code()
toggleComment: -> @ace.toggleCommentLines()
outdent: -> @ace.blockOutdent()
indent: -> @ace.indent()
forwardWord: -> @ace.navigateWordRight()
backWord: -> @ace.navigateWordLeft()
deleteWord: -> @ace.removeWordRight()
home: -> @ace.navigateFileStart()
end: -> @ace.navigateFileEnd()
consolelog: ->
@ace.insert 'console.log ""'
@ace.navigateLeft()

View File

@@ -1,28 +0,0 @@
# Using the DOM event system, and copying the JQuery Event API
# https://developer.mozilla.org/en/DOM/Creating_and_triggering_events
module.exports =
class Event
@events: {}
@on: (name, callback) ->
window.document.addEventListener name, callback
callback
@off: (name, callback) ->
window.document.removeEventListener name, callback
@trigger: (name, data, bubbleToApp=true) ->
if bubbleToApp and name.match /^app:/
OSX.NSApp.triggerGlobalEvent_data name, data
return
event = @events[name]
if not event
event = window.document.createEvent "CustomEvent"
event.initCustomEvent name, true, true, null
@events[name] = event
event.details = data
window.document.dispatchEvent event
null

View File

@@ -1,15 +0,0 @@
KeyBinder = require 'key-binder'
fs = require 'fs'
module.exports =
class Extension
pane: null
constructor: ->
console.log "#{@constructor.name}: Loaded"
storageNamespace: -> @constructor.name
startup: ->
shutdown: ->

View File

@@ -1,147 +0,0 @@
_ = require 'underscore'
fs = require 'fs'
Watcher = require 'watcher'
{CoffeeScript} = require 'coffee-script'
module.exports =
class KeyBinder
@bindings: {}
@scopes: {}
@register: (name, scope) ->
@scopes[name] = scope
@load: (path) ->
try
Watcher.watch path, =>
# Should we keep track of which file bindings are associated with? That
# way we could clear bindings when the file is changed or deleted. I
# think the answer is yes, but I don't want to do this right now.
console.log "#{@name}: Reloading #{path}"
@load path
json = CoffeeScript.eval "return " + (fs.read path)
for scopeName, bindings of json
@create scopeName, binding, method for binding, method of bindings
catch error
console.error "#{@name}: Could not load key bindings at `#{path}`. #{error}"
@create: (scope, binding, method) ->
if typeof scope is "string"
throw "#{@name}: Unknown scope `#{scope}`" unless @scopes[scope]
scope = @scopes[scope]
callback = if _.isFunction method
-> method scope
else if scope[method]
-> scope[method]()
else
throw "#{@name}: '#{method}' not found found in scope #{scope}"
callbacks = @bindings[@bindingParser binding] ?= []
callbacks.push callback
@handleEvent: (event) ->
keys = []
keys.push @modifierKeys.command if event.modifierFlags & OSX.NSCommandKeyMask
keys.push @modifierKeys.shift if event.modifierFlags & OSX.NSShiftKeyMask
keys.push @modifierKeys.control if event.modifierFlags & OSX.NSControlKeyMask
keys.push @modifierKeys.alt if event.modifierFlags & OSX.NSAlternateKeyMask
keys.push event.charactersIgnoringModifiers.toLowerCase().charCodeAt 0
binding = keys.sort().join "-"
callbacks = @bindings[binding]
return false if not callbacks
# Only use the most recently added binding
try
_.last(callbacks)()
catch e
console.warn "Failed to run binding #{@bindingFromAscii binding}. #{e}"
true
@bindingParser: (binding) ->
keys = binding.trim().split '-'
modifiers = []
key = null
for k in keys
k = k.toLowerCase()
if @modifierKeys[k]
modifiers.push @modifierKeys[k]
else if key
throw "#{@name}: #{binding} specifies TWO keys, we don't handle that yet."
else if @namedKeys[k]
key = @namedKeys[k]
else if k.length > 1
throw "#{@name}: #{binding} uses an unknown key #{k}."
else
key = k.charCodeAt 0
modifiers.concat(key).sort().join "-"
@bindingFromAscii: (binding) ->
inverseModifierKeys = {}
inverseModifierKeys[number] = label for label, number of @modifierKeys
inverseNamedKeys = {}
inverseNamedKeys[number] = label for label, number of @namedKeys
asciiKeys = binding.split '-'
keys = []
for asciiKey in asciiKeys
key = inverseModifierKeys[asciiKey]
key ?= inverseNamedKeys[asciiKey]
key ?= String.fromCharCode asciiKey
keys.push key or "?"
keys.join '-'
@modifierKeys:
'': 16
'': 91
'': 18
shift: 16
alt: 18
option: 18
control: 17
ctrl: 17
command: 91
cmd: 91
@namedKeys:
backspace: 8
tab: 9
clear: 12
enter: 13
return: 13
esc: 27
escape: 27
space: 32
left: 37
up: 38
right: 39
down: 40
del: 46
delete: 46
home: 36
end: 35
pageup: 33
pagedown: 34
',': 188
'.': 190
'/': 191
'`': 192
'-': 189
'=': 187
';': 186
'\'': 222
'[': 219
']': 221
'\\': 220

View File

@@ -1,3 +1,33 @@
# Like sands through the hourglass, so are the days of our lives.
require 'window'
window.atom = {}
App = require 'app'
App.startup()
Browser = require 'browser'
Editor = require 'editor'
Event = require 'event'
ExtensionManager = require 'extension-manager'
KeyBinder = require 'key-binder'
Native = require 'native'
Project = require 'project'
Resource = require 'resource'
Settings = require 'settings'
Storage = require 'storage'
atom.event = new Event
# atom.on, atom.off, etc.
for name, method of atom.event
atom[name] = atom.event[name]
atom.native = new Native
atom.storage = new Storage
atom.keybinder = new KeyBinder
atom.settings = new Settings
atom.extensions = {}
atom.extensionManager = new ExtensionManager
atom.app = new App
window.startup()

20
src/stdlib/event.coffee Normal file
View File

@@ -0,0 +1,20 @@
_ = require 'underscore'
module.exports =
class Event
events: {}
on: (name, callback) ->
@events[name] ?= []
@events[name].push callback
off: (name, callback) ->
delete @events[name][_.indexOf callback] if @events[name]
trigger: (name, data...) ->
if name.match /^app:/
OSX.NSApp.triggerGlobalEvent_data name, data
return
_.each @events[name], (callback) => callback data...
null

View File

@@ -0,0 +1,166 @@
_ = require 'underscore'
fs = require 'fs'
Watcher = require 'watcher'
{CoffeeScript} = require 'coffee-script'
module.exports =
class KeyBinder
# keymaps are name => { binding: method } mappings
keymaps: {}
constructor: ->
@load require.resolve "key-bindings.coffee"
if fs.isFile "~/.atomicity/key-bindings.coffee"
@load "~/.atomicity/key-bindings.coffee"
register: (name, scope) ->
load: (path) ->
try
# Watcher.watch path, =>
# @load path
json = CoffeeScript.eval "return " + (fs.read path)
# Iterate in reverse order scopes are declared.
# Scope at the top of the file is checked last.
for name in _.keys(json).reverse()
bindings = json[name]
@keymaps[name] ?= {}
for binding, method of bindings
@keymaps[name][@bindingParser binding] = method
catch error
console.error "Can't load key bindings at `#{path}`."
console.error error
handleEvent: (event) ->
keys = []
if event.modifierFlags & OSX.NSCommandKeyMask
keys.push @modifierKeys.command
if event.modifierFlags & OSX.NSControlKeyMask
keys.push @modifierKeys.control
if event.modifierFlags & OSX.NSAlternateKeyMask
keys.push @modifierKeys.alt
if event.modifierFlags & OSX.NSShiftKeyMask
keys.push @modifierKeys.shift
keys.push event.charactersIgnoringModifiers.toLowerCase().charCodeAt 0
binding = keys.sort().join "-"
try
@triggerBinding binding
catch error
console.error "Failed to run binding #{@bindingFromAscii binding}."
console.error error
# Given a keyboard combination, goes through the responder
# chain and checks if any object (or any of that object's super
# classes) respond to the binding.
#
# If so, it triggers the binding.
#
# binding - A String in the form of "#{charCode}-#{chardCode}"
#
# Returns true if we found and triggered the binding, false if not.
triggerBinding: (binding) ->
for responder in @responders()
name = responder.constructor.name?.toLowerCase()
name = 'window' if responder is window
if method = @keymaps[name]?[binding]
if _.isFunction method
method responder
else
responder[method]()
return true
false
responders: ->
extensions = _.select (_.values atom.extensions), (extension) ->
extension.running?
_.flatten [ extensions, window.resource.responder(), window, atom.app ]
bindingParser: (binding) ->
keys = binding.trim().split '-'
modifiers = []
key = null
for k in keys
if modifier = @modifierKeys[k.toLowerCase()]
modifiers.push modifier
else if key
throw "#{@name}: #{binding} specifies TWO keys, we don't handle that yet."
else if namedKey = @namedKeys[k.toLowerCase()]
key = namedKey
else if k.toLowerCase() isnt k
if not _.include modifiers, @modifierKeys.shift
modifiers.push @modifierKeys.shift
key = k.toLowerCase().charCodeAt 0
else if k.length > 1
throw "#{@name}: #{binding} uses an unknown key #{k}."
else
charCode = k.charCodeAt 0
key = k.charCodeAt 0
modifiers.concat(key).sort().join "-"
bindingFromAscii: (binding) ->
inverseModifierKeys = {}
inverseModifierKeys[number] = label for label, number of @modifierKeys
inverseNamedKeys = {}
inverseNamedKeys[number] = label for label, number of @namedKeys
asciiKeys = binding.split '-'
keys = []
for asciiKey in asciiKeys.reverse()
key = inverseModifierKeys[asciiKey]
key ?= inverseNamedKeys[asciiKey]
key ?= String.fromCharCode asciiKey
keys.push key or "?"
keys.join '-'
modifierKeys:
'': 16
'': 91
'': 18
shift: 16
alt: 18
option: 18
control: 17
ctrl: 17
command: 91
cmd: 91
namedKeys:
backspace: 8
tab: 9
clear: 12
enter: 13
return: 13
esc: 27
escape: 27
space: 32
left: OSX.NSLeftArrowFunctionKey
up: OSX.NSUpArrowFunctionKey
right: OSX.NSRightArrowFunctionKey
down: OSX.NSDownArrowFunctionKey
del: 46
delete: 46
home: 36
end: 35
pageup: 33
pagedown: 34
',': 188
'.': 190
'/': 191
'`': 192
'-': 189
'=': 187
';': 186
'\'': 222
'[': 219
']': 221
'\\': 220

View File

@@ -1,6 +1,6 @@
module.exports =
class Native
@alert: (message, detailedMessage, buttons) ->
alert: (message, detailedMessage, buttons) ->
alert = OSX.NSAlert.alloc.init
alert.setMessageText message
alert.setInformativeText detailedMessage
@@ -13,13 +13,13 @@ class Native
return callbacks[buttonTag]()
# path - Optional. The String path to the file to base it on.
@newWindow: (path) ->
newWindow: (path) ->
controller = OSX.NSApp.createController path
controller.window
controller.window.makeKeyAndOrderFront null
# Returns null or a file path.
@openPanel: ->
openPanel: ->
panel = OSX.NSOpenPanel.openPanel
panel.setCanChooseDirectories true
if panel.runModal isnt OSX.NSFileHandlingPanelOKButton
@@ -28,19 +28,18 @@ class Native
localStorage.lastOpenedPath = filename
filename.toString()
@openURL: (url) ->
openURL: (url) ->
window.location = url
App = require 'app'
App.activeWindow.setTitle _.last url.replace(/\/$/,'').split '/'
atom.app.activeWindow.setTitle _.last url.replace(/\/$/,'').split '/'
# Returns null or a file path.
@savePanel: ->
savePanel: ->
panel = OSX.NSSavePanel.savePanel
if panel.runModal isnt OSX.NSFileHandlingPanelOKButton
return null
panel.filenames.lastObject
@writeToPasteboard: (text) ->
writeToPasteboard: (text) ->
pb = OSX.NSPasteboard.generalPasteboard
pb.declareTypes_owner [OSX.NSStringPboardType], null
pb.setString_forType text, OSX.NSStringPboardType

15
src/stdlib/path.coffee Normal file
View File

@@ -0,0 +1,15 @@
# node.js path module
# http://nodejs.org/docs/v0.6.0/api/path.html
_ = require 'underscore'
module.exports =
# Return the last portion of a path. Similar to the Unix basename command.
basename: (filepath) ->
_.last filepath.split '/'
# Return the extension of the path, from the last '.' to end of string in
# the last portion of the path. If there is no '.' in the last portion of
# the path or the first character of it is '.', then it returns an empty string.
extname: (filepath) ->
_.last filepath.split '.'

View File

@@ -7,6 +7,8 @@ else
resourcePath = OSX.NSBundle.mainBundle.resourcePath
paths = [
"#{resourcePath}/src/stdlib"
"#{resourcePath}/src/atom"
"#{resourcePath}/src"
"#{resourcePath}/extensions"
"#{resourcePath}/vendor"

View File

@@ -0,0 +1,8 @@
fs = require 'fs'
module.exports =
class Settings
constructor: ->
atom.on 'window:load', ->
if fs.isFile "~/.atomicity/settings.coffee"
require "~/.atomicity/settings.coffee"

View File

@@ -1,6 +1,6 @@
module.exports =
class Storage
@get: (key, defaultValue) ->
get: (key, defaultValue) ->
try
value = OSX.NSApp.storageGet_defaultValue(key, defaultValue)
@toJS value
@@ -8,10 +8,10 @@ class Storage
error.message += "\nGetting #{key}"
console.error(error)
@set: (key, value) ->
set: (key, value) ->
OSX.NSApp.storageSet_value key, value
@toJS: (value) ->
toJS: (value) ->
if not value
value
else if value.isKindOfClass OSX.NSDictionary.class

View File

@@ -1,126 +0,0 @@
Browser = require 'browser'
Editor = require 'editor'
Extension = require 'extension'
Event = require 'event'
KeyBinder = require 'key-binder'
Native = require 'native'
Storage = require 'storage'
fs = require 'fs'
_ = require 'underscore'
# This a weirdo file. We don't create a Window class, we just add stuff to
# the DOM window.
windowAdditions =
editor: null
browser: null
extensions: {}
appRoot: OSX.NSBundle.mainBundle.resourcePath
path: null
startup: () ->
KeyBinder.register "window", window
@path = $atomController.path
@setTitle _.last @path.split '/'
# Remember sizing!
defaultFrame = x: 0, y: 0, width: 600, height: 800
frame = Storage.get "window.frame.#{@path}", defaultFrame
rect = OSX.CGRectMake(frame.x, frame.y, frame.width, frame.height)
$atomController.window.setFrame_display rect, true
@editor = new Editor
@browser = new Browser
@loadExtensions()
@loadKeyBindings()
@loadSettings()
@editor.restoreOpenBuffers()
$atomController.window.makeKeyWindow
shutdown: ->
extension.shutdown() for name, extension of @extensions
frame = $atomController.window.frame
x = frame.origin.x
y = frame.origin.y
width = frame.size.width
height = frame.size.height
Storage.set "window.frame.#{@path}", {x:x, y:y, width:width, height:height}
loadExtensions: ->
extension.shutdown() for name, extension of @extensions
@extensions = {}
extensionPaths = fs.list require.resourcePath + "/extensions"
for extensionPath in extensionPaths when fs.isDirectory extensionPath
try
extension = require extensionPath
@extensions[extension.name] = new extension
catch error
console.warn "window: Loading Extension '#{fs.base extensionPath}' failed."
console.warn error
# After all the extensions are created, start them up.
for name, extension of @extensions
try
extension.startup()
catch error
console.warn "window: Extension #{extension.constructor.name} failed to startup."
console.warn error
Event.trigger 'extensions:loaded'
loadKeyBindings: ->
KeyBinder.load "#{@appRoot}/static/key-bindings.coffee"
if fs.isFile "~/.atomicity/key-bindings.coffee"
KeyBinder.load "~/.atomicity/key-bindings.coffee"
loadSettings: ->
if fs.isFile "~/.atomicity/settings.coffee"
require "~/.atomicity/settings.coffee"
showConsole: ->
$atomController.webView.inspector.showConsole true
setTitle: (title) ->
$atomController.window.title = title
reload: ->
@shutdown()
$atomController.close
OSX.NSApp.createController @path
open: (path) ->
$atomController.window.makeKeyAndOrderFront $atomController
Event.trigger 'window:open', path
close: (path) ->
@shutdown()
$atomController.close
Event.trigger 'window:close', path
handleKeyEvent: ->
KeyBinder.handleEvent.apply KeyBinder, arguments
triggerEvent: ->
Event.trigger arguments...
canOpen: (path) ->
parent = @path.replace(/([^\/])$/, "$1/")
child = path.replace(/([^\/])$/, "$1/")
# If the child is contained by the parent, it can be opened by this window
child.match "^" + parent
for key, value of windowAdditions
console.warn "DOMWindow already has a key named `#{key}`" if window[key]
window[key] = value

64
static/atom.css Normal file
View File

@@ -0,0 +1,64 @@
* {
margin: 0;
padding: 0;
}
body {
font-family: Lucida Grande;
font-size: 12px;
overflow: hidden;
}
#app-horizontal {
background-image: url(images/linen.png);
display: -webkit-box;
min-height: 100%;
-webkit-box-orient: horizontal;
}
#app-vertical {
display: -webkit-box;
-webkit-box-flex: 1;
-webkit-box-orient: vertical;
}
#main {
display: -webkit-box;
-webkit-box-flex: 1;
-webkit-box-orient: vertical;
}
.main {
-webkit-box-flex: 1;
}
.pane.hidden {
display: none;
}
.pane {
display: -webkit-box;
-webkit-box-orient: vertical;
}
.left {
background-color: gray;
}
.right {
background-color: green;
}
.top {
background-color: purple;
}
.bottom {
background-color: blue;
}
.ace_editor {
position: relative !important;
font: 18px Inconsolata, Monaco, Courier !important;
-webkit-box-flex: 1;
}

BIN
static/images/linen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -1,4 +1,4 @@
@import url(//fonts.googleapis.com/css?family=Droid+Sans+Mono);
/*@import url(//fonts.googleapis.com/css?family=Droid+Sans+Mono);*/
.ace_editor {