Command-T: File Finder™

This commit is contained in:
Chris Wanstrath
2011-09-15 00:42:02 -07:00
parent a001be87e4
commit b55cd6e559
6 changed files with 481 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
#facebox {
position:absolute;
top:0;
left:0;
z-index:100;
text-align:left;
}
#facebox .popup{
position:relative;
border:3px solid rgba(0,0,0,0);
-webkit-border-radius:5px;
-moz-border-radius:5px;
border-radius:5px;
-webkit-box-shadow:0 0 18px rgba(0,0,0,0.4);
-moz-box-shadow:0 0 18px rgba(0,0,0,0.4);
box-shadow:0 0 18px rgba(0,0,0,0.4);
}
#facebox .content{
display:table;
width:370px;
padding:10px;
background:#fff;
-webkit-border-radius:4px;
-moz-border-radius:4px;
border-radius:4px;
}
#facebox .content > p:first-child{
margin-top:0;
}
#facebox .content > p:last-child{
margin-bottom:0;
}
#facebox .close{
position:absolute;
top:5px;
right:5px;
padding:2px;
width:8px;
height:8px;
opacity:0.3;
z-index:10000;
}
#facebox .close:hover{
opacity:1.0;
text-decoration:none;
}
#facebox .fb-loading{
height:32px;
text-align:center;
}
#facebox img{
display:block;
border:0;
margin:0 auto;
}
#facebox-overlay{
position:fixed;
top:0px;
left:0px;
height:100%;
width:100%;
background-color:#000;
z-index:99;
display:none;
}
#facebox .close.inline{
background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEpJREFUeNpiYGBgEAXivUCsxYAAukC8CyoHlvwPxK+gErpQ9n+oHIrAKzS2LrKRMAkUSSYG3OAvNt1YrSDoSJg3ddG8CRITBQgwALGIIG7dYZgsAAAAAElFTkSuQmCC") top right no-repeat;
}
#facebox .fb-loading{
background:center url("data:image/gif;base64,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") no-repeat;
}

View File

@@ -0,0 +1,154 @@
// facebox 2.0
// MIT Licensed
// https://github.com/defunkt/facebox
(function($){
// jQuery plugin
//
// $('a[rel*=facebox]').facebox()
// $('.js-add-cc').facebox(function() {
// $('#facebox .js-thanks, #facebox .rule:first').hide()
// })
$.fn.facebox = function( callback ) {
return this.live('click.facebox', function(){
// rel="facebox.my-class" adds my-class to '#facebox .content'
var contentClass = ( /facebox\.(\S+)/.exec(this.rel) || [] )[1]
if ( callback )
$(document).one('show.facebox', callback)
$.facebox({ div: this.href }, contentClass)
return false
})
}
// The Real Deal
//
// $.facebox('<b>Cool!</b>')
$.facebox = function( data, contentClass ) {
if ( $('#facebox .fb-loading').length == 0 ) {
show('<div class="fb-loading">&nbsp;</div>')
$(document).trigger('loading.facebox')
}
$('#facebox .content').addClass(contentClass)
if ( !data ) return
// $.facebox('#info') => $.facebox({div: '#info'})
if ( /^#/.test(data) )
data = { div: data }
// $.facebox('/some/url') => $.facebox({ajax: '/some/url' })
if ( /^\//.test(data) )
data = { ajax: data }
var href = data.ajax || data.image || data.div
if ( href ) {
// div
var div = /#.+$/.exec(href)
if ( div ) {
show( $(div[0]).html() )
// image
} else if ( data.image || /\.(png|jpe?g|gif)(\?\S*)?$/i.test(href) ) {
var image = new Image
image.onload = function() {
show('<img src="' + image.src + '" />')
}
image.src = href
// ajax
} else {
$.get(href, show)
}
}
else if ( $.isFunction(data) )
data()
else
show(data)
}
//
// Default settings
//
var settings = $.facebox.settings = {
opacity : 0.2,
overlay : true,
faceboxHTML: '<div id="facebox"><div class="popup"><div class="content"></div><a href="#" class="close inline">&nbsp;</a></div></div>'
}
//
// Methods
//
// Close Facebox
var close = $.facebox.close = function(){
$(document).trigger('close.facebox')
return false
}
// Show the Facebox
function show( data ) {
// Dim the lights
if ( settings.overlay && !$('#facebox-overlay').is(':visible') ) {
$('body').append('<div id="facebox-overlay"></div>')
$('#facebox-overlay')
.css('opacity', settings.opacity)
.fadeIn(200)
.click(close)
}
// Create the Facebox
if ( $('#facebox').length == 0 ) {
$('body').append( $(settings.faceboxHTML).hide() )
$(document).trigger('setup.facebox')
}
// Make that money
$('#facebox .content').html(data).show()
// Position Facebox based on the user's scroll
$('#facebox').show().css({
top: $(window).scrollTop() + ($(window).height() / 10),
left: $(window).width() / 2 - ($('#facebox .popup').outerWidth() / 2)
})
// Fire events if we're not loading.
if ( $('.fb-loading:visible').length == 0 )
$(document).trigger('show.facebox').trigger('reveal.facebox')
}
//
// Bindings
//
$(document).bind('close.facebox', function(){
$('#facebox').fadeOut(function(){
$(this).remove()
$(document).trigger('afterClose.facebox')
})
$('#facebox-overlay').fadeOut(200, function(){
$(this).remove()
})
})
$(document).one('show.facebox', function(){
// Click on close image = Close Facebox
$('#facebox .close').live('click', close)
// ESC = Close Facebox
$(document).bind('keydown.facebox', function(e){
if ( e.keyCode == 27 && $('#facebox:visible').length ) close()
return true
})
})
})(jQuery);

View File

@@ -0,0 +1,106 @@
$ = require 'jquery'
_ = require 'underscore'
{activeWindow} = require 'app'
File = require 'fs'
Pane = require 'pane'
jQuery = $
facebox = eval File.read require.resolve 'filefinder/facebox'
require 'filefinder/stringscore'
module.exports =
class Filefinder extends Pane
showing: false
files: []
html: require "filefinder/filefinder.html"
keymap:
'Command-T': 'toggle'
# really wish i could put up/down keyboad shortcuts here
# and have them activated when the filefinder is open
initialize: ->
$('#filefinder input').live 'keydown', @onKeydown
css = File.read require.resolve 'filefinder/facebox.css'
head = $('head')[0]
style = document.createElement 'style'
rules = document.createTextNode css
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()
else if e.keyCode is keys.up
@moveUp()
else if e.keyCode is keys.down
@moveDown()
else
@filterFiles()
toggle: ->
if @showing
$.facebox.close()
else
@showFinder()
@showing = not @showing
showFinder: ->
$.facebox @html
@files = []
for file in activeWindow.project.paths()
@files.push file.replace "#{activeWindow.project.dir}/", ''
@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 = activeWindow.project.dir
file = $('#filefinder .selected').text()
activeWindow.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

@@ -0,0 +1,17 @@
<style>
#filefinder .filelist {
height: 100px;
overflow: hidden;
padding: 10px 0;
}
#filefinder .filelist .selected {
background-color: yellow;
}
</style>
<div id='filefinder'>
<input type='search'>
<br>
<ul class='filelist'>
</ul>
</div>

View File

@@ -0,0 +1,3 @@
exports.Filefinder = Filefinder = require 'filefinder/filefinder'
new Filefinder

View File

@@ -0,0 +1,122 @@
/*!
* string_score.js: String Scoring Algorithm 0.1.9
*
* http://joshaven.com/string_score
* https://github.com/joshaven/string_score
*
* Copyright (C) 2009-2011 Joshaven Potter <yourtech@gmail.com>
* Copyright (C) 2010-2011 Yesudeep Mangalapilly <yesudeep@gmail.com>
* MIT license: http://www.opensource.org/licenses/mit-license.php
*
* Date: Tue Mar 1 2011
*/
/**
* Scores a string against another string.
* 'Hello World'.score('he'); //=> 0.5931818181818181
* 'Hello World'.score('Hello'); //=> 0.7318181818181818
*/
String.prototype.score = function(abbreviation, fuzziness) {
var total_character_score = 0,
abbreviation_length = abbreviation.length,
string = this,
string_length = string.length,
start_of_string_bonus,
abbreviation_score,
fuzzies=1,
final_score;
// If the string is equal to the abbreviation, perfect match.
if (string == abbreviation) {return 1.0;}
// Walk through abbreviation and add up scores.
for (var i = 0,
character_score/* = 0*/,
index_in_string/* = 0*/,
c/* = ''*/,
index_c_lowercase/* = 0*/,
index_c_uppercase/* = 0*/,
min_index/* = 0*/;
i < abbreviation_length;
++i) {
// Find the first case-insensitive match of a character.
c = abbreviation[i];
//index_in_string = __first_valid_index(
// string.indexOf(c.toLowerCase()),
// string.indexOf(c.toUpperCase())
//);
// Inlined the above call below.
index_c_lowercase = string.indexOf(c.toLowerCase());
index_c_uppercase = string.indexOf(c.toUpperCase());
min_index = Math.min(index_c_lowercase, index_c_uppercase);
index_in_string = (min_index > -1) ?
min_index :
Math.max(index_c_lowercase, index_c_uppercase);
// End inlining.
if (index_in_string === -1) {
if (fuzziness) {
fuzzies += 1-fuzziness;
break;
} else {
return 0;
}
} else {
character_score = 0.1;
}
// Set base score for matching 'c'.
// Same case bonus.
if (string[index_in_string] === c) {
character_score += 0.1;
}
// Consecutive letter & start-of-string Bonus
if (index_in_string === 0) {
// Increase the score when matching first character of the
// remainder of the string
character_score += 0.6;
if (i === 0) {
// If match is the first character of the string
// & the first character of abbreviation, add a
// start-of-string match bonus.
start_of_string_bonus = 1 //true;
}
}
// Acronym Bonus
// Weighing Logic: Typing the first character of an acronym is as if you
// preceded it with two perfect character matches.
if (string.charAt(index_in_string - 1) === ' ') {
character_score += 0.8; // * Math.min(index_in_string, 5); // Cap bonus at 0.4 * 5
}
// Left trim the already matched part of the string
// (forces sequential matching).
string = string.substring(index_in_string + 1, string_length);
total_character_score += character_score;
} // end of for loop
// Uncomment to weigh smaller words higher.
// return total_character_score / string_length;
abbreviation_score = total_character_score / abbreviation_length;
//percentage_of_matched_string = abbreviation_length / string_length;
//word_score = abbreviation_score * percentage_of_matched_string;
// Reduce penalty for longer strings.
//final_score = (word_score + abbreviation_score) / 2;
final_score = ((abbreviation_score * (abbreviation_length / string_length)) + abbreviation_score) / 2;
final_score = final_score / fuzzies;
if (start_of_string_bonus && (final_score + 0.15 < 1)) {
final_score += 0.15;
}
return final_score;
};