diff --git a/spec/atom/file-finder-spec.coffee b/spec/atom/file-finder-spec.coffee
new file mode 100644
index 000000000..d83242991
--- /dev/null
+++ b/spec/atom/file-finder-spec.coffee
@@ -0,0 +1,32 @@
+FileFinder = require 'file-finder'
+
+describe 'FileFinder', ->
+ finder = null
+
+ beforeEach ->
+ urls = ['app.coffee', 'buffer.coffee', 'atom/app.coffee', 'atom/buffer.coffee']
+ finder = FileFinder.build {urls}
+
+ describe "when characters are typed into the input element", ->
+ it "displays matching urls in the ol element", ->
+ expect(finder.urlList.find('li')).not.toExist()
+
+ finder.input.text('ap')
+ finder.input.keypress()
+
+ expect(finder.urlList.children().length).toBe 2
+ expect(finder.urlList.find('li:contains(app.coffee)').length).toBe 2
+ expect(finder.urlList.find('li:contains(atom/app.coffee)').length).toBe 1
+
+ # we should clear the list before re-populating it
+ finder.input.text('a/ap')
+ finder.input.keypress()
+
+ expect(finder.urlList.children().length).toBe 1
+ expect(finder.urlList.find('li:contains(atom/app.coffee)').length).toBe 1
+
+ describe "findMatches(queryString)", ->
+ it "returns urls sorted by score of match against the given query", ->
+ expect(finder.findMatches('ap')).toEqual ["app.coffee", "atom/app.coffee"]
+ expect(finder.findMatches('a/ap')).toEqual ["atom/app.coffee"]
+
diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
index f9aceeab6..94204773f 100644
--- a/spec/spec-helper.coffee
+++ b/spec/spec-helper.coffee
@@ -1,3 +1,4 @@
+nakedLoad 'jasmine-jquery'
$ = require 'jquery'
_ = require 'underscore'
Native = require 'native'
diff --git a/spec/stdlib/template-spec.coffee b/spec/stdlib/template-spec.coffee
index 1a139ffd0..c4d9e4824 100644
--- a/spec/stdlib/template-spec.coffee
+++ b/spec/stdlib/template-spec.coffee
@@ -3,18 +3,64 @@ Template = require 'template'
describe "Template", ->
describe "toView", ->
Foo = null
+ view = null
beforeEach ->
class Foo extends Template
- content: ->
- div ->
- h1 @title
+ content: (attrs) ->
+ @div =>
+ @h1 attrs.title
+ @list()
+
+ list: ->
+ @ol =>
+ @li outlet: 'li1', click: 'li1Clicked', class: 'foo', "one"
+ @li outlet: 'li2', keypress:'li2Keypressed', class: 'bar', "two"
+
+ viewProperties:
+ initialize: (attrs) ->
+ @initializeCalledWith = attrs
+ foo: "bar",
+ li1Clicked: ->,
+ li2Keypressed: ->
+
+ view = Foo.build(title: "Zebra")
afterEach ->
delete window.Foo
- it "builds a jquery object based on the content method and extends it with the viewProperties", ->
- view = Foo.buildView(title: "Hello World")
- expect(view.find('h1').text()).toEqual "Hello World"
+ describe ".build(attributes)", ->
+ it "generates markup based on the content method", ->
+ expect(view).toMatchSelector "div"
+ expect(view.find("h1:contains(Zebra)")).toExist()
+ expect(view.find("ol > li.foo:contains(one)")).toExist()
+ expect(view.find("ol > li.bar:contains(two)")).toExist()
+ it "extends the view with viewProperties, calling the 'constructor' property if present", ->
+ expect(view.constructor).toBeDefined()
+ expect(view.foo).toBe("bar")
+ expect(view.initializeCalledWith).toEqual title: "Zebra"
+
+ it "wires references for elements with 'outlet' attributes", ->
+ expect(view.li1).toMatchSelector "li.foo:contains(one)"
+ expect(view.li2).toMatchSelector "li.bar:contains(two)"
+
+ it "binds events for elements with event name attributes", ->
+ spyOn(view, 'li1Clicked').andCallFake (event, elt) ->
+ expect(event.type).toBe 'click'
+ expect(elt).toMatchSelector 'li.foo:contains(one)'
+
+ spyOn(view, 'li2Keypressed').andCallFake (event, elt) ->
+ expect(event.type).toBe 'keypress'
+ expect(elt).toMatchSelector "li.bar:contains(two)"
+
+ view.li1.click()
+ expect(view.li1Clicked).toHaveBeenCalled()
+ expect(view.li2Keypressed).not.toHaveBeenCalled()
+
+ view.li1Clicked.reset()
+
+ view.li2.keypress()
+ expect(view.li2Keypressed).toHaveBeenCalled()
+ expect(view.li1Clicked).not.toHaveBeenCalled()
diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee
new file mode 100644
index 000000000..ea055a5f7
--- /dev/null
+++ b/spec/stdlib/template/builder-spec.coffee
@@ -0,0 +1,41 @@
+Builder = require 'template/builder'
+
+describe "Builder", ->
+ builder = null
+
+ beforeEach -> builder = new Builder
+
+ describe ".tag(name, args...)", ->
+ it "can generate simple tags", ->
+ builder.tag 'div'
+ expect(builder.toHtml()).toBe "
"
+
+ builder.reset()
+ builder.tag 'ol'
+ expect(builder.toHtml()).toBe "
"
+
+ it "can generate tags with content", ->
+ builder.tag 'ol', ->
+ builder.tag 'li'
+ builder.tag 'li'
+
+ expect(builder.toHtml()).toBe "
"
+
+ it "can generate tags with text", ->
+ builder.tag 'div', "hello"
+ expect(builder.toHtml()).toBe "hello
"
+
+ builder.reset()
+ builder.tag 'div', 22
+ expect(builder.toHtml()).toBe "22
"
+
+ it "can generate tags with attributes", ->
+ builder.tag 'div', id: 'foo', class: 'bar'
+ fragment = builder.toFragment()
+ expect(fragment.attr('id')).toBe 'foo'
+ expect(fragment.attr('class')).toBe 'bar'
+
+ it "can generate self-closing tags", ->
+ builder.tag 'br', id: 'foo'
+ expect(builder.toHtml()).toBe '
'
+
diff --git a/src/atom/file-finder.coffee b/src/atom/file-finder.coffee
new file mode 100644
index 000000000..e615017e0
--- /dev/null
+++ b/src/atom/file-finder.coffee
@@ -0,0 +1,26 @@
+$ = require 'jquery'
+Template = require 'template'
+stringScore = require 'stringscore'
+
+module.exports =
+class FileFinder extends Template
+ content: ->
+ @div class: 'file-finder', =>
+ @ol outlet: 'urlList'
+ @input outlet: 'input', keypress: 'populateUrlList'
+
+ viewProperties:
+ urls: null
+
+ initialize: ({@urls}) ->
+
+ populateUrlList: ->
+ @urlList.empty()
+ for url in @findMatches(@input.text())
+ @urlList.append $("#{url}")
+
+ findMatches: (query) ->
+ scoredUrls = ({url, score: stringScore(url, query)} for url in @urls)
+ sortedUrls = scoredUrls.sort (a, b) -> a.score > b.score
+ urlAndScore.url for urlAndScore in sortedUrls when urlAndScore.score > 0
+
diff --git a/src/atom/layout.coffee b/src/atom/layout.coffee
index b59fa7dd5..2496aa567 100644
--- a/src/atom/layout.coffee
+++ b/src/atom/layout.coffee
@@ -4,13 +4,13 @@ Template = require 'template'
module.exports =
class Layout extends Template
@attach: ->
- view = @buildView()
+ view = @build()
$('body').append(view)
view
content: ->
- link rel: 'stylesheet', href: 'static/atom.css'
- div id: 'app-horizontal', ->
- div id: 'app-vertical', ->
- div id: 'main'
+ @link rel: 'stylesheet', href: 'static/atom.css'
+ @div id: 'app-horizontal', =>
+ @div id: 'app-vertical', =>
+ @div id: 'main'
diff --git a/src/stdlib/template.coffee b/src/stdlib/template.coffee
index 35f358e53..d4585a360 100644
--- a/src/stdlib/template.coffee
+++ b/src/stdlib/template.coffee
@@ -1,10 +1,43 @@
$ = require 'jquery'
-coffeekup = require 'coffeekup'
+_ = require 'underscore'
+Builder = require 'template/builder'
module.exports =
class Template
- @buildView: (attributes) ->
- (new this).buildView(attributes)
+ @events: 'blur change click dblclick error focus keydown
+ keypress keyup load mousedown mousemove mouseout mouseover
+ mouseup resize scroll select submit unload'.split /\s+/
+
+ @buildTagMethod: (name) ->
+ this.prototype[name] = (args...) -> @builder.tag(name, args...)
+
+ @buildTagMethod(name) for name in Builder.elements.normal
+ @buildTagMethod(name) for name in Builder.elements.void
+
+ @build: (attributes) ->
+ (new this).build(attributes)
+
+ build: (attributes) ->
+ @builder = new Builder
+ @content(attributes)
+ view = @builder.toFragment()
+ @wireOutlets(view)
+ @bindEvents(view)
+ if @viewProperties
+ $.extend(view, @viewProperties)
+ view.initialize?(attributes)
+ view
+
+ wireOutlets: (view) ->
+ view.find('[outlet]').each ->
+ elt = $(this)
+ outletName = elt.attr('outlet')
+ view[outletName] = elt
+
+ bindEvents: (view) ->
+ for eventName in this.constructor.events
+ view.find("[#{eventName}]").each ->
+ elt = $(this)
+ methodName = elt.attr(eventName)
+ elt[eventName]((event) -> view[methodName](event, elt))
- buildView: (attributes) ->
- $(coffeekup.render(@content, attributes))
diff --git a/src/stdlib/template/builder.coffee b/src/stdlib/template/builder.coffee
new file mode 100644
index 000000000..819842bc7
--- /dev/null
+++ b/src/stdlib/template/builder.coffee
@@ -0,0 +1,60 @@
+_ = require 'underscore'
+$ = require 'jquery'
+OpenTag = require 'template/open-tag'
+CloseTag = require 'template/close-tag'
+Text = require 'template/text'
+
+module.exports =
+class Builder
+ @elements:
+ normal: 'a abbr address article aside audio b bdi bdo blockquote body button
+ canvas caption cite code colgroup datalist dd del details dfn div dl dt em
+ fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup
+ html i iframe ins kbd label legend li map mark menu meter nav noscript object
+ ol optgroup option output p pre progress q rp rt ruby s samp script section
+ select small span strong style sub summary sup table tbody td textarea tfoot
+ th thead time title tr u ul video'.split /\s+/
+
+ void: 'area base br col command embed hr img input keygen link meta param
+ source track wbr'.split /\s+/
+
+ constructor: ->
+ @reset()
+
+ toHtml: ->
+ _.map(@document, (x) -> x.toHtml()).join('')
+
+ toFragment: ->
+ $(@toHtml())
+
+ tag: (name, args...) ->
+ options = @extractOptions(args)
+
+ @document.push(new OpenTag(name, options.attributes))
+ if @elementIsVoid(name)
+ if (options.text? or options.content?)
+ throw new Error("Self-closing tag #{tag} cannot have text or content")
+ else
+ options.content?()
+ @text(options.text) if options.text
+ @document.push(new CloseTag(name))
+
+
+ elementIsVoid: (name) ->
+ _.contains(this.constructor.elements.void, name)
+
+ extractOptions: (args) ->
+ options = {}
+ for arg in args
+ options.content = arg if _.isFunction(arg)
+ options.text = arg if _.isString(arg)
+ options.text = arg.toString() if _.isNumber(arg)
+ options.attributes = arg if _.isObject(arg)
+ options
+
+ text: (string) ->
+ @document.push(new Text(string))
+
+ reset: ->
+ @document = []
+
diff --git a/src/stdlib/template/close-tag.coffee b/src/stdlib/template/close-tag.coffee
new file mode 100644
index 000000000..3e4874dfc
--- /dev/null
+++ b/src/stdlib/template/close-tag.coffee
@@ -0,0 +1,7 @@
+module.exports =
+class CloseTag
+ constructor: (@name) ->
+
+ toHtml: ->
+ "#{@name}>"
+
diff --git a/src/stdlib/template/open-tag.coffee b/src/stdlib/template/open-tag.coffee
new file mode 100644
index 000000000..ec37868aa
--- /dev/null
+++ b/src/stdlib/template/open-tag.coffee
@@ -0,0 +1,12 @@
+_ = require 'underscore'
+
+module.exports =
+class OpenTag
+ constructor: (@name, @attributes) ->
+
+ toHtml: ->
+ "<#{@name}#{@attributesHtml()}>"
+
+ attributesHtml: ->
+ s = _.map(@attributes, (value, key) -> "#{key}=\"#{value}\"").join(' ')
+ if s == "" then "" else " " + s
diff --git a/src/stdlib/template/text.coffee b/src/stdlib/template/text.coffee
new file mode 100644
index 000000000..5f20e1d2e
--- /dev/null
+++ b/src/stdlib/template/text.coffee
@@ -0,0 +1,6 @@
+module.exports =
+class Text
+ constructor: (@string) ->
+
+ toHtml: -> @string
+
diff --git a/vendor/jasmine-jquery.js b/vendor/jasmine-jquery.js
new file mode 100644
index 000000000..e1cafb895
--- /dev/null
+++ b/vendor/jasmine-jquery.js
@@ -0,0 +1,177 @@
+jQuery = require('jquery');
+jasmine.JQuery = function() {};
+
+jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
+ return jQuery('').append(html).html();
+};
+
+jasmine.JQuery.elementToString = function(element) {
+ return jQuery('').append(element.clone()).html();
+};
+
+jasmine.JQuery.matchersClass = {};
+
+(function(namespace) {
+ var data = {
+ spiedEvents: {},
+ handlers: []
+ };
+
+ namespace.events = {
+ spyOn: function(selector, eventName) {
+ var handler = function(e) {
+ data.spiedEvents[[selector, eventName]] = e;
+ };
+ jQuery(selector).bind(eventName, handler);
+ data.handlers.push(handler);
+ },
+
+ wasTriggered: function(selector, eventName) {
+ return !!(data.spiedEvents[[selector, eventName]]);
+ },
+
+ cleanUp: function() {
+ data.spiedEvents = {};
+ data.handlers = [];
+ }
+ }
+})(jasmine.JQuery);
+
+(function(){
+ var jQueryMatchers = {
+ toHaveClass: function(className) {
+ return this.actual.hasClass(className);
+ },
+
+ toBeVisible: function() {
+ return this.actual.is(':visible');
+ },
+
+ toBeHidden: function() {
+ return this.actual.is(':hidden');
+ },
+
+ toBeSelected: function() {
+ return this.actual.is(':selected');
+ },
+
+ toBeChecked: function() {
+ return this.actual.is(':checked');
+ },
+
+ toBeEmpty: function() {
+ return this.actual.is(':empty');
+ },
+
+ toExist: function() {
+ return this.actual.size() > 0;
+ },
+
+ toHaveAttr: function(attributeName, expectedAttributeValue) {
+ return hasProperty(this.actual.attr(attributeName), expectedAttributeValue);
+ },
+
+ toHaveId: function(id) {
+ return this.actual.attr('id') == id;
+ },
+
+ toHaveHtml: function(html) {
+ return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html);
+ },
+
+ toHaveText: function(text) {
+ if (text && jQuery.isFunction(text.test)) {
+ return text.test(this.actual.text());
+ } else {
+ return this.actual.text() == text;
+ }
+ },
+
+ toHaveValue: function(value) {
+ return this.actual.val() == value;
+ },
+
+ toHaveData: function(key, expectedValue) {
+ return hasProperty(this.actual.data(key), expectedValue);
+ },
+
+ toMatchSelector: function(selector) {
+ return this.actual.is(selector);
+ },
+
+ toContain: function(selector) {
+ return this.actual.find(selector).size() > 0;
+ },
+
+ toBeDisabled: function(selector){
+ return this.actual.is(':disabled');
+ },
+
+ // tests the existence of a specific event binding
+ toHandle: function(eventName) {
+ var events = this.actual.data("events");
+ return events && events[eventName].length > 0;
+ },
+
+ // tests the existence of a specific event binding + handler
+ toHandleWith: function(eventName, eventHandler) {
+ var stack = this.actual.data("events")[eventName];
+ var i;
+ for (i = 0; i < stack.length; i++) {
+ if (stack[i].handler == eventHandler) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ var hasProperty = function(actualValue, expectedValue) {
+ if (expectedValue === undefined) {
+ return actualValue !== undefined;
+ }
+ return actualValue == expectedValue;
+ };
+
+ var bindMatcher = function(methodName) {
+ var builtInMatcher = jasmine.Matchers.prototype[methodName];
+
+ jasmine.JQuery.matchersClass[methodName] = function() {
+ if (this.actual instanceof jQuery) {
+ var result = jQueryMatchers[methodName].apply(this, arguments);
+ this.actual = jasmine.JQuery.elementToString(this.actual);
+ return result;
+ }
+
+ if (builtInMatcher) {
+ return builtInMatcher.apply(this, arguments);
+ }
+
+ return false;
+ };
+ };
+
+ for(var methodName in jQueryMatchers) {
+ bindMatcher(methodName);
+ }
+})();
+
+beforeEach(function() {
+ this.addMatchers(jasmine.JQuery.matchersClass);
+ this.addMatchers({
+ toHaveBeenTriggeredOn: function(selector) {
+ this.message = function() {
+ return [
+ "Expected event " + this.actual + " to have been triggered on" + selector,
+ "Expected event " + this.actual + " not to have been triggered on" + selector
+ ];
+ };
+ return jasmine.JQuery.events.wasTriggered(selector, this.actual);
+ }
+ })
+});
+
+afterEach(function() {
+ jasmine.JQuery.events.cleanUp();
+});
+
diff --git a/vendor/stringscore.js b/vendor/stringscore.js
index a734adc9f..7e758b002 100644
--- a/vendor/stringscore.js
+++ b/vendor/stringscore.js
@@ -1,3 +1,5 @@
+// MODIFIED BY NS/CJ - Don't extend the prototype of String
+
/*!
* string_score.js: String Scoring Algorithm 0.1.9
*
@@ -16,10 +18,10 @@
* 'Hello World'.score('he'); //=> 0.5931818181818181
* 'Hello World'.score('Hello'); //=> 0.7318181818181818
*/
-String.prototype.score = function(abbreviation, fuzziness) {
+
+module.exports = function(string, abbreviation, fuzziness) {
var total_character_score = 0,
abbreviation_length = abbreviation.length,
- string = this,
string_length = string.length,
start_of_string_bonus,
abbreviation_score,