├── Support ├── help.mdown ├── assets │ ├── progress_wheel.gif │ ├── _file.html.erb │ ├── gotofile.html.erb │ ├── gotofile.css │ └── gotofile.js └── lib │ ├── go_to_file.rb │ ├── insertPath.applescript │ ├── insertRelPath.applescript │ ├── file_finder.rb │ └── fuzzy_file_finder.rb ├── LICENSE ├── Commands ├── Go to File.tmCommand └── Help.tmCommand ├── info.plist └── README.markdown /Support/help.mdown: -------------------------------------------------------------------------------- 1 | ../README.markdown -------------------------------------------------------------------------------- /Support/assets/progress_wheel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/gotofile.tmbundle/master/Support/assets/progress_wheel.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project and source codes referring to it are under the 2 | Creative Commons Attribution-NonCommercial-ShareAlike 1.0 3 | license. For more information, please visit 4 | http://creativecommons.org/licenses/by-nc-sa/1.0/ 5 | 6 | NOTE: Support/lib/fuzzy_file_finder.rb has its own licence, please see http://github.com/jamis/fuzzy_file_finder. 7 | -------------------------------------------------------------------------------- /Support/assets/_file.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 | ' onclick='myClick("<%= p[:path] %>")'> 8 | <%= hpath %> 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /Support/lib/go_to_file.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | require 'erb' 3 | 4 | asset_path = ENV['TM_BUNDLE_SUPPORT'] + '/assets' 5 | 6 | html_path = asset_path + '/gotofile.html.erb' 7 | css_path = asset_path.gsub(' ', '%20') + '/gotofile.css' 8 | js_path = asset_path.gsub(' ', '%20') + '/gotofile.js' 9 | 10 | project_path = ENV['TM_PROJECT_DIRECTORY'] || ENV['TM_DIRECTORY'] || ENV['TM_FILEPATH'] && File.dirname(ENV['TM_FILEPATH']) 11 | 12 | js_vars = { 13 | :bundle_support => ENV['TM_BUNDLE_SUPPORT'], 14 | :path_to_ruby => ENV['TM_RUBY'] || 'ruby', 15 | }.collect { |var, value| "var #{var} = '#{value}';\n" } 16 | 17 | 18 | puts ERB.new(File.read(html_path)).result 19 | -------------------------------------------------------------------------------- /Support/assets/gotofile.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Go to File 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 19 |
20 |
21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /Support/lib/insertPath.applescript: -------------------------------------------------------------------------------- 1 | -- 2 | -- inserts the absolute path into the current document and 3 | -- activates it 4 | -- 5 | -- Mar 03 2009 - written by Hans-J. Bibiko bibiko@eva.mpg.de 6 | -- 7 | 8 | on run (argv) 9 | set chosenFile to item 1 of argv 10 | tell application "TextMate" 11 | tell document 1 to activate 12 | try 13 | set curFile to the path of document 1 14 | set dummy to the length of curFile 15 | on error 16 | insert "${1:" & chosenFile & "}" with as snippet 17 | do shell script "open 'txmt://open?'" 18 | return 19 | end try 20 | insert "${1:" & chosenFile & "}" with as snippet 21 | do shell script "open 'txmt://open?url=file://" & curFile & "'" 22 | end tell 23 | end run -------------------------------------------------------------------------------- /Commands/Go to File.tmCommand: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | beforeRunningCommand 6 | nop 7 | command 8 | RUBYLIB="$TM_BUNDLE_SUPPORT/lib:$RUBYLIB" 9 | "${TM_RUBY:=ruby}" -- "${TM_BUNDLE_SUPPORT}/lib/go_to_file.rb" 10 | input 11 | none 12 | keyEquivalent 13 | @K 14 | name 15 | Go to File 16 | output 17 | showAsHTML 18 | uuid 19 | D058F9B3-CD75-4AF0-A257-20942B39D163 20 | 21 | 22 | -------------------------------------------------------------------------------- /Commands/Help.tmCommand: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | beforeRunningCommand 6 | nop 7 | command 8 | 9 | . "$TM_SUPPORT_PATH/lib/webpreview.sh" 10 | 11 | html_header "Go to File — Help" "Go to File" "Help" 12 | 13 | "$TM_SUPPORT_PATH/lib/markdown_to_help.rb" "$TM_BUNDLE_SUPPORT/help.mdown" 14 | 15 | html_footer 16 | input 17 | none 18 | name 19 | Help 20 | output 21 | showAsHTML 22 | uuid 23 | 0ACF99C3-3A64-4A06-BDA7-4FA4E3C2F6A3 24 | 25 | 26 | -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | contactEmailRot13 6 | nzvry.znegva@tznvy.pbz 7 | contactName 8 | Amiel Martin 9 | description 10 | Imitates TextMate’s “Go to File” ⌘T functionality and includes the chance to narrow down the list of files by certain folders using '/' as a delimiter. Furthermore one can open the selected file with the default application, insert the absolute/relative path, and open the file in QuickLook. This GUI makes usage of <a href="http://github.com/jamis/fuzzy_file_finder/tree/master">Jamis Buck’s fuzzy file finder</a> and code fragments from <a href="http://github.com/amiel/gotofile.tmbundle/tree/amiels_original"> the original fuzzyfilefinder bundle</a>. 11 | mainMenu 12 | 13 | items 14 | 15 | D058F9B3-CD75-4AF0-A257-20942B39D163 16 | 0ACF99C3-3A64-4A06-BDA7-4FA4E3C2F6A3 17 | 18 | submenus 19 | 20 | 21 | name 22 | GoToFile 23 | uuid 24 | 4A51CFD1-BBCD-4F91-8AD1-8F250A99E25E 25 | 26 | 27 | -------------------------------------------------------------------------------- /Support/lib/insertRelPath.applescript: -------------------------------------------------------------------------------- 1 | -- 2 | -- inserts the relative path to the file passed as first shell argument 3 | -- seen from TextMate's current document into the current document and 4 | -- activates it 5 | -- 6 | -- if TextMate's current document hasn't saved yet it inserts the absolute path instead 7 | -- 8 | -- Mar 03 2009 - written by Hans-J. Bibiko bibiko@eva.mpg.de 9 | -- 10 | 11 | on run (argv) 12 | set chosenFile to item 1 of argv 13 | tell application "TextMate" 14 | tell document 1 to activate 15 | try 16 | set curFile to the path of document 1 17 | set dummy to the length of curFile 18 | on error 19 | insert "${1:" & chosenFile & "}" with as snippet 20 | do shell script "open 'txmt://open?'" 21 | return 22 | end try 23 | set curPath to (do shell script "dirname " & quoted form of curFile) 24 | set chosenPath to (do shell script "dirname " & quoted form of chosenFile) 25 | set chosenFileName to (do shell script "basename " & quoted form of chosenFile) 26 | set AppleScript's text item delimiters to "/" 27 | set curPathArr to text items of curPath 28 | set chosenPathArr to text items of chosenPath 29 | set curPathArrLen to length of curPathArr 30 | set chosenPathArrLen to length of chosenPathArr 31 | set maxLoop to curPathArrLen 32 | if chosenPathArrLen < maxLoop then 33 | set maxLoop to chosenPathArrLen 34 | end if 35 | set idx to 1 36 | repeat 37 | if idx > maxLoop then 38 | exit repeat 39 | end if 40 | if item idx of chosenPathArr = item idx of curPathArr then 41 | set idx to idx + 1 42 | else 43 | exit repeat 44 | end if 45 | end repeat 46 | set relPath to "" 47 | repeat with i from idx to curPathArrLen 48 | set relPath to relPath & "../" 49 | end repeat 50 | repeat with i from idx to chosenPathArrLen 51 | set relPath to relPath & item i of chosenPathArr & "/" 52 | end repeat 53 | set relPath to relPath & chosenFileName 54 | insert "${1:" & relPath & "}" with as snippet 55 | do shell script "open 'txmt://open?url=file://" & curFile & "'" 56 | end tell 57 | end run -------------------------------------------------------------------------------- /Support/lib/file_finder.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -wKU 2 | 3 | require 'erb' 4 | 5 | # max number of found files after sorting by score 6 | MAX_OUTPUT = 100 7 | 8 | require File.dirname(__FILE__) + '/fuzzy_file_finder' 9 | 10 | TM_FUZZYFINDER_REVERSEPATHMODE = (ENV['TM_FUZZYFINDER_REVERSEPATHMODE'] and ENV['TM_FUZZYFINDER_REVERSEPATHMODE'].to_i != 0) ? true : false 11 | TM_FUZZYFINDER_IGNORE = ENV['TM_FUZZYFINDER_IGNORE'] ? ENV['TM_FUZZYFINDER_IGNORE'].to_s.split(/,/) : nil 12 | TM_FUZZYFINDER_CEILING = ENV['TM_FUZZYFINDER_CEILING'] ? ENV['TM_FUZZYFINDER_CEILING'].to_i : 10000 13 | 14 | 15 | project_path = ENV['TM_PROJECT_DIRECTORY'] || ENV['TM_DIRECTORY'] || ENV['TM_FILEPATH'] && File.dirname(ENV['TM_FILEPATH']) 16 | 17 | if project_path.nil? 18 | puts '

GoToFile could not determine a base location to start searching. Please open a project or save this file.

' 19 | exit 20 | end 21 | 22 | search_string = ARGV[0].gsub(/(\\( ))|(([^\\]) )/, '\2') 23 | 24 | if search_string.nil? || search_string.empty? 25 | print '

Please enter a search

' 26 | exit 27 | end 28 | 29 | 30 | if search_string =~ /\\(?! )/ 31 | TM_FUZZYFINDER_REVERSEPATHMODE = true 32 | search_string = search_string.split(/\\(?! )/).reverse.join("/") 33 | else 34 | search_string = search_string.split(/\\(?! )/).reverse.join("/") if TM_FUZZYFINDER_REVERSEPATHMODE 35 | end 36 | 37 | asset_path = ENV['TM_BUNDLE_SUPPORT'] + '/assets' 38 | 39 | 40 | # counter for outputted files 41 | cnt = 0 42 | 43 | template_path = asset_path + '/_file.html.erb' 44 | template = ERB.new(File.read(template_path)) 45 | 46 | begin 47 | FuzzyFileFinder.new(project_path, TM_FUZZYFINDER_CEILING, TM_FUZZYFINDER_IGNORE, FuzzyFileFinder::HtmlCharacterRun).find(search_string).sort{|b,a| a[:score] <=> b[:score] }.each do |p| 48 | sc = (p[:score].to_f * 100).to_i 49 | sc = sc > 100 ? 100 : sc 50 | hpath = p[:highlighted_path] 51 | hpath = hpath.split(%r{/(?!span)}).reverse.join("<") if TM_FUZZYFINDER_REVERSEPATHMODE 52 | puts template.result(binding) 53 | 54 | cnt += 1 55 | if cnt > MAX_OUTPUT 56 | puts %(

… more than #{MAX_OUTPUT} files found.

) 57 | break 58 | end 59 | end 60 | rescue FuzzyFileFinder::TooManyEntries 61 | puts %(

The root directory ‘#{project_path.gsub(/^#{ENV['HOME']}/, '~')}’ contains more than #{TM_FUZZYFINDER_CEILING} files. To increase the number of files parsed set up a TextMate shell variable TM_FUZZYFINDER_CEILING accordingly.

) 62 | rescue 63 | puts %(

#{$!}

) 64 | end 65 | 66 | puts %(

nothing found

) if cnt == 0 and !search_string.nil? and !search_string.empty? 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /Support/assets/gotofile.css: -------------------------------------------------------------------------------- 1 | /* CSS Styles for GoToFile TextMate Bundle */ 2 | 3 | /* Window Styling */ 4 | html, body 5 | { 6 | margin: 0; 7 | padding: 0; 8 | font-family: "Lucida Grande", "Trebuchet MS", Verdana, sans-serif; 9 | } 10 | 11 | /* Header */ 12 | #head 13 | { 14 | background-color: #666; 15 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABQCAYAAADvCdDvAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAOhJREFUeNrs3UuKAkEURUELyi+KioXoWt27qYtwcMA40Bu4wUsoJz2NMV4rZZq/f2sztEDeZmiBDDMAERAgAgJEQIDIh6ELkQuRCwEiT5YLkQsBIk+WC5ELkQsBIk+WCxEQT5ZcCBABERAgAgJEvkNciIAAERABASIgQAQEiID8H4jfslyIgAARECACAkS+Q1yIgMiT5UIExJOlnzaNMZ5maIE8zNACuZuhBbKYoQVyM0ML5GqGFsjFDC2QsxlaICcztECOZmiBHMzQAtmboQWyM0MLZGuGFsjGDC0Q/5w4BjKbodNHgAEA3LJ02F62uMsAAAAASUVORK5CYII=); 16 | border-bottom: 1px solid #666; 17 | height: 40px; 18 | position: fixed; 19 | top: 0px; 20 | width: 100%; 21 | z-index: 5; 22 | } 23 | 24 | #head input#search 25 | { 26 | margin: 11px 5% 0 5%; 27 | width: 90%; 28 | -webkit-appearance: searchfield; 29 | } 30 | 31 | /* Search Results */ 32 | #result 33 | { 34 | margin-top: 41px; 35 | } 36 | 37 | .file 38 | { 39 | padding: 4px; 40 | } 41 | 42 | .file:nth-child(odd) 43 | { 44 | background: #ECF3FE; 45 | } 46 | 47 | .file:hover 48 | { 49 | background: #DAE8FE; 50 | } 51 | 52 | .file:nth-child(odd):hover 53 | { 54 | background: #C9DDFE; 55 | } 56 | 57 | .file .score_wrapper 58 | { 59 | background: #fff; 60 | border: 1px solid #999; 61 | float: left; 62 | height: 6px; 63 | margin-top: 2px; 64 | padding: 1px; 65 | width: 15%; 66 | } 67 | 68 | 69 | .file .score 70 | { 71 | background-color: #1553AA; 72 | height: 100%; 73 | width: 1%; 74 | } 75 | 76 | .mylink 77 | { 78 | color: #111; 79 | cursor: pointer; 80 | font-size: 8.5pt; 81 | overflow: hidden; 82 | padding-left: 2%; 83 | width: 75%; 84 | white-space: nowrap; 85 | display: block; 86 | } 87 | 88 | .fuzzyff_match 89 | { 90 | background-color: #ffa; 91 | } 92 | 93 | /* Current Selection */ 94 | .selected, .file.selected:nth-child(odd), .file.selected:hover 95 | { 96 | background-color: #1553AA; 97 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADVJREFUeNpi/P//fwQDjQELEP8btYQUS/4OG0tGg2vwWfJnNLhGg2s0uEYjfjS4hrMlAAEGAHZHG24TN7qTAAAAAElFTkSuQmCC); 98 | } 99 | 100 | .selected .mylink 101 | { 102 | color: #fff; 103 | } 104 | 105 | .selected .fuzzyff_match 106 | { 107 | background: transparent; 108 | text-decoration: underline; 109 | } 110 | 111 | /* Notices */ 112 | .notice 113 | { 114 | background-color: #fc0; 115 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADVJREFUeNpi/P//fwQDjQELEP8btYQUS/4OG0tGg2vwWfJnNLhGg2s0uEYjfjS4hrMlAAEGAHZHG24TN7qTAAAAAElFTkSuQmCC); 116 | border-bottom: 1px solid #fc0; 117 | font-size: 9pt; 118 | font-weight: bold; 119 | padding: 1% 2%; 120 | } 121 | 122 | /* Error Message */ 123 | .error 124 | { 125 | background-color: #fc0; 126 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADVJREFUeNpi/P//fwQDjQELEP8btYQUS/4OG0tGg2vwWfJnNLhGg2s0uEYjfjS4hrMlAAEGAHZHG24TN7qTAAAAAElFTkSuQmCC); 127 | border-bottom: 1px solid #fc0; 128 | font-size: 9pt; 129 | font-weight: bold; 130 | color: #ff0000; 131 | padding: 1% 2%; 132 | } 133 | 134 | /*Progress spinning wheel*/ 135 | #progress { 136 | position: absolute; 137 | top: 0; 138 | right: 0; 139 | bottom: 0; 140 | left: 0; 141 | width: 10%; 142 | height: 10%; 143 | margin: auto; 144 | z-index: 8; 145 | } 146 | .progress_image { 147 | width: 56px; 148 | } 149 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Outline # 2 | 3 | “GoToFile” imitates TextMate’s “Go to File…” ⌘T functionality [see here](http://manual.macromates.com/en/working_with_multiple_files#moving_between_files_with_grace). In addition it is possible to narrow down the list of files by considering (parts of) the file path using '/' as a delimiter. 4 | 5 | Furthermore it is not only possible to open the selected file in TextMate but also to: 6 | 7 | * open the selected file in the default application (by using “open”) 8 | * activate QuickLook 9 | * insert the relative path from the current file (that file which had the focus while invoking “GoToFile”) to the selected one 10 | * insert the absolute path. 11 | 12 | The list of found files is sorted by the score which is calculated inside of Jamis Buck's “Fuzzy File Finder” routine. If the letters typed in match word prefixes (as _mf_ does in _my\_file.txt_), the match is usually prioritised. The maximum number of outputted files is set to 100 (can be changed in “FileFinder.rb” line 4). 13 | 14 | To ignore certain files add a TextMate shell variable called `TM_FUZZYFINDER_IGNORE`, in it put the file globs separated by commas. For example: '\*.pyc,\*.zip,\*.gz,\*.bz,\*.tar,\*.jpg,\*.png,\*.gif,\*.avi,\*.wmv,\*.ogg,\*.mp3,\*.mov'. 15 | 16 | As default there is a limit of 10.000 files in the tree structure. To increase/decrease that maximum number add the TextMate shell variable `TM_FUZZYFINDER_CEILING` and set it accordingly. 17 | 18 | This GUI makes usage of makes usage of Jamis Buck's [“Fuzzy File Finder”](http://github.com/jamis/fuzzy_file_finder) and was inspired by Amiel Martin's [“FuzzyFileFinder”](http://github.com/amiel/gotofile.tmbundle/tree/amiels_original) bundle which a few code fragments are taken from. 19 | 20 | # Installation # 21 | 22 | * by using “GetBundles” 23 | 24 | * by using “git”: 25 | 26 |
cd ~/Library/Application\ Support/TextMate/Bundles/
27 | git clone git://github.com/amiel/gotofile.tmbundle.git GoToFile.tmbundle
28 | 
29 | 30 | * by downloading this [zip archive](http://github.com/amiel/gotofile.tmbundle/zipball/master), decompressing it, renaming it to “GoToFile.tmbundle”, and finally double-clicking at it 31 | 32 | # Usage # 33 | 34 | invokes “GoToFile”. The root directory will be taken from `$TM_PROJECT_DIRECTORY` || `$TM_DIRECTORY` || current directory. “GoToFile” won't work on unsaved documents. There is a mouse-over event to display the entire file path. 35 | 36 | 37 | ## Input Field ## 38 | 39 | Type characters in order to narrow down the list of files. The dialogue will be updated while typing. To search only in certain folders type for instance: `s/rb` or `s/li/mm` etc. If the letters match word prefixes (as _mf_ does in _my\_file.txt_), the match is usually prioritized. 40 | 41 | If you would like to search for the file and then narrow down to subdirectory, you can type `rb\s` or `mm\li\s`. 42 | 43 | Normally spaces are ignored. If one wants to look for a space one has to escape the space: `\␣` 44 | 45 | ## Shortcuts ## 46 | 47 | * or opens the file in TextMate 48 | * or opens the file with the default application 49 | * or inserts the relative file path 50 | * or inserts the absolute file path 51 | * toggles the QuickLook mode (Leopard only) 52 | * adds an (escaped) space character in the search query 53 | * and resp. and navigates through the list of files 54 | * sets the focus to the input field 55 | * or closes the “GoToFile” window 56 | 57 | # Official Git Repos # 58 | 59 | Can be found here: http://github.com/amiel/gotofile.tmbundle 60 | 61 | # ToDo / wish list # 62 | 63 | * window should close when a file is opened (maybe configurable as it seems that some people like that it stays open) 64 | * take out duplicate files 65 | * clean up javascript (incorporate [sizzle](http://sizzlejs.com/) or jquery) 66 | 67 | ***also check out the todo list on the [github wiki](http://wiki.github.com/amiel/gotofile.tmbundle/todo)*** 68 | 69 | # Contributions # 70 | 71 | ***Date: Feb 26 2009*** 72 |
73 | -  Jamis Buck — fuzzy_file_finder library - jamis@37signals.com 
74 | 
75 |
76 | -  Amiel Martin  amiel.martin@gmail.com
77 | -  Hans-Jörg Bibiko  bibiko@eva.mpg.de
78 | -  Eric Doughty-Papassideris  github:ddlsmurf
79 | -  Travis Jeffery  t.jeffery@utoronto.ca
80 | -  Eric O'Connell   github:drd
81 | -  Nathan Carnes  github:nathancarnes
82 | 
83 | -------------------------------------------------------------------------------- /Support/assets/gotofile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * the following will be assigned by the ruby file 3 | * * bundle_support 4 | * * path_to_ruby 5 | */ 6 | 7 | var myCommand = null; 8 | var actpath = ""; 9 | var oldterm; 10 | var outStr = ""; 11 | var startTimer; 12 | var progressTimer; 13 | var term; 14 | var ft; 15 | var current_file=null; 16 | var current_ql_command=null; 17 | var current_ql_command_id=0; 18 | 19 | function init() { 20 | document.getElementById("search").focus(); 21 | startSearch(""); 22 | } 23 | 24 | function startSearch(t) { 25 | term = t; 26 | window.clearTimeout(startTimer); 27 | if (myCommand) myCommand.cancel(); 28 | startTimer = window.setTimeout("startSearchTimed()", 0); 29 | } 30 | function startProgressWheel() { 31 | window.clearTimeout(progressTimer); 32 | progressTimer = window.setTimeout("showProgressWheel()", 500); 33 | } 34 | function showProgressWheel() { 35 | document.getElementById("progress").innerHTML = ""; 36 | } 37 | function stopProgressWheel() { 38 | window.clearTimeout(progressTimer); 39 | document.getElementById("progress").innerHTML = ""; 40 | } 41 | function startSearchTimed() { 42 | TextMate.isBusy = false; 43 | outStr = ""; 44 | startProgressWheel(); 45 | setSelection(null); 46 | var cmd = "'" + path_to_ruby + "' '" + bundle_support + "/lib/file_finder.rb' '" + term + "'"; 47 | myCommand = TextMate.system(cmd, function(task) {}); 48 | myCommand.onreadoutput = output; 49 | } 50 | function output(str) { 51 | outStr += str; 52 | stopProgressWheel(); 53 | document.getElementById("result").innerHTML = outStr; 54 | if (current_file == null) 55 | changeSelect(1); 56 | } 57 | 58 | function setFile(path) { 59 | actpath = path; 60 | } 61 | 62 | function gotofile() { 63 | if (actpath != "") { 64 | TextMate.system("file -b '" + actpath + "' | grep text && mate '" + actpath + "' &", null); 65 | } 66 | } 67 | function insertPath() { 68 | if (actpath != "") { 69 | cmd = "osascript '" + bundle_support + "/lib/insertPath.applescript' '" + actpath + "' &"; 70 | TextMate.system(cmd, null); 71 | } 72 | } 73 | function insertRelPath() { 74 | if (actpath != "") { 75 | cmd = "osascript '" + bundle_support + "/lib/insertRelPath.applescript' '" + actpath + "' &"; 76 | TextMate.system(cmd, null); 77 | } 78 | } 79 | function openFile() { 80 | if (actpath != "") { 81 | TextMate.system("open '" + actpath + "' &", null); 82 | } 83 | } 84 | function cancel_quicklook() { 85 | var closed_quicklook = current_ql_command != null; 86 | if (current_ql_command) 87 | current_ql_command.cancel(); 88 | current_ql_command = null; 89 | return closed_quicklook; 90 | } 91 | function quicklook() { 92 | if (!current_file) return; 93 | var display_id = current_ql_command_id + 1; 94 | if (current_ql_command) 95 | cancel_quicklook(); 96 | else { 97 | current_ql_command_id = display_id; 98 | current_ql_command = TextMate.system("qlmanage -p '" + actpath + "'", function(task) { 99 | if (display_id == current_ql_command_id) 100 | current_ql_command = null; 101 | }); 102 | } 103 | } 104 | 105 | function myClick(path) { 106 | wkey = event.keyCode; 107 | actpath = path; 108 | if (event.shiftKey && event.altKey) insertPath(); 109 | else if (event.shiftKey) insertRelPath(); 110 | else if (event.altKey) openFile(); 111 | else gotofile(); 112 | } 113 | 114 | function getItemTopOffset(item) { 115 | var parent = item; 116 | var top = item.offsetTop; 117 | while(parent = parent.offsetParent) 118 | top += parent.offsetTop; 119 | return top; 120 | } 121 | function scrollToItem(item) { 122 | var itemPos = getItemTopOffset(item.parentNode); 123 | var headHeight = document.getElementById('head').offsetHeight; 124 | if (itemPos < document.body.scrollTop + headHeight) { 125 | document.body.scrollTop = itemPos - headHeight - 2; 126 | } else if ((itemPos + item.parentNode.offsetHeight >= document.body.clientHeight + document.body.scrollTop)) { 127 | document.body.scrollTop = itemPos; 128 | } 129 | } 130 | 131 | /* stolen from jquery */ 132 | function grep( elems, callback, inv ) { 133 | var ret = []; 134 | 135 | // Go through the array, only saving the items 136 | // that pass the validator function 137 | for ( var i = 0, length = elems.length; i < length; i++ ) 138 | if ( !inv != !callback( elems[ i ], i ) ) 139 | ret.push( elems[ i ] ); 140 | 141 | return ret; 142 | } 143 | 144 | function addClass(elem, className) { 145 | alert(elem.className); 146 | elem.className += (elem.className ? " " : "") + className; 147 | } 148 | 149 | function removeClass(elem, className) { 150 | elem.className = grep(elem.className.split(/\s+/), function(name){ 151 | return name == className; 152 | }, true).join(" "); 153 | } 154 | 155 | function setSelection(item) { 156 | if (item == current_file) return; 157 | var bReopenQuickLook = cancel_quicklook(); 158 | if (current_file) { 159 | removeClass(current_file.parentNode, "selected"); 160 | } 161 | current_file = item; 162 | if (current_file) { 163 | addClass(current_file.parentNode, "selected"); 164 | setFile(current_file.value); 165 | scrollToItem(current_file); 166 | if (bReopenQuickLook) 167 | quicklook(); 168 | } 169 | } 170 | 171 | function changeSelect(y) { 172 | var oItems = document.getElementById('result').getElementsByTagName("input"); 173 | var iCurIndex = -1; 174 | if (current_file) 175 | for (var i=0; i < oItems.length; i++) { 176 | if (oItems[i] == current_file) { 177 | iCurIndex = i; 178 | break; 179 | } 180 | }; 181 | iCurIndex += y; 182 | if (iCurIndex >= oItems.length) iCurIndex = 0; 183 | if (iCurIndex < 0) iCurIndex = oItems.length - 1; 184 | if (iCurIndex >= 0 && iCurIndex < oItems.length) { 185 | setSelection(oItems[iCurIndex]); 186 | } 187 | else { 188 | setSelection(null); 189 | }; 190 | } 191 | 192 | function insertEscapedSpace() { 193 | var searchBox = document.getElementById('search'); 194 | var query = searchBox.value; 195 | var selStart = searchBox.selectionStart; 196 | searchBox.value = query.substr(0, selStart) + "\\ " + query.substr(searchBox.selectionEnd); 197 | searchBox.selectionStart = selStart + 2; 198 | searchBox.selectionEnd = searchBox.selectionStart; 199 | startSearch(searchBox.value); 200 | } 201 | document.onkeydown = function keyPress(event) { 202 | if (typeof event == "undefined") event = window.event; 203 | wkey = event.keyCode; 204 | if (wkey == 32 || wkey == 27) { 205 | event.stopPropagation(); 206 | event.preventDefault(); 207 | } else if (wkey == 38 || wkey == 40 || wkey == 33 || wkey == 34 || wkey == 9) { 208 | event.stopPropagation(); 209 | event.preventDefault(); 210 | var iDiff = 1; 211 | if (wkey == 33) 212 | iDiff = -10; 213 | else if (wkey == 34) 214 | iDiff = 10; 215 | else if (wkey == 38 || (wkey == 9 && event.shiftKey)) 216 | iDiff = -1; 217 | 218 | changeSelect(iDiff); 219 | } else if (wkey == 13) { 220 | event.stopPropagation(); 221 | event.preventDefault(); 222 | } 223 | }; 224 | document.onkeyup = function keyPress(event) { 225 | if (typeof event == "undefined") event = window.event; 226 | wkey = event.keyCode; 227 | if (wkey == 38 || wkey == 40 || wkey == 33 || wkey == 34 || wkey == 9) { 228 | event.stopPropagation(); 229 | event.preventDefault(); 230 | } 231 | if (wkey == 27) { 232 | if (myCommand) myCommand.cancel(); 233 | window.clearTimeout(progressTimer); 234 | window.clearTimeout(startTimer); 235 | stopProgressWheel(); 236 | if (document.getElementById('search').value == "") 237 | window.close(); 238 | else 239 | document.getElementById('search').value = ""; 240 | event.stopPropagation(); 241 | event.preventDefault(); 242 | } 243 | if (wkey == 32) { 244 | if (event.altKey) 245 | insertEscapedSpace(); 246 | else 247 | quicklook(); 248 | event.stopPropagation(); 249 | event.preventDefault(); 250 | } 251 | if (wkey == 13) { 252 | if (event.shiftKey && event.altKey) insertPath(); 253 | else if (event.shiftKey) insertRelPath(); 254 | else if (event.altKey) openFile(); 255 | else gotofile(); 256 | event.stopPropagation(); 257 | event.preventDefault(); 258 | } 259 | }; -------------------------------------------------------------------------------- /Support/lib/fuzzy_file_finder.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # ================================================================== 3 | # Author: Jamis Buck (jamis@jamisbuck.org) 4 | # Date: 2008-10-09 5 | # 6 | # This file is in the public domain. Usage, modification, and 7 | # redistribution of this file are unrestricted. 8 | # ================================================================== 9 | #++ 10 | 11 | # The "fuzzy" file finder provides a way for searching a directory 12 | # tree with only a partial name. This is similar to the "cmd-T" 13 | # feature in TextMate (http://macromates.com). 14 | # 15 | # Usage: 16 | # 17 | # finder = FuzzyFileFinder.new 18 | # finder.search("app/blogcon") do |match| 19 | # puts match[:highlighted_path] 20 | # end 21 | # 22 | # In the above example, all files matching "app/blogcon" will be 23 | # yielded to the block. The given pattern is reduced to a regular 24 | # expression internally, so that any file that contains those 25 | # characters in that order (even if there are other characters 26 | # in between) will match. 27 | # 28 | # In other words, "app/blogcon" would match any of the following 29 | # (parenthesized strings indicate how the match was made): 30 | # 31 | # * (app)/controllers/(blog)_(con)troller.rb 32 | # * lib/c(ap)_(p)ool/(bl)ue_(o)r_(g)reen_(co)loratio(n) 33 | # * test/(app)/(blog)_(con)troller_test.rb 34 | # 35 | # And so forth. 36 | 37 | require 'CGI' 38 | 39 | class FuzzyFileFinder 40 | module Version 41 | MAJOR = 1 42 | MINOR = 0 43 | TINY = 4 44 | STRING = [MAJOR, MINOR, TINY].join(".") 45 | end 46 | 47 | # This is the exception that is raised if you try to scan a 48 | # directory tree with too many entries. By default, a ceiling of 49 | # 10,000 entries is enforced, but you can change that number via 50 | # the +ceiling+ parameter to FuzzyFileFinder.new. 51 | class TooManyEntries < RuntimeError; end 52 | 53 | # Used internally to represent a run of characters within a 54 | # match. This is used to build the highlighted version of 55 | # a file name. 56 | class CharacterRun < Struct.new(:string, :inside) #:nodoc: 57 | def to_s 58 | if inside 59 | "(#{string})" 60 | else 61 | string 62 | end 63 | end 64 | end 65 | 66 | # Just like CharacterRun except outputs HTML. 67 | class HtmlCharacterRun < Struct.new(:string, :inside) #:nodoc: 68 | def to_s 69 | if inside 70 | "#{CGI.escapeHTML(string)}" 71 | else 72 | CGI.escapeHTML(string) 73 | end 74 | end 75 | end 76 | 77 | # Used internally to represent a file within the directory tree. 78 | class FileSystemEntry #:nodoc: 79 | attr_reader :parent 80 | attr_reader :name 81 | 82 | def initialize(parent, name) 83 | @parent = parent 84 | @name = name 85 | end 86 | 87 | def path 88 | File.join(parent.name, name) 89 | end 90 | end 91 | 92 | # Used internally to represent a subdirectory within the directory 93 | # tree. 94 | class Directory #:nodoc: 95 | attr_reader :name 96 | 97 | def initialize(name, is_root=false) 98 | @name = name 99 | @is_root = is_root 100 | end 101 | 102 | def root? 103 | is_root 104 | end 105 | end 106 | 107 | # The roots directory trees to search. 108 | attr_reader :roots 109 | 110 | # The list of files beneath all +roots+ 111 | attr_reader :files 112 | 113 | # The maximum number of files beneath all +roots+ 114 | attr_reader :ceiling 115 | 116 | # The prefix shared by all +roots+. 117 | attr_reader :shared_prefix 118 | 119 | # The list of glob patterns to ignore. 120 | attr_reader :ignores 121 | 122 | # The class used to output the highlighted text in the desired format. 123 | attr_reader :highlighted_match_class 124 | 125 | # Initializes a new FuzzyFileFinder. This will scan the 126 | # given +directories+, using +ceiling+ as the maximum number 127 | # of entries to scan. If there are more than +ceiling+ entries 128 | # a TooManyEntries exception will be raised. 129 | def initialize(directories=['.'], ceiling=10_000, ignores=nil, highlighter=CharacterRun) 130 | directories = Array(directories) 131 | directories << "." if directories.empty? 132 | 133 | # expand any paths with ~ 134 | root_dirnames = directories.map { |d| File.expand_path(d) }.select { |d| File.directory?(d) }.uniq 135 | 136 | @roots = root_dirnames.map { |d| Directory.new(d, true) } 137 | @shared_prefix = determine_shared_prefix 138 | @shared_prefix_re = Regexp.new("^#{Regexp.escape(shared_prefix)}" + (shared_prefix.empty? ? "" : "/")) 139 | 140 | @files = [] 141 | @ceiling = ceiling 142 | 143 | @ignores = Array(ignores) 144 | 145 | @highlighted_match_class = highlighter 146 | 147 | rescan! 148 | end 149 | 150 | # Rescans the subtree. If the directory contents every change, 151 | # you'll need to call this to force the finder to be aware of 152 | # the changes. 153 | def rescan! 154 | @files.clear 155 | roots.each { |root| follow_tree(root) } 156 | end 157 | 158 | # Takes the given +pattern+ (which must be a string) and searches 159 | # all files beneath +root+, yielding each match. 160 | # 161 | # +pattern+ is interpreted thus: 162 | # 163 | # * "foo" : look for any file with the characters 'f', 'o', and 'o' 164 | # in its basename (discounting directory names). The characters 165 | # must be in that order. 166 | # * "foo/bar" : look for any file with the characters 'b', 'a', 167 | # and 'r' in its basename (discounting directory names). Also, 168 | # any successful match must also have at least one directory 169 | # element matching the characters 'f', 'o', and 'o' (in that 170 | # order. 171 | # * "foo/bar/baz" : same as "foo/bar", but matching two 172 | # directory elements in addition to a file name of "baz". 173 | # 174 | # Each yielded match will be a hash containing the following keys: 175 | # 176 | # * :path refers to the full path to the file 177 | # * :directory refers to the directory of the file 178 | # * :name refers to the name of the file (without directory) 179 | # * :highlighted_directory refers to the directory of the file with 180 | # matches highlighted in parentheses. 181 | # * :highlighted_name refers to the name of the file with matches 182 | # highlighted in parentheses 183 | # * :highlighted_path refers to the full path of the file with 184 | # matches highlighted in parentheses 185 | # * :abbr refers to an abbreviated form of :highlighted_path, where 186 | # path segments without matches are compressed to just their first 187 | # character. 188 | # * :score refers to a value between 0 and 1 indicating how closely 189 | # the file matches the given pattern. A score of 1 means the 190 | # pattern matches the file exactly. 191 | def search(pattern, &block) 192 | #pattern.gsub!(" ", "") 193 | path_parts = pattern.split("/") 194 | path_parts.push "" if pattern[-1,1] == "/" 195 | 196 | file_name_part = path_parts.pop || "" 197 | 198 | if path_parts.any? 199 | path_regex_raw = "^(.*?)" + path_parts.map { |part| make_pattern(part) }.join("(.*?/.*?)") + "(.*?)$" 200 | path_regex = Regexp.new(path_regex_raw, Regexp::IGNORECASE) 201 | end 202 | 203 | file_regex_raw = "^(.*?)" << make_pattern(file_name_part) << "(.*)$" 204 | file_regex = Regexp.new(file_regex_raw, Regexp::IGNORECASE) 205 | 206 | path_matches = {} 207 | files.each do |file| 208 | path_match = match_path(file.parent, path_matches, path_regex, path_parts.length) 209 | next if path_match[:missed] 210 | 211 | match_file(file, file_regex, path_match, &block) 212 | end 213 | end 214 | 215 | # Takes the given +pattern+ (which must be a string, formatted as 216 | # described in #search), and returns up to +max+ matches in an 217 | # Array. If +max+ is nil, all matches will be returned. 218 | def find(pattern, max=nil) 219 | results = [] 220 | search(pattern) do |match| 221 | results << match 222 | break if max && results.length >= max 223 | end 224 | return results 225 | end 226 | 227 | # Displays the finder object in a sane, non-explosive manner. 228 | def inspect #:nodoc: 229 | "#<%s:0x%x roots=%s, files=%d>" % [self.class.name, object_id, roots.map { |r| r.name.inspect }.join(", "), files.length] 230 | end 231 | 232 | private 233 | 234 | # Recursively scans +directory+ and all files and subdirectories 235 | # beneath it, depth-first. 236 | def follow_tree(directory) 237 | Dir.entries(directory.name).each do |entry| 238 | next if entry[0,1] == "." 239 | raise TooManyEntries if files.length > ceiling 240 | 241 | full = File.join(directory.name, entry) 242 | 243 | if File.directory?(full) 244 | follow_tree(Directory.new(full)) 245 | elsif !ignore?(full.sub(@shared_prefix_re, "")) 246 | files.push(FileSystemEntry.new(directory, entry)) 247 | end 248 | end 249 | end 250 | 251 | # Returns +true+ if the given name matches any of the ignore 252 | # patterns. 253 | def ignore?(name) 254 | ignores.any? { |pattern| File.fnmatch(pattern, name) } 255 | end 256 | 257 | # Takes the given pattern string "foo" and converts it to a new 258 | # string "(f)([^/]*?)(o)([^/]*?)(o)" that can be used to create 259 | # a regular expression. 260 | def make_pattern(pattern) 261 | pattern = pattern.split(//) 262 | pattern << "" if pattern.empty? 263 | 264 | pattern.inject("") do |regex, character| 265 | regex << "([^/]*?)" if regex.length > 0 266 | regex << "(" << Regexp.escape(character) << ")" 267 | end 268 | end 269 | 270 | # Given a MatchData object +match+ and a number of "inside" 271 | # segments to support, compute both the match score and the 272 | # highlighted match string. The "inside segments" refers to how 273 | # many patterns were matched in this one match. For a file name, 274 | # this will always be one. For directories, it will be one for 275 | # each directory segment in the original pattern. 276 | def build_match_result(match, inside_segments) 277 | runs = [] 278 | inside_chars = total_chars = 0 279 | is_word_prefixes = inside_segments == 1 280 | match.captures.each_with_index do |capture, index| 281 | if capture.length > 0 282 | # odd-numbered captures are matches inside the pattern. 283 | # even-numbered captures are matches between the pattern's elements. 284 | inside = index % 2 != 0 285 | 286 | total_chars += capture.gsub(%r(/), "").length # ignore '/' delimiters 287 | inside_chars += capture.length if inside 288 | 289 | if runs.last && runs.last.inside == inside 290 | runs.last.string << capture 291 | else 292 | runs << @highlighted_match_class.new(capture, inside) 293 | end 294 | 295 | if !inside && is_word_prefixes && index != match.captures.length - 1 296 | if capture.match(/[A-Za-z]$/i) #if this inbetween item finishes with a letter, the next is not an initial letter 297 | is_word_prefixes = false 298 | end 299 | end 300 | end 301 | end 302 | 303 | # Determine the score of this match. 304 | # 1. fewer "inside runs" (runs corresponding to the original pattern) 305 | # is better. 306 | # 2. better coverage of the actual path name is better 307 | 308 | inside_runs = runs.select { |r| r.inside } 309 | run_ratio = inside_runs.length.zero? ? 1 : inside_segments / inside_runs.length.to_f 310 | 311 | char_ratio = total_chars.zero? ? 1 : inside_chars.to_f / total_chars 312 | 313 | score = run_ratio * char_ratio 314 | 315 | return { :score => score, :result => runs.join, :is_word_start_match => is_word_prefixes } 316 | end 317 | 318 | # Match the given path against the regex, caching the result in +path_matches+. 319 | # If +path+ is already cached in the path_matches cache, just return the cached 320 | # value. 321 | def match_path(path, path_matches, path_regex, path_segments) 322 | return path_matches[path] if path_matches.key?(path) 323 | 324 | name_with_slash = path.name + "/" # add a trailing slash for matching the prefix 325 | matchable_name = name_with_slash.sub(@shared_prefix_re, "") 326 | matchable_name.chop! # kill the trailing slash 327 | 328 | if path_regex 329 | match = matchable_name.match(path_regex) 330 | 331 | path_matches[path] = 332 | match && build_match_result(match, path_segments) || 333 | { :score => 1, :result => matchable_name, :missed => true } 334 | else 335 | path_matches[path] = { :score => 1, :result => matchable_name } 336 | end 337 | end 338 | 339 | # Match +file+ against +file_regex+. If it matches, yield the match 340 | # metadata to the block. 341 | def match_file(file, file_regex, path_match, &block) 342 | if file_match = file.name.match(file_regex) 343 | match_result = build_match_result(file_match, 1) 344 | full_match_result = path_match[:result].empty? ? match_result[:result] : File.join(path_match[:result], match_result[:result]) 345 | shortened_path = path_match[:result].gsub(/[^\/]+/) { |m| m.index("(") ? m : m[0,1] } 346 | abbr = shortened_path.empty? ? match_result[:result] : File.join(shortened_path, match_result[:result]) 347 | plain_score = (path_match[:score] * match_result[:score]) / 2.0 348 | 349 | result = { :path => file.path, 350 | :abbr => abbr, 351 | :directory => file.parent.name, 352 | :name => file.name, 353 | :highlighted_directory => path_match[:result], 354 | :highlighted_name => match_result[:result], 355 | :highlighted_path => full_match_result, 356 | :score => (match_result[:is_word_start_match] ? 0.5 : 0) + plain_score } 357 | yield result 358 | end 359 | end 360 | 361 | def determine_shared_prefix 362 | # the common case: if there is only a single root, then the entire 363 | # name of the root is the shared prefix. 364 | return roots.first.name if roots.length == 1 365 | 366 | split_roots = roots.map { |root| root.name.split(%r{/}) } 367 | segments = split_roots.map { |root| root.length }.max 368 | master = split_roots.pop 369 | 370 | segments.times do |segment| 371 | if !split_roots.all? { |root| root[segment] == master[segment] } 372 | return master[0,segment].join("/") 373 | end 374 | end 375 | 376 | # shouldn't ever get here, since we uniq the root list before 377 | # calling this method, but if we do, somehow... 378 | return roots.first.name 379 | end 380 | end 381 | --------------------------------------------------------------------------------