xiki 0.5.0a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. data/Gemfile +11 -0
  2. data/LICENSE +22 -0
  3. data/README.markdown +83 -0
  4. data/Rakefile +8 -0
  5. data/bin/xiki +46 -0
  6. data/etc/command/xiki_command.rb +203 -0
  7. data/etc/command/xiki_process.rb +52 -0
  8. data/etc/command/xiki_wrapper +2 -0
  9. data/etc/js/menu1.js +55 -0
  10. data/etc/js/xiki.js +259 -0
  11. data/etc/logo.png +0 -0
  12. data/etc/presentations/bootstrap.deck +5 -0
  13. data/etc/presentations/databases.deck +41 -0
  14. data/etc/presentations/diffs.deck +23 -0
  15. data/etc/presentations/documentation.deck +14 -0
  16. data/etc/presentations/effects.deck +5 -0
  17. data/etc/presentations/face.deck +297 -0
  18. data/etc/presentations/files.deck +79 -0
  19. data/etc/presentations/icons.deck +22 -0
  20. data/etc/presentations/images.deck +24 -0
  21. data/etc/presentations/key_shortcuts.deck +16 -0
  22. data/etc/presentations/macros.deck +18 -0
  23. data/etc/presentations/menu_classes.deck +44 -0
  24. data/etc/presentations/menu_directories.deck +30 -0
  25. data/etc/presentations/notes.deck +19 -0
  26. data/etc/presentations/other_languages.deck +55 -0
  27. data/etc/presentations/other_wiki_syntaxes.deck +4 -0
  28. data/etc/presentations/piano.deck +5 -0
  29. data/etc/presentations/potential/diffs.deck +38 -0
  30. data/etc/presentations/potential/evolution.deck +18 -0
  31. data/etc/presentations/potential/intro.deck +711 -0
  32. data/etc/presentations/potential/intro1.deck +711 -0
  33. data/etc/presentations/potential/intro2.deck +97 -0
  34. data/etc/presentations/potential/make_mysql_menu.deck +36 -0
  35. data/etc/presentations/potential/ruby_development.deck +17 -0
  36. data/etc/presentations/potential/ui_prototyping.deck +50 -0
  37. data/etc/presentations/potential/web_dev.deck +4 -0
  38. data/etc/presentations/potential/web_development.deck +10 -0
  39. data/etc/presentations/potential/wiki.deck +45 -0
  40. data/etc/presentations/presentations.deck +24 -0
  41. data/etc/presentations/rails_development.deck +29 -0
  42. data/etc/presentations/search_key_shortcuts.deck +37 -0
  43. data/etc/presentations/simplest_possible_ui.deck +37 -0
  44. data/etc/presentations/svg.deck +5 -0
  45. data/etc/presentations/testing.deck +28 -0
  46. data/etc/presentations/the_end.deck +13 -0
  47. data/etc/presentations/type_something_and_double_click.deck +57 -0
  48. data/etc/presentations/type_the_acronym.deck +144 -0
  49. data/etc/presentations/web_browser.deck +56 -0
  50. data/etc/presentations/xiki_command.deck +20 -0
  51. data/etc/presentations/xiki_url.deck +14 -0
  52. data/etc/shark.icns +0 -0
  53. data/etc/shark_script.icns +0 -0
  54. data/etc/snippets/html.notes +7 -0
  55. data/etc/snippets/notes.notes +20 -0
  56. data/etc/snippets/rb.notes +38 -0
  57. data/etc/templates/menu_template.menu +2 -0
  58. data/etc/templates/menu_template.rb +8 -0
  59. data/etc/templates/template.rb +5 -0
  60. data/etc/themes/Dark_Metal.notes +28 -0
  61. data/etc/themes/Orange_Path.notes +15 -0
  62. data/etc/themes/Shiny_Blue.notes +27 -0
  63. data/etc/themes/Shiny_Green.notes +27 -0
  64. data/etc/wrappers/wrapper.js +17 -0
  65. data/etc/wrappers/wrapper.py +20 -0
  66. data/etc/wrappers/wrapper.rb +25 -0
  67. data/lib/block.rb +72 -0
  68. data/lib/bookmarks.rb +352 -0
  69. data/lib/buffers.rb +170 -0
  70. data/lib/clipboard.rb +333 -0
  71. data/lib/code.rb +860 -0
  72. data/lib/code_tree.rb +476 -0
  73. data/lib/color.rb +274 -0
  74. data/lib/console.rb +557 -0
  75. data/lib/control_lock.rb +9 -0
  76. data/lib/control_tab.rb +176 -0
  77. data/lib/core_ext.rb +31 -0
  78. data/lib/cursor.rb +111 -0
  79. data/lib/deletes.rb +65 -0
  80. data/lib/diff_log.rb +297 -0
  81. data/lib/effects.rb +145 -0
  82. data/lib/environment.rb +5 -0
  83. data/lib/file_tree.rb +1875 -0
  84. data/lib/files.rb +334 -0
  85. data/lib/hide.rb +259 -0
  86. data/lib/history.rb +286 -0
  87. data/lib/image.rb +51 -0
  88. data/lib/incrementer.rb +15 -0
  89. data/lib/insert.rb +7 -0
  90. data/lib/irc.rb +22 -0
  91. data/lib/key_bindings.rb +658 -0
  92. data/lib/keys.rb +754 -0
  93. data/lib/launcher.rb +1351 -0
  94. data/lib/line.rb +429 -0
  95. data/lib/links.rb +6 -0
  96. data/lib/location.rb +175 -0
  97. data/lib/macros.rb +48 -0
  98. data/lib/man.rb +19 -0
  99. data/lib/menu.rb +708 -0
  100. data/lib/merb.rb +259 -0
  101. data/lib/message.rb +5 -0
  102. data/lib/meths.rb +56 -0
  103. data/lib/mode.rb +34 -0
  104. data/lib/move.rb +248 -0
  105. data/lib/notes.rb +1000 -0
  106. data/lib/numbers.rb +45 -0
  107. data/lib/ol.rb +203 -0
  108. data/lib/ol_helper.rb +44 -0
  109. data/lib/overlay.rb +167 -0
  110. data/lib/pause_means_space.rb +68 -0
  111. data/lib/php.rb +22 -0
  112. data/lib/projects.rb +21 -0
  113. data/lib/relinquish_exception.rb +2 -0
  114. data/lib/remote.rb +206 -0
  115. data/lib/requirer.rb +46 -0
  116. data/lib/rest_tree.rb +108 -0
  117. data/lib/ruby.rb +57 -0
  118. data/lib/ruby_console.rb +165 -0
  119. data/lib/search.rb +1572 -0
  120. data/lib/search_term.rb +40 -0
  121. data/lib/snippet.rb +68 -0
  122. data/lib/specs.rb +229 -0
  123. data/lib/styles.rb +274 -0
  124. data/lib/svn.rb +682 -0
  125. data/lib/text_util.rb +110 -0
  126. data/lib/tree.rb +1871 -0
  127. data/lib/tree_cursor.rb +87 -0
  128. data/lib/trouble_shooting.rb +27 -0
  129. data/lib/url_tree.rb +11 -0
  130. data/lib/view.rb +1474 -0
  131. data/lib/window.rb +133 -0
  132. data/lib/xiki.rb +404 -0
  133. data/menus/accounts.rb +5 -0
  134. data/menus/address_book.rb +21 -0
  135. data/menus/agenda.rb +28 -0
  136. data/menus/all.rb +5 -0
  137. data/menus/amazon.rb +16 -0
  138. data/menus/app.rb +16 -0
  139. data/menus/applescript.rb +46 -0
  140. data/menus/as.rb +15 -0
  141. data/menus/bookmarklet.rb +63 -0
  142. data/menus/bootstrap.rb +568 -0
  143. data/menus/browse.rb +13 -0
  144. data/menus/browser.rb +78 -0
  145. data/menus/cassandra_db.rb +36 -0
  146. data/menus/chmod.rb +27 -0
  147. data/menus/classes.rb +5 -0
  148. data/menus/coffee_script.rb +35 -0
  149. data/menus/computer.rb +24 -0
  150. data/menus/contacts.rb +5 -0
  151. data/menus/cookies.rb +25 -0
  152. data/menus/couch.rb +184 -0
  153. data/menus/crop.rb +45 -0
  154. data/menus/css.rb +55 -0
  155. data/menus/current.rb +5 -0
  156. data/menus/db.rb +12 -0
  157. data/menus/deck.rb +219 -0
  158. data/menus/dictionary.rb +9 -0
  159. data/menus/dir.rb +8 -0
  160. data/menus/disk.rb +5 -0
  161. data/menus/do.rb +13 -0
  162. data/menus/docs.rb +58 -0
  163. data/menus/dotsies.rb +107 -0
  164. data/menus/edited.rb +18 -0
  165. data/menus/emacs.rb +17 -0
  166. data/menus/enter.rb +13 -0
  167. data/menus/eval.rb +17 -0
  168. data/menus/executable.rb +16 -0
  169. data/menus/filter.rb +46 -0
  170. data/menus/firefox.rb +607 -0
  171. data/menus/foo.rb +30 -0
  172. data/menus/french.rb +7 -0
  173. data/menus/git.rb +185 -0
  174. data/menus/gito.rb +785 -0
  175. data/menus/google.rb +35 -0
  176. data/menus/google_images.rb +11 -0
  177. data/menus/google_patents.rb +10 -0
  178. data/menus/gutenberg.rb +13 -0
  179. data/menus/head.rb +10 -0
  180. data/menus/headings.rb +39 -0
  181. data/menus/html.rb +61 -0
  182. data/menus/icon.rb +40 -0
  183. data/menus/images.menu +2 -0
  184. data/menus/img.rb +15 -0
  185. data/menus/info.rb +9 -0
  186. data/menus/ip.rb +10 -0
  187. data/menus/iterm.rb +36 -0
  188. data/menus/itunes.rb +78 -0
  189. data/menus/javascript.rb +74 -0
  190. data/menus/layout.rb +18 -0
  191. data/menus/local_storage.rb +67 -0
  192. data/menus/ls.rb +19 -0
  193. data/menus/mac.rb +87 -0
  194. data/menus/maps.rb +18 -0
  195. data/menus/matches.rb +18 -0
  196. data/menus/memcache.rb +117 -0
  197. data/menus/mkdir.rb +23 -0
  198. data/menus/mongo.rb +83 -0
  199. data/menus/mysql.rb +294 -0
  200. data/menus/node.rb +88 -0
  201. data/menus/open.rb +19 -0
  202. data/menus/outline.rb +24 -0
  203. data/menus/piano.rb +746 -0
  204. data/menus/postgres.rb +34 -0
  205. data/menus/pre.rb +5 -0
  206. data/menus/python.rb +39 -0
  207. data/menus/rails.rb +160 -0
  208. data/menus/rake.rb +12 -0
  209. data/menus/redmine.rb +168 -0
  210. data/menus/riak_tree.rb +204 -0
  211. data/menus/rss.rb +15 -0
  212. data/menus/safari.rb +11 -0
  213. data/menus/sass.rb +15 -0
  214. data/menus/say.rb +6 -0
  215. data/menus/scale.rb +49 -0
  216. data/menus/serve.rb +78 -0
  217. data/menus/shuffle.rb +24 -0
  218. data/menus/spanish.rb +7 -0
  219. data/menus/standalone.rb +57 -0
  220. data/menus/tail.rb +41 -0
  221. data/menus/technologies.rb +19 -0
  222. data/menus/themes.rb +32 -0
  223. data/menus/thesaurus.rb +13 -0
  224. data/menus/to.rb +24 -0
  225. data/menus/twitter.rb +57 -0
  226. data/menus/wikipedia.rb +34 -0
  227. data/menus/words.rb +11 -0
  228. data/spec/code_tree_spec.rb +59 -0
  229. data/spec/diff_log_spec.rb +40 -0
  230. data/spec/file_tree_spec.rb +102 -0
  231. data/spec/keys_spec.rb +24 -0
  232. data/spec/line_spec.rb +68 -0
  233. data/spec/menu_spec.rb +50 -0
  234. data/spec/ol_spec.rb +98 -0
  235. data/spec/remote_spec.rb +43 -0
  236. data/spec/search_spec.rb +162 -0
  237. data/spec/text_util_spec.rb +119 -0
  238. data/spec/tree_cursor_spec.rb +91 -0
  239. data/spec/tree_spec.rb +955 -0
  240. data/tests/console_test.rb +11 -0
  241. data/tests/couch_db_test.rb +12 -0
  242. data/tests/diff_log_test.rb +43 -0
  243. data/tests/el_mixin.rb +16 -0
  244. data/tests/git_test.rb +95 -0
  245. data/tests/keys_test.rb +19 -0
  246. data/tests/line_test.rb +38 -0
  247. data/tests/merb_test.rb +11 -0
  248. data/tests/redmine_test.rb +50 -0
  249. data/tests/remote_test.rb +31 -0
  250. data/tests/rest_tree_test.rb +70 -0
  251. data/xiki.gemspec +37 -0
  252. metadata +332 -0
@@ -0,0 +1,110 @@
1
+ require 'line'
2
+ require 'ol'
3
+
4
+ class TextUtil
5
+
6
+ def self.menu
7
+ %`
8
+ > Change case
9
+ @ TextUtil.camel_case "hey you"
10
+ @ TextUtil.hyphen_case "hey you"
11
+ @ TextUtil.snake_case "hey you"
12
+ @ TextUtil.title_case "hey you"
13
+
14
+ > Modify vars in-place
15
+ You can also use bang versions, like:
16
+ @ s = "hey you"; TextUtil.camel_case! s; p s
17
+
18
+ > Unindent
19
+ @ TextUtil.unindent "hey you"
20
+ `
21
+ end
22
+
23
+ def self.case_choices
24
+ [
25
+ ['upper', lambda {|o| o.upcase}],
26
+ ['lower', lambda {|o| o.downcase}],
27
+ ['title', lambda {|o| TextUtil.title_case(o)}],
28
+ ['camel', lambda {|o| TextUtil.camel_case(o)}],
29
+ ['snake', lambda {|o| TextUtil.snake_case(o)}],
30
+ ['plus', lambda {|o| TextUtil.plus_case(o)}],
31
+ ['hyphen', lambda {|o| TextUtil.hyphen_case(o)}],
32
+ ]
33
+ end
34
+
35
+ def self.unindent txt, options={}
36
+ txt = txt.sub(/\A\n/, '')# if ! options[:no_strip] # Delete initial blank line if there
37
+
38
+ txt.gsub!(/^\s+/) { |t| t.gsub("\t", ' ') } # Untab indent
39
+
40
+ # If 1st line has no indent and 2nd line has indent (at least 3 spaces)
41
+ if txt !~ /\A / and txt =~ /\A.+\n( +)/
42
+ indent = $1 # Indent left by 2nd indent
43
+ return txt.gsub(/^#{indent}/, '')
44
+ end
45
+
46
+ old_indent = Line.indent(txt) # Get indent of first line
47
+
48
+ txt.gsub!(/^#{old_indent}/, '') # Delete current indent
49
+ "#{txt.strip}\n"
50
+ end
51
+
52
+ # TextUtil.snake_case("hi there") # -> hi_there
53
+ def self.snake_case s
54
+ s.gsub(/[ -]/, '_').
55
+ gsub(/([a-z0-9])([A-Z])/) {"#{$1}_#{$2}"}.downcase.
56
+ gsub(/[^\w]/, "").
57
+ gsub(/__+/, "_")
58
+ end
59
+
60
+ # TextUtil.plus_case("hi there") # -> hi+there
61
+ def self.plus_case s
62
+ self.snake_case(s).gsub('_', '+')
63
+ end
64
+
65
+ def self.snake_case! s
66
+ s.replace self.snake_case(s)
67
+ end
68
+
69
+ # TextUtil.hyphen_case("hi there") # -> hi-there
70
+ def self.hyphen_case s
71
+ s.gsub(/[ _]/, '-').
72
+ gsub(/([a-z])([A-Z0-9])/) {"#{$1}-#{$2}"}.downcase.
73
+ gsub(/--+/, "-")
74
+ end
75
+
76
+ # TextUtil.camel_case("hi there") # -> HiThere
77
+ def self.camel_case s
78
+ # If it's all capitals, make subsequent copitals lowercase
79
+ if s =~ /^[A-Z_-]+$/
80
+ s = s.gsub(/([A-Z][A-Z]+)/) {"#{$1.capitalize}"}
81
+ end
82
+
83
+ s.gsub(/[ -]/, '_').
84
+ gsub(/_([a-z]+)/) {"#{$1.capitalize}"}.
85
+ sub(/(.)/) {$1.upcase}.
86
+ gsub("_", "").
87
+ gsub(/[^\w]/, "")
88
+ end
89
+
90
+ def self.camel_case! s
91
+ s.replace self.camel_case(s)
92
+ end
93
+
94
+ # TextUtil.title_case("hi there") # -> Hi There
95
+ def self.title_case s, options={}
96
+ s = s.gsub(/[ -]/, '_').
97
+ gsub(/([a-z])([A-Z0-9])/) {"#{$1}_#{$2}"}.downcase.
98
+ gsub(/([a-z]+)/) {"#{$1.capitalize}"}.
99
+ gsub(/__*/, " ")
100
+
101
+ s.gsub! " ", "_" if options[:underscores]
102
+
103
+ s
104
+ end
105
+
106
+ def self.title_case! s
107
+ s.replace self.title_case(s)
108
+ end
109
+
110
+ end
@@ -0,0 +1,1871 @@
1
+ require 'tree_cursor'
2
+
3
+ class Tree
4
+ def self.menu
5
+ "
6
+ - api/
7
+ > Get dir (or file) menu is nested under
8
+ @ p Tree.file
9
+ @ p Tree.file :require=>1 # Raise message if not nested under dir or file
10
+ @ p Tree.file :require=>'file' # Raise message if not nested under file
11
+ @ p Tree.file :require=>'dir' # Raise message if not nested under dir
12
+
13
+ > Show siblings
14
+ @ p Tree.siblings
15
+
16
+ | Include all siblings (current line is usually ommited), just
17
+ | siblings before, or just siblings after:
18
+ @ p Tree.siblings :all=>1
19
+ @ p Tree.siblings :after=>1
20
+ @ p Tree.siblings :before=>1
21
+ @ p Tree.siblings :string=>1 # Consecutive lines, quotes removed
22
+
23
+ > Moving around
24
+ @ Tree.to_parent # Go to parent, regardless of blanks
25
+ @ Tree.after_children # Go after children of this element, crossing blank lines
26
+ @ Tree.before_siblings # Jumps to first sibling, crossing blank lines, but not double-blank lines
27
+ @ Tree.after_siblings # Go after last sibling, crossing blank lines, but not double-blank lines
28
+
29
+ > All methods
30
+ @ Tree.meths
31
+ "
32
+ end
33
+
34
+ def self.search options={}
35
+ return $xiki_no_search=false if $xiki_no_search
36
+
37
+ recursive = options[:recursive]
38
+ recursive_quotes = options[:recursive_quotes]
39
+ left, right = options[:left], options[:right]
40
+ if ! left
41
+ ignore, left, right = View.block_positions "^>"
42
+ end
43
+
44
+ # No search if there aren't more than 3 lines
45
+ return if((Line.number(right) - Line.number(left)) <= 1 && View.txt(left, right) !~ /\/$/) && options[:always_search].nil?
46
+
47
+ # Make cursor blue
48
+ Cursor.remember :before_file_tree
49
+ Cursor.blue
50
+ error = ""
51
+
52
+ pattern = ""
53
+ lines = $el.buffer_substring(left, right).split "\n"
54
+
55
+ ch = nil
56
+
57
+ if options[:first_letter]
58
+ ch, ch_raw = self.first_letter lines
59
+ return if ! ch
60
+ end
61
+
62
+ Message << "filter... "
63
+
64
+ ch, ch_raw = Keys.char if ch.nil?
65
+
66
+ if ch.nil?
67
+ return Cursor.restore(:before_file_tree)
68
+ end
69
+
70
+ # While chars to search for (alpha chars etc.), narrow down list...
71
+
72
+ while (ch =~ /[ -"&-),-.\/-:<?A-~]/ && # Be careful editing, due to ranges (_-_)
73
+ (ch_raw < 67108912 || ch_raw > 67108921) && ch_raw != 67108909) || # If not control-<number> or C--
74
+ (recursive && ch_raw == 2 || ch_raw == 6) ||
75
+ ch == :up || ch == :down
76
+
77
+ if ch == ' ' && pattern != "" # If space and not already cleared out
78
+ pattern = ''
79
+ elsif ch_raw == 2 # C-b
80
+ while(Line.previous == 0)
81
+ next if FileTree.dir? # Keep going if line is a dir
82
+ Line.to_words
83
+ break # Otherwise, stop
84
+ end
85
+
86
+ elsif ch_raw == 6 # C-f
87
+ while(Line.next == 0)
88
+ next if FileTree.dir? # Keep going if line is a dir
89
+ Line.to_words
90
+ break # Otherwise, stop
91
+ end
92
+ elsif ch == :up
93
+ Line.previous
94
+ Line.to_words
95
+ elsif ch == :down
96
+ Line.next
97
+ Line.to_words
98
+
99
+ else
100
+ if ch == "\\" # If escape, get real char
101
+ ch = $el.char_to_string($el.read_char)
102
+ end
103
+ pattern << Regexp.quote(ch)
104
+
105
+ if pattern =~ /[A-Z]$/ # If upper, remove any lower
106
+ pattern.sub!(/^[a-z]+/, '')
107
+ elsif pattern =~ /[a-z]$/ # If lower, remove any upper
108
+ pattern.sub!(/^[A-Z]+/, '')
109
+ end
110
+
111
+ regexp = pattern
112
+
113
+ $el.delete_region left, right
114
+
115
+ regexp = "\\/$|#{regexp}" if recursive
116
+ # Always keep if "- file" or "- /dir"
117
+ regexp = "^ *:\\d|^ *[+-] [a-zA-Z0-9@:.\/]|#{regexp}" if recursive_quotes
118
+
119
+ regexp = /#{regexp}/i
120
+ lines_new = nil
121
+ if pattern =~ /[A-Z]$/ # If upper, search in directory
122
+ lines_new = search_dir_names(lines, /#{pattern}/i)
123
+ else
124
+ lines_new = lines.grep(regexp)
125
+ end
126
+ # If search not found, don't delete all
127
+ if lines_new.size == 0
128
+ error = " ---------- no matches! ---------- "
129
+ View.beep
130
+ else
131
+ lines = lines_new
132
+ end
133
+
134
+ # Remove dirs with nothing under them
135
+ self.clear_empty_dirs! lines if recursive
136
+ self.clear_empty_dirs!(lines, :quotes=>true) if recursive_quotes
137
+
138
+ # Put back into buffer
139
+ View.insert(lines.join("\n") + "\n")
140
+ right = $el.point
141
+
142
+ # Go to first file
143
+ $el.goto_char left
144
+
145
+ # Move to first file
146
+ if recursive
147
+ FileTree.select_next_file
148
+ elsif recursive_quotes
149
+ Search.forward "|\\|#"
150
+ Line.to_beginning
151
+ else
152
+ Line.to_beginning
153
+ end
154
+
155
+ end
156
+
157
+ message = "filter... #{pattern}#{error}"
158
+ message << " (space for 'and')" if pattern.present?
159
+ Message << message
160
+ ch, ch_raw = Keys.char
161
+ if ch.nil?
162
+ return Cursor.restore(:before_file_tree)
163
+ end
164
+
165
+ end
166
+
167
+ Cursor.restore :before_file_tree # Exiting, so restore cursor
168
+
169
+ # Options during search
170
+
171
+ # Do something based on char that exited search, like run as command...
172
+
173
+ case ch
174
+ when "0"
175
+ file = self.construct_path # Expand out ~
176
+ # Open in OS
177
+ $el.shell_command("open #{file}")
178
+ # when "\C-a"
179
+ # Line.to_left
180
+ when "\C-j"
181
+ ch = Keys.input :chars=>1
182
+ if ch == 't' # just_time
183
+ self.to_parent
184
+ self.kill_under
185
+ FileTree.dir :date_sort=>true
186
+ elsif ch == 's' # just_size
187
+ self.to_parent
188
+ self.kill_under
189
+ FileTree.dir :size_sort=>true
190
+ elsif ch == 'n' # just_name
191
+ self.to_parent
192
+ self.kill_under
193
+ FileTree.dir
194
+ end
195
+ when :return # If return, just stop (like isearch)
196
+ # Do nothing
197
+
198
+ Keys.clear_prefix
199
+ Launcher.launch
200
+
201
+ # when :control_return, :return, "\C-m", :control_period, :right # If C-., go in but don't collapse siblings
202
+ when :control_return, "\C-m", :control_period, :right # If C-., go in but don't collapse siblings
203
+ Keys.clear_prefix
204
+ Launcher.launch
205
+ when "\t" # If tab, hide siblings and go in
206
+ $el.delete_region(Line.left(2), right)
207
+ Keys.clear_prefix
208
+ Launcher.launch
209
+ when :backspace, :left # Collapse tree
210
+ self.to_parent
211
+ self.kill_under
212
+ self.search(:left => Line.left, :right => Line.left(2))
213
+
214
+ when :control_slash # Collapse tree and exit
215
+
216
+ line = Line.txt
217
+
218
+ # Don't kill siblings if "<<" or "<=" line
219
+
220
+ if line =~ /^<+=? /
221
+ Keys.clear_prefix
222
+ Launcher.launch
223
+ return
224
+ end
225
+
226
+ # If CodeTree search
227
+ if CodeTree.handles?
228
+ # Kill others
229
+ View.delete(Line.left(2), right)
230
+
231
+ if Line.without_label =~ /^\./ # If just a method
232
+ # Back up to first . on last line
233
+ Search.forward "\\."
234
+ right = View.cursor
235
+ Line.previous
236
+ Search.forward "\\."
237
+ else # Else, just delete previous line
238
+ right = View.cursor
239
+ Line.previous
240
+ Line.to_beginning
241
+ end
242
+ View.delete(View.cursor, right)
243
+ return Launcher.launch
244
+ end
245
+
246
+ $el.delete_region(Line.left(2), right) # Delete other files
247
+ $el.delete_horizontal_space
248
+ $el.delete_backward_char 1
249
+
250
+ # delete -|+ if there
251
+ if View.txt(View.cursor, Line.right) =~ /^[+-] /
252
+ $el.delete_char 2
253
+ end
254
+
255
+ Launcher.launch if line =~ /\/$/ # Only launch if it can expand
256
+
257
+
258
+ when "#" # Show ##.../ search
259
+ self.stop_and_insert left, right, pattern
260
+ View.insert self.indent("- ##/", 0)
261
+ View.to(Line.right - 1)
262
+
263
+ when "*" # Show **.../ search
264
+ self.stop_and_insert left, right, pattern
265
+ View.insert self.indent("- **/", 0)
266
+ View.to(Line.right - 1)
267
+
268
+ when "$" # Insert '$ ' for command
269
+ self.stop_and_insert left, right, pattern
270
+ View.insert self.indent("$ ", 0)
271
+
272
+ when "%" # Insert '!' for command
273
+ self.stop_and_insert left, right, pattern
274
+ View.insert self.indent("% ", 0)
275
+
276
+ when "-" # Insert '-' for bullet
277
+ self.stop_and_insert left, right, pattern
278
+ View.insert self.indent("- ", 0)
279
+
280
+ when "@" # Insert '@' for menus
281
+ self.stop_and_insert left, right, pattern
282
+ View.insert self.indent("@", 0)
283
+
284
+ when "+" # Create dir
285
+ self.stop_and_insert left, right, pattern, :dont_disable_control_lock=>true
286
+ Line.previous
287
+ parent = self.construct_path
288
+ Line.next
289
+ View.insert self.indent("", 0)
290
+ name = Keys.input(:prompt=>'Name of dir to create: ')
291
+ Dir.mkdir("#{parent}#{name}")
292
+ View.insert "- #{name}/\n"
293
+ View.insert self.indent("", 0)
294
+ #Line.to_right
295
+
296
+ when ">" # Split view, then launch
297
+ $el.delete_region(Line.left(2), right)
298
+ Keys.clear_prefix
299
+ View.create
300
+ Launcher.launch
301
+
302
+ when "\C-a" # Also C-a
303
+ # If a quote, insert lines indented lower
304
+ if Line.matches(/\|/)
305
+ CodeTree.kill_siblings
306
+ self.enter_under
307
+ elsif FileTree.dir? # A Dir, so do recursive search
308
+ $el.delete_region(Line.left(2), right)
309
+ FileTree.dir_recursive
310
+ else # A file, so enter lines
311
+ $el.delete_region(Line.left(2), right)
312
+ FileTree.enter_lines(//) # Insert all lines
313
+ end
314
+
315
+ when "\C-o" # When 9 or C-o, show methods, or outline
316
+ $el.delete_region(Line.left(2), right) # Delete other files
317
+ return FileTree.drill_quotes_or_enter_lines self.construct_path.sub(/\|.*/, ''), Line.=~(/^ *\|/)
318
+ when "1".."9"
319
+ if ch == "7" and ! View.bar? # Open in bar
320
+ $el.delete_region(Line.left(2), right) # Delete other files
321
+ View.bar
322
+ Keys.clear_prefix
323
+ # Expand or open
324
+ Launcher.launch
325
+ return
326
+ end
327
+ Keys.clear_prefix
328
+ n = ch.to_i
329
+
330
+ # Pull whole string out
331
+ lines = $el.buffer_substring(left, right).split "\n"
332
+ $el.delete_region left, right
333
+ if recursive
334
+ filtered = []
335
+ file_count = 0
336
+ # Replace out lines that don't match (and aren't dirs)
337
+ lines.each_with_index do |l, i|
338
+ is_dir = (l =~ /\/$/)
339
+ file_count += 1 unless is_dir
340
+ # If dir or nth, keep
341
+ filtered << l if (is_dir or (file_count == n))
342
+ end
343
+
344
+ # Remove dirs with nothing under them
345
+ self.clear_empty_dirs! filtered
346
+
347
+ # Put back into buffer
348
+ View.insert(filtered.join("\n") + "\n")
349
+ right = $el.point
350
+
351
+ # Go to first file and go back into search
352
+ $el.goto_char left
353
+ FileTree.select_next_file
354
+
355
+ # Todo: merge this and the following .search
356
+ self.search(:recursive => true, :left => Line.left, :right => Line.left(2))
357
+ else
358
+ nth = lines[ch.to_i - 1]
359
+ View.insert "#{nth}\n"
360
+ $el.previous_line
361
+ if options[:number_means_enter] # If explicitly supposed to enter
362
+ Launcher.launch
363
+ elsif FileTree.dir? # If a dir, go into it
364
+ Launcher.launch
365
+ else
366
+ Launcher.launch
367
+ return
368
+
369
+ Line.to_beginning
370
+ # Get back into search, waiting for input
371
+ self.search(:left => Line.left, :right => Line.left(2))
372
+ end
373
+ end
374
+
375
+ when "\C-s"
376
+ $el.isearch_forward
377
+
378
+ when "\C-r"
379
+ $el.isearch_backward
380
+
381
+ when ";" # Replace parent
382
+
383
+ # CodeTree.kill_siblings
384
+ Tree.collapse :replace_parent=>1
385
+ return Launcher.launch
386
+
387
+
388
+ # when "/" # Append selected dir to parent dir
389
+ # Just search for a slash
390
+
391
+
392
+ when "=" # Drill into the file
393
+ dir = self.construct_path # Expand out ~
394
+ View.open(dir)
395
+
396
+ # Does something else above
397
+ # when "0" # Drill into the file
398
+ # # TODO Is this ever used? - does it work?
399
+ # $el.delete_region(Line.left(2), right) # Delete other files
400
+ # self.drill
401
+
402
+ when "\a" # Typed C-g
403
+ View.beep
404
+ else
405
+ $el.command_execute ch
406
+ end
407
+ end
408
+
409
+
410
+ def self.first_letter lines
411
+ letters = {}
412
+ lines.each_with_index do |l, i|
413
+ found = false
414
+ l.length.times do |j|
415
+ # letter = l[/^ \S+ (\w)/, 1] # Grab 1st letter
416
+ letter = l[j].chr
417
+ next if letter !~ /[a-z]/i
418
+ next if letters[letter]
419
+
420
+ letters[letter] = [i+1, j+1] # Set letter to index, if not there yet
421
+ break
422
+ end
423
+ end
424
+
425
+ self.highlight_tree_keys letters, Line.number
426
+
427
+ # Get input
428
+ Message << "type 1st letter... "
429
+ ch, ch_raw = Keys.char
430
+ letterized = $el.char_to_string(Keys.remove_control ch_raw).to_s
431
+ Overlay.delete_all
432
+
433
+ if ch_raw == 7 # C-g
434
+ return Cursor.restore :before_file_tree
435
+ end
436
+
437
+ if letters[letterized]
438
+ Cursor.restore :before_file_tree
439
+ Line.next letters[letterized][0] - 1
440
+ CodeTree.kill_siblings
441
+ Launcher.launch
442
+ return nil
443
+ end
444
+
445
+ # If was a valid letter but no match
446
+ if ch =~ /^[a-z0-9]$/i
447
+ return [ch, ch_raw] # We didn't do anything, so continue on
448
+ end
449
+
450
+ Cursor.restore :before_file_tree
451
+ $el.command_execute ch
452
+
453
+ nil # We handled it
454
+ end
455
+
456
+ def self.highlight_tree_keys letters, line
457
+
458
+ letters.each do |k, v|
459
+ View.line = line + v[0] - 1
460
+ cursor = View.cursor
461
+ Overlay.face :tree_keys, :left=>cursor-1+v[1], :right=>cursor+v[1]
462
+ end
463
+
464
+ View.line = line
465
+
466
+ end
467
+
468
+
469
+ #
470
+ # If foo/bar line, returns bar.
471
+ # If pipe-quoted returns siblings as multi-line string (assuming current
472
+ # line is pipe-quoted).
473
+ #
474
+ # Tree.leaf "hey/you"
475
+ # Tree.leaf "| hii" # Would get siblings also if line cursor is on had siblings
476
+ #
477
+ def self.leaf path, options={}
478
+ if path =~ /(?:^\||\/\|) ?(.*)/ # If has ^| or /|, grab siblings
479
+ # We should probably verify that the current line is the same as the path too? (in case cursor moved to other quoted tree?)
480
+ orig = $1
481
+ # First, make sure the current line is quoted (otherwise, .siblings will be pulling from somewhere else)
482
+ return orig if options[:dont_look] || Line.value !~ /^ *\|/
483
+
484
+ siblings = Tree.siblings(:quotes=>1)
485
+ siblings = siblings.map{|i| i.gsub(/^\| ?/, '')}.join("\n") # :
486
+ siblings << "\n" if siblings =~ /\n/
487
+ return siblings
488
+ end
489
+
490
+ path.split("/")[-1]
491
+
492
+ end
493
+
494
+ def self.rootless path
495
+ path.sub /^\/?[^\/]+\/?/, ''
496
+ end
497
+
498
+ def self.root path
499
+ path.sub /\/.*/, ''
500
+ end
501
+
502
+
503
+ #
504
+ # Not used - apparently surplanted during Tree.search by
505
+ # the methods C-a and C-o call.
506
+ #
507
+ # def self.drill
508
+ # Line.to_left
509
+ # # Get indent between | and tabs
510
+ # match = Line.value.match(/^( *)\|( *)(.+)/)
511
+ # # Get content
512
+ # path = Bookmarks.expand(construct_path) # Get path
513
+ # matches = ""
514
+ # if match # It's a quoted line
515
+ # indent, indent_after_bar, rest = match[1,3]
516
+ # indent_after_bar.gsub!("\t", " ")
517
+ # #"#{indent_after_bar}#{rest}"
518
+ # # Go through and find line
519
+ # found = nil
520
+ # IO.foreach(path[/(.+?)\|/, 1]) do |line|
521
+ # line.sub!(/[\r\n]+$/, '')
522
+ # # If not found yet, check for line
523
+ # if !found && line == "#{indent_after_bar}#{rest}"
524
+ # found = true
525
+ # next
526
+ # end
527
+ # next unless found
528
+
529
+ # # Exit if indented at same level (unless blank or comment)
530
+ # if((Line.indent(line).length <= indent_after_bar.length) &&
531
+ # (! (line =~ /^\s*$/)) &&
532
+ # (! (line =~ /^\s*#/))
533
+ # )
534
+ # break
535
+ # end
536
+
537
+ # # Skip unless indented 2 later
538
+ # next unless Line.indent(line).length == 2 + indent_after_bar.length
539
+
540
+ # matches << "#{indent}| #{line}\n"
541
+ # end
542
+ # self.insert_quoted_and_search matches
543
+ # else # It's a file
544
+ # indent = Line.value[/^ */]
545
+ # this_was_used = last_was_used = false
546
+ # IO.foreach(path) do |line| # Print lines with no indent
547
+ # last_was_used = this_was_used
548
+ # this_was_used = false
549
+ # line.sub!(/[\r\n]+$/, '')
550
+ # next if line =~ /^ +/ # Skip non top-level lines
551
+ # next if line =~ /^$/ and ! last_was_used # Skip blank lines, unless following top-level
552
+ # matches << "#{indent} | #{line}\n"
553
+ # this_was_used = true
554
+ # end
555
+ # self.insert_quoted_and_search matches
556
+ # # TODO Search in result
557
+
558
+ # # TODO Do some checking for duplicates
559
+ # end
560
+ # end
561
+
562
+ def self.clear_empty_dirs! lines, options={}
563
+ regex = options[:quotes] ?
564
+ /^ +[+-] [^#|]+$|^ *:\d+$/ :
565
+ /^[^|]+\/$/
566
+
567
+ lines = lines.split("\n") if lines.is_a?(String)
568
+
569
+ file_indent = 0
570
+ i = lines.length
571
+ while( i > 0)
572
+ i -= 1
573
+ l = lines[i]
574
+
575
+ l =~ /^( +)/
576
+ spaces = $1 ? $1.length : 0
577
+ if l =~ regex # If thing to always keep (dir, or dir and file)
578
+ if spaces < file_indent # If lower than indent, decrement indent
579
+ file_indent -= 2
580
+ else # Else, delete
581
+ lines.delete_at i
582
+ end
583
+ else # If file
584
+ file_indent = spaces # Set indent
585
+ end
586
+ end
587
+ lines
588
+ end
589
+
590
+ def self.stop_and_insert left, right, pattern, options={}
591
+ $el.goto_char left
592
+ # TODO: delete left if recursive - emulate what "delete" does to delete, first
593
+ pattern == "" ?
594
+ $el.delete_region($el.point, right) :
595
+ Line.next
596
+ $el.open_line 1
597
+ ControlLock.disable unless options[:dont_disable_control_lock]
598
+ end
599
+
600
+ # Go to the right side of the tree item (move to after last item that is indented lower)
601
+ def self.after_children options={}
602
+ indent = Line.indent.size
603
+
604
+ pattern = "\\(\n\n\n\\|^ \\{0,#{indent}\\}[^ \n]\\)" # Find line with same or less indent
605
+
606
+ # Why would I do this at all?
607
+ # self.minus_to_plus_maybe unless options[:no_plus]
608
+
609
+ Line.next
610
+ Search.forward pattern, :go_anyway=>1
611
+ Line.to_left
612
+
613
+ Search.backward "^."
614
+ Line.next
615
+ end
616
+
617
+ # Move cursor to after last sibling, crossing blank lines, but not double-blank lines.
618
+ # Tree.after_siblings
619
+ def self.after_siblings # options={}
620
+ indent = Line.indent.size
621
+
622
+ return if indent == 0
623
+
624
+ indent -= 2
625
+
626
+ pattern = "\\(\n\n\n\\|^ \\{0,#{indent}\\}[^ \n]\\)" # Find line with less indent
627
+
628
+ Search.forward pattern, :go_anyway=>1
629
+ Line.to_left
630
+
631
+ Search.backward "^."
632
+ Line.next
633
+ nil
634
+ end
635
+
636
+ def self.kill_under options={}
637
+
638
+ # Get indent
639
+ orig = Line.left
640
+ left = Line.left(Keys.prefix_u? ? 1 : 2)
641
+
642
+ self.after_children options
643
+
644
+ View.delete(left, View.cursor)
645
+ View.to orig
646
+ end
647
+
648
+ #
649
+ # Inserts text indented under, and searches.
650
+ # Called by Tree.<<
651
+ #
652
+ def self.under txt, options={}
653
+
654
+ return if txt.nil?
655
+
656
+ txt = TextUtil.unindent(txt) if txt =~ /\A[ \n]/
657
+
658
+ escape = options[:escape] || ''
659
+ txt = txt.gsub!(/^/, escape)
660
+ txt.gsub!(/^\| $/, '|')
661
+
662
+ # Add linebreak at end if none
663
+ txt = "#{txt}\n" unless txt =~ /\n/
664
+
665
+ # Insert linebreak if at end of file
666
+
667
+ txt.gsub! /^ /, '' if options[:before] || options[:after] # Move back to left
668
+
669
+ self.output_and_search txt, options
670
+ nil
671
+ end
672
+
673
+ # Jumps to first sibling, crossing blank lines, but not double-blank lines
674
+ def self.before_siblings
675
+
676
+ indent = Line.value[/^ ( *)/, 1]
677
+
678
+ # For now, don't handle if at root
679
+
680
+ regex = "\\(\n\n\n\\|^#{indent}[^\t \n]\\)"
681
+ Search.backward regex, :go_anyway=>true
682
+
683
+ # Move.to_next_paragraph(:no_skip=>1)
684
+ hit_two_blanks = View.cursor == Line.right
685
+
686
+ return Move.to_next_paragraph(:no_skip=>1) if hit_two_blanks || Line.value(2).blank?
687
+ Line.next
688
+
689
+ # Can't get siblings of item at left margin - undecided how to implement it" if !indent
690
+ end
691
+
692
+ # Jumps to parent, regardless of blanks
693
+ def self.to_parent prefix=nil #, options={}
694
+
695
+ prefix ||= Keys.prefix :clear=>true
696
+
697
+ # U means go to previous line at margin
698
+ if prefix == :u
699
+ Search.backward "^[^ \t\n]"
700
+ return
701
+ end
702
+
703
+ times = prefix || 1
704
+ times.times do
705
+ indent = Line.value[/^ ( *)/, 1]
706
+
707
+ # If odd indent, subtract 1
708
+ indent.slice!(/ /) if indent && indent.length % 2 == 1
709
+
710
+ regex = "^#{indent}[^\t \n]"
711
+ $el.search_backward_regexp regex
712
+ Line.to_beginning :quote=>1
713
+ end
714
+ end
715
+
716
+ def self.plus_to_minus
717
+ self.toggle_plus_and_minus if Line.matches(/^\s*\+ /)
718
+ end
719
+
720
+ def self.plus_to_minus_maybe
721
+ self.plus_to_minus if Line.matches(/(^\s*[+-] [a-z]|\/$)/)
722
+ end
723
+
724
+ def self.minus_to_plus
725
+ self.toggle_plus_and_minus if Line.matches(/^\s*- /)
726
+ end
727
+
728
+ def self.minus_to_plus_maybe
729
+ self.minus_to_plus if Line.matches(/(^\s*[+-] [a-z]|\/$)/)
730
+ end
731
+
732
+ # Mapped to Enter when on a FileTree buffer. Opens file cursor is on in the tree.
733
+ # It assumes the path to a dir is on the current line.
734
+ def self.construct_path options={}
735
+ begin
736
+ path = []
737
+ orig = $el.point
738
+
739
+ # Do until we're at a root
740
+ line = Line.value
741
+ clean = self.clean_path line
742
+ while(line =~ /^ / && (options[:all] || clean !~ /^@/))
743
+ line =~ /^ ( *)(.*)/
744
+ spaces, item = $1, $2
745
+ item = clean unless options[:raw] # Removes labels, ##..., **...
746
+ if item != "" # If item wasn't completely cleaned away
747
+ path.unshift item # Add item to list
748
+ end
749
+ $el.search_backward_regexp "^#{spaces}[^\t \n]"
750
+
751
+ # If ignoring Ol lines, keep searching until not on one
752
+ if options[:ignore_ol]
753
+ while Line =~ /^[# ]*Ol\b/
754
+ $el.search_backward_regexp "^#{spaces}[^\t \n]"
755
+ end
756
+ end
757
+
758
+ line = Line.value
759
+ clean = self.clean_path line
760
+ end
761
+ # Add root of tree
762
+ root = Line.value.sub(/^ +/, '')
763
+ root = self.clean_path(root) unless options[:raw]
764
+ root.slice! /^@ ?/
765
+ path.unshift root
766
+
767
+ last = path.length - 1
768
+ path = path.map_with_index{|o, i|
769
+ next o if i == last # Don't add slash to last
770
+ o =~ /\/$/ ? o : "#{o}/"
771
+ } if options[:slashes]
772
+
773
+ $el.goto_char orig
774
+ if options[:indented]
775
+ indentify_path path
776
+ elsif options[:list]
777
+ path
778
+ else
779
+ path.join
780
+ end
781
+ rescue Exception=>e
782
+ raise ".construct_path couldn't construct the path - is this a well-formed tree\?: #{e}"
783
+ end
784
+ end
785
+
786
+ #
787
+ # Moves cursor to root of tree.
788
+ #
789
+ # Tree.to_root # To last @.. line
790
+ # Tree.to_root :highest=>1 # All the way to highest root (left margin)
791
+ #
792
+ def self.to_root options={}
793
+
794
+ Move.to_end # In case already at left of line and root
795
+
796
+ # Always go up at least once
797
+ Tree.to_parent
798
+
799
+ # Until we're at the root, keep jumping to parent
800
+ line = Line.value
801
+
802
+ if options[:highest]
803
+ while(line =~ /^\s/) do
804
+ Tree.to_parent
805
+ line = Line.value
806
+ end
807
+ return
808
+ end
809
+
810
+ while(line =~ /^\s/ && line !~ /^ *([+-] )?@/) do
811
+ Tree.to_parent
812
+ line = Line.value
813
+ end
814
+
815
+ nil
816
+ end
817
+
818
+ def self.is_root? path
819
+ # It's the root if it's not at the left margin
820
+ result = path !~ /^ /
821
+ result ? true : false
822
+ end
823
+
824
+ def self.clean_path path
825
+ path = Line.without_label :line=>path#, :leave_indent=>true
826
+ path.sub!(/^([^|\n-]*)##.+/, "\\1") # Ignore "##"
827
+ path.sub!(/^([^|\n-]*)\*\*.+/, "\\1") # Ignore "**"
828
+ path
829
+ end
830
+
831
+ def self.toggle_plus_and_minus
832
+ orig = Location.new
833
+ l = Line.value 1, :delete => true
834
+ case l[/^\s*([+-])/, 1]
835
+ when '+'
836
+ View.insert l.sub(/^(\s*)([+-]) /, "\\1- ")
837
+ orig.go
838
+ '+'
839
+ when '-'
840
+ View.insert l.sub(/^(\s*)([+-]) /, "\\1+ ")
841
+ orig.go
842
+ '-'
843
+ else
844
+ View.insert l
845
+ orig.go
846
+ nil
847
+ end
848
+ end
849
+
850
+ def self.acronym_regexp search
851
+ search.gsub(/([a-zA-Z])/, "[a-z]*[_.]\\1").sub(/.+?\].+?\]/,'^ +')
852
+ end
853
+
854
+ def self.search_dir_names(lines, regexp)
855
+ result = []
856
+ stack = [0]
857
+ indent = indent_size(lines[0])
858
+ lines.each do |l|
859
+ last_indent = indent
860
+
861
+ indent, name = l.match(/^( *)(.+)/)[1..2]
862
+ indent = indent_size(indent)
863
+ if indent > last_indent
864
+ stack << 0
865
+ elsif indent < last_indent
866
+ (last_indent - indent).times { stack.pop }
867
+ end
868
+ stack[stack.size-1] = name
869
+ # If file, remove this line if path doesn't match
870
+ if stack.last !~ /\/$/
871
+ next unless stack[0..-2].join =~ regexp
872
+ end
873
+ result << l
874
+ end
875
+ result
876
+ end
877
+
878
+ def self.indent_size(line)
879
+ spaces = line[/^ +/]
880
+ return 0 unless spaces
881
+ spaces.size / 2
882
+ end
883
+
884
+ def self.indent txt, line=1
885
+ indent = Line.indent(Line.value(line))
886
+ txt.gsub!(/^/, "#{indent} ")
887
+ end
888
+
889
+ def self.unquote! txt
890
+ txt.replace self.unquote(txt)
891
+ end
892
+
893
+ def self.unquote txt
894
+ txt.gsub(/^\| ?/, '')
895
+ end
896
+
897
+ def self.quote txt, options={}
898
+ if options[:leave_headings]
899
+ return TextUtil.unindent(txt).gsub(/^([^>])/, "| \\1").gsub(/^\| $/, '|')
900
+ end
901
+
902
+ TextUtil.unindent(txt).gsub(/^/, "| ").gsub(/^\| $/, '|')
903
+
904
+ # TextUtil.unindent(txt).gsub(/^([^|@>+-])/, "| \\1").gsub(/^\| $/, '|')
905
+ end
906
+
907
+ def self.insert_quoted_and_search matches, options={}
908
+ # Insert matches
909
+ Line.next
910
+ left = $el.point
911
+ View.insert matches, options
912
+ right = $el.point
913
+
914
+ $el.goto_char left
915
+ if options[:line_found] && options[:line_found] > 0
916
+ Line.next(options[:line_found]-1)
917
+ Color.colorize :l
918
+ end
919
+
920
+ Line.to_words
921
+ # Do a search
922
+
923
+ return if options[:no_search]
924
+
925
+ Tree.search(:left=>left, :right=>right)
926
+ end
927
+
928
+ def self.<< txt, options={}
929
+ self.under txt, options
930
+ end
931
+
932
+ # Inserts indented underneath.
933
+ # Not sure why it's calling View.under instead of Tree.under
934
+ def self.after txt
935
+ # Is anything calling this - maybe make them just call Tree.under.
936
+ # View.under does a couple lines, then calls Tree.under.
937
+ View.under txt, :after=>1
938
+ end
939
+
940
+ #
941
+ # Returns group of lines close to current line that are indented at the same level.
942
+ # The bounds are determined by any lines indented *less* than the current line (including
943
+ # blank lines). In this context, lines having only spaces are not considered blank.
944
+ # Any lines indented *more* than the current line won't affect the bounds, but will be
945
+ # filtered out.
946
+ #
947
+ # p Tree.siblings # Doesn't include current line
948
+ # p Tree.siblings :string=>1 # As string, not list
949
+ # p Tree.siblings :all=>1 # Includes current line
950
+ # p Tree.siblings :everything=>1 # Includes current line and all children (returns string)
951
+ # p Tree.siblings :cross_blank_lines=>1 # Includes current line, and crosses single blank lines (returns string)
952
+ #
953
+ # sample chile
954
+ #
955
+ def self.siblings options={}
956
+ return self.siblings(:all=>true).join("\n").gsub(/^ *\| ?/, '')+"\n" if options[:string] && ! options[:cross_blank_lines] && ! options[:before] && ! options[:after]
957
+
958
+ if options[:cross_blank_lines]
959
+ left1, right2 = self.sibling_bounds :cross_blank_lines=>1
960
+ # For now, if :cross_blank_lines, assume just left1, right2
961
+ options[:all] = 1
962
+ else
963
+ left1, right1, left2, right2 = self.sibling_bounds# options
964
+ end
965
+
966
+ # Combine and process siblings
967
+ if options[:all] || options[:everything]
968
+ siblings = View.txt(options.merge(:left=>left1, :right=>right2))
969
+
970
+ elsif options[:quotes] # Only grab contiguous quoted lines
971
+ above = View.txt(options.merge(:left=>left1, :right=>right1))
972
+ found = true
973
+ above = above.split("\n").reverse.select{|o| found && o =~ /^ *\|/ or found = false}.reverse.join("\n")
974
+ above << "\n" if above.any?
975
+
976
+ middle = View.txt(options.merge(:left=>right1, :right=>left2))
977
+
978
+ below = View.txt(options.merge(:left=>left2, :right=>right2))
979
+ found = true
980
+ below = below.split("\n").select{|o| found && o =~ /^ *\|/ or found = false}.join("\n")
981
+ below << "\n" if below.any?
982
+
983
+ siblings = "#{above}#{middle}#{below}" #.strip
984
+
985
+ elsif options[:before]
986
+ siblings = View.txt(options.merge(:left=>left1, :right=>right1))
987
+ elsif options[:after]
988
+ siblings = View.txt(options.merge(:left=>left2, :right=>right2))
989
+ else
990
+ # By default, don't include sibling on current line
991
+ # TODO: swap this, so it includes all by default?
992
+ # Go through and make new :exclude_current param, and make invocations use it
993
+ siblings = View.txt(options.merge(:left=>left1, :right=>right1)) + View.txt(options.merge(:left=>left2, :right=>right2))
994
+ end
995
+
996
+ if options[:everything]
997
+ indent = siblings[/\A */]
998
+ return siblings.gsub(/^#{indent}/, '')
999
+ end
1000
+
1001
+ siblings.gsub! /^#{Line.indent} .*\n/, '' # Remove more indented lines (children)
1002
+ siblings.gsub! /^ +\n/, '' # Remove blank lines
1003
+ siblings.gsub! /^ +/, '' # Remove indents
1004
+
1005
+ if options[:string] # Must have :before or :after option also if it got here
1006
+ return siblings.gsub /^\| ?/, ''
1007
+ end
1008
+
1009
+ siblings = siblings.split("\n")
1010
+
1011
+ unless options[:include_label] # Optionally remove labels
1012
+ siblings.map!{|i| Line.without_label(:line=>i)}
1013
+ end
1014
+
1015
+ # Change blanks to nil
1016
+ siblings.map!{|o| o.blank? ? nil : o}
1017
+
1018
+ siblings
1019
+ end
1020
+
1021
+ def self.sibling_bounds options={}
1022
+ if options[:cross_blank_lines] # If :cross_blank_lines, just jump to parent, then use .after_children
1023
+ orig = Location.new
1024
+ Tree.before_siblings
1025
+ left = Line.left
1026
+ Tree.after_siblings
1027
+ right = View.cursor
1028
+
1029
+ orig.go
1030
+ return [left, right]
1031
+ end
1032
+
1033
+ indent_size = Line.indent.size # Get indent
1034
+ indent_less = indent_size - 1
1035
+
1036
+ orig = Location.new
1037
+
1038
+ right1 = Line.left # Right side of lines before
1039
+
1040
+ # Search for line indented less - parent (to get siblings after)
1041
+ found = indent_less < 0 ?
1042
+ Search.backward("^$", :go_anyway=>1) :
1043
+ Search.backward("^ \\{0,#{indent_less}\\}\\($\\|[^ \n]\\)")
1044
+
1045
+ Line.next if found
1046
+ left1 = Line.left # Left side of lines before
1047
+
1048
+ orig.go
1049
+
1050
+ # Search for line indented same or less (to get siblings after)
1051
+ Line.next
1052
+ Search.forward "^ \\{0,#{indent_size}\\}\\($\\|[^ \n]\\)" # Move after original node's children, if any
1053
+ Line.to_left
1054
+ left2 = View.cursor
1055
+ # Search for line indented less
1056
+ indent_less < 0 ?
1057
+ Search.forward("^$") :
1058
+ Search.forward("^ \\{0,#{indent_less}\\}\\($\\|[^ \n]\\)")
1059
+ right2 = Line.left # Left side of lines before
1060
+ orig.go
1061
+
1062
+ [left1, right1, left2, right2]
1063
+ end
1064
+
1065
+ def self.search_appropriately left, right, output, options={}
1066
+
1067
+ View.cursor = left unless options[:line_found]
1068
+ Line.to_words
1069
+
1070
+ # Determine how to search based on output!
1071
+
1072
+ options.merge! :left=>left, :right=>right
1073
+
1074
+ root_indent = output[/\A */]
1075
+ if output =~ /^#{root_indent} / # If any indenting
1076
+ if output =~ /^ +\|/
1077
+ Search.forward "^ +\\(|\\|- ##\\)", :beginning=>true
1078
+ Line.to_beginning
1079
+ options[:recursive_quotes] = true
1080
+ else
1081
+ FileTree.select_next_file
1082
+ options[:recursive] = true
1083
+ end
1084
+ Tree.search options
1085
+ else
1086
+ Tree.search options.merge(:number_means_enter=>true)
1087
+ end
1088
+
1089
+ end
1090
+
1091
+ # Iterate through each line in the tree
1092
+ # Tree.traverse("a\n b") {|o| p o}
1093
+ def self.traverse tree, options={}, &block
1094
+ branch, line_indent, indent = [], 0, 0
1095
+
1096
+ tree = tree.split("\n")
1097
+ tree.each_with_index do |line, i|
1098
+
1099
+ # If empty line use last non-blank line's indent
1100
+ if line.empty?
1101
+ line = nil
1102
+ last_indent = line_indent
1103
+ line_indent = tree[i+1] # Use indent of following line
1104
+ line_indent = line_indent ? (line_indent[/^ */].length / 2) : 0
1105
+ raise "Blank lines in trees between parents and children aren't allowed." if line_indent > 0 && line_indent > last_indent
1106
+ else
1107
+ line_indent = line[/^ */].length / 2
1108
+ line.strip!
1109
+ end
1110
+
1111
+ branch[line_indent] = line
1112
+
1113
+ if line_indent < indent
1114
+ branch = branch[0..line_indent]
1115
+ end
1116
+
1117
+ branch_dup = branch.dup
1118
+
1119
+ if options[:no_bullets]
1120
+ branch_dup.map!{|o| o ? o.sub(/^[<+-][<=]* /, '') : nil }
1121
+ end
1122
+
1123
+ flattened = branch_dup.dup
1124
+
1125
+ flattened.map!{|o| o ? o.sub(/^[+-] /, '') : nil } if ! options[:no_bullets] # Might have side-effects if done twice
1126
+ flattened = flattened.join('')#.gsub(/[.:]/, '') # Why were :'s removed??
1127
+
1128
+ block.call [branch_dup, flattened]
1129
+
1130
+ indent = line_indent
1131
+ end
1132
+ end
1133
+
1134
+ # Insert section from a file under it in tree
1135
+ def self.enter_under
1136
+ Line.beginning
1137
+ path = Tree.construct_path # Get path
1138
+ path.sub!(/\|.+/, '') # Remove file
1139
+ path = Bookmarks.expand(path)
1140
+
1141
+ # Cut off indent and pipe (including following space)
1142
+ Line.value =~ /(^ +)\| (.+)/
1143
+ quote_indent, line = $1, $2
1144
+
1145
+ if line =~ /^> / # If heading in a notes file
1146
+ # Go through lines in file until end of section
1147
+ matches = ""
1148
+ found_yet = false
1149
+ IO.foreach(path) do |l|
1150
+ l.sub!(/[\r\n]+$/, '')
1151
+ l.gsub!("\c@", '.') # Replace out characters that el4r can't handle
1152
+ # Swallow up until match
1153
+ if !found_yet
1154
+ found_yet = l == line
1155
+ next
1156
+ end
1157
+ # Grab rest until another pipe
1158
+ break if l =~ /^\> /
1159
+
1160
+ l = " #{l}" unless l.empty?
1161
+ matches << "#{quote_indent} |#{l}\n"
1162
+ end
1163
+
1164
+ # Insert and start search
1165
+ Tree.insert_quoted_and_search matches
1166
+
1167
+ else # Otherwise, grab by indent
1168
+
1169
+ # Go through lines in file until we've found it
1170
+ indent = line[/^\s*/].gsub("\t", ' ').length
1171
+ matches = ""
1172
+ found_yet = false
1173
+ IO.foreach(path) do |l|
1174
+ l.sub!(/[\r\n]+$/, '')
1175
+ l.gsub!("\c@", '.') # Replace out characters that el4r can't handle
1176
+ # Swallow up until match
1177
+ if !found_yet
1178
+ found_yet = l == line
1179
+ next
1180
+ end
1181
+ # Grab rest until not indented less
1182
+
1183
+ current_indent = l[/^\s*/].gsub("\t", ' ').length
1184
+
1185
+ break matches.<<("#{quote_indent} |\n") if line.blank?
1186
+
1187
+ break if current_indent <= indent
1188
+
1189
+ l = " #{l}" unless l.blank?
1190
+ matches << "#{quote_indent} |#{l}\n"
1191
+ end
1192
+
1193
+ # Insert and start search
1194
+ Tree.insert_quoted_and_search matches
1195
+ end
1196
+ end
1197
+
1198
+
1199
+
1200
+ def self.dotify! tree, target
1201
+ target_flat = target.join "/"
1202
+
1203
+ self.traverse(tree, :no_bullets=>1) do |branch, path|
1204
+
1205
+ match = self.target_match path, target_flat
1206
+
1207
+ if match == :same || match == :longer
1208
+ indent = branch.length - 1
1209
+
1210
+ # If last item in path has period and target doesn't
1211
+ if branch[indent] =~ /^\./ && target[indent] !~ /^\./
1212
+ # Add period to nth item in target
1213
+ target[indent].sub! /^/, '.'
1214
+ end
1215
+ end
1216
+ end
1217
+
1218
+ # Optimization
1219
+ # If last path wasn't match and indent is lower than last path, we won't match
1220
+
1221
+ end
1222
+
1223
+
1224
+ #
1225
+ # Called by Tree.children and Tree.dotify
1226
+ #
1227
+ # p Tree.target_match "a/b", "a/b"
1228
+ # p Tree.target_match "a/b", "a"
1229
+ # p Tree.target_match "a", "a/b"
1230
+ #
1231
+ def self.target_match path, target
1232
+ pi, ti = 0, 0
1233
+
1234
+ while true
1235
+ pathi, targeti = path[pi], target[ti]
1236
+
1237
+ if pathi.nil? || targeti.nil?
1238
+ return :same if pathi.nil? && targeti.nil? # Handles a, a and a/, a/
1239
+ return :same if pathi.nil? && target[ti+1].nil? && targeti.chr == "/" # Handles a, a/
1240
+ return :same if targeti.nil? && path[pi+1].nil? && pathi.chr == "/" # Handles a/, a
1241
+
1242
+ return :shorter if pathi && (pathi.chr == "/" || path[pi-1].chr == "/" || pi == 0)
1243
+ return :longer if targeti && (targeti.chr == "/" || target[ti-1].chr == "/" || ti == 0)
1244
+ return nil # At end of one, but no match
1245
+ end
1246
+
1247
+ # If chars equal, increment
1248
+ if pathi == targeti
1249
+ pi += 1
1250
+ next ti += 1
1251
+
1252
+ # If path has /. or . at beginning, increment path
1253
+ elsif pathi.chr == "." && (path[pi-1].chr == "/" || pi == 0) && (target[ti-1].chr == "/" || ti == 0)
1254
+ next pi += 1
1255
+
1256
+ # If path has /*/ or /* at end, increment path and increment target to next / or end
1257
+ elsif pathi.chr == "*" && path[pi-1].chr == "/" && (path[pi+1].nil? || path[pi+1].chr == "/")
1258
+ pi += 1
1259
+ ti = target.index("/", ti+1) || target.length
1260
+ next
1261
+ end
1262
+
1263
+ break # Not found
1264
+ end
1265
+
1266
+ nil
1267
+ end
1268
+
1269
+
1270
+ #
1271
+ # Cuts off 1st item in the path.
1272
+ #
1273
+ # Use instead of .leaf when you know all but the root is part of the leaf
1274
+ # (in case there are slashes).
1275
+ #
1276
+ # Tree.rest "hey/you/there"
1277
+ #
1278
+ def self.rest path
1279
+
1280
+ path = self.rootless path
1281
+ path = "|#{path}" unless path =~ /^(\||$)/
1282
+
1283
+ self.leaf(path)
1284
+ end
1285
+
1286
+
1287
+ # Copy children from treeb to treea, but only for branches in treea where children were removed.
1288
+ def self.restore treea, treeb
1289
+
1290
+ treea, treeb = TreeCursor.new(treea), TreeCursor.new(treeb)
1291
+
1292
+ # For each leaf in A
1293
+ treea.each do
1294
+ next unless treea.at_leaf? # We only care about leafs
1295
+
1296
+ treeb.select treea.line # Find branch in B
1297
+ next if treeb.at_leaf? # Skip if no children children
1298
+
1299
+ treea << treeb.under # Grab them and move into A
1300
+ end
1301
+
1302
+ treea.txt
1303
+ end
1304
+
1305
+ def self.collapse options={}
1306
+ # If at root or end of line, go to next
1307
+ Line.next if Line !~ /^ / || Line.at_right?
1308
+ CodeTree.kill_siblings
1309
+
1310
+ Move.to_end -1
1311
+
1312
+ Line.sub! /([ +-]*).+/, "\\1" if options[:replace_parent]
1313
+
1314
+ left = View.cursor
1315
+ $el.skip_chars_forward(" \n+-")
1316
+ View.delete left, View.cursor
1317
+
1318
+ Move.to_end
1319
+ left, right = View.paragraph :bounds=>true, :start_here=>true
1320
+
1321
+ $el.indent_rigidly View.cursor, right, -2
1322
+ end
1323
+
1324
+ # Gets path from root to here, indenting each line by 2 spaces deeper.
1325
+ # Tree.ancestors_indented
1326
+ def self.ancestors_indented options={}
1327
+
1328
+ all = options[:just_sub_tree] ? nil : 1
1329
+ path = Tree.construct_path(:list=>1, :ignore_ol=>1, :all=>all)
1330
+ result = ""
1331
+ path.each_with_index { |o, i|
1332
+ result << "#{' ' * i}#{o}\n"
1333
+ }
1334
+
1335
+ result
1336
+ end
1337
+
1338
+ # Returns self and all siblings (without children).
1339
+ def self.unset_env_vars
1340
+ ENV['no_slash'] = nil
1341
+ end
1342
+
1343
+ def self.output_and_search block_or_string, options={}
1344
+
1345
+ line = options[:line]
1346
+
1347
+ if $el
1348
+ buffer_orig = View.buffer
1349
+ orig = Location.new
1350
+ orig_left = View.cursor
1351
+ end
1352
+
1353
+ error_happened = nil
1354
+
1355
+ self.unset_env_vars
1356
+
1357
+ output =
1358
+ if block_or_string.is_a? String
1359
+ block_or_string
1360
+ else # Must be a proc
1361
+ begin
1362
+ block_or_string.call line
1363
+ rescue Exception=>e
1364
+ message = e.message
1365
+
1366
+ error_happened = true
1367
+ CodeTree.draw_exception e, Code.to_ruby(block_or_string)
1368
+ end
1369
+ end
1370
+
1371
+ return if output.blank?
1372
+
1373
+ if output.is_a?(String) && $el && output.strip =~ /\A<<< (.+)\/\z/
1374
+ Tree.replace_item $1
1375
+ Launcher.launch
1376
+ return true
1377
+ end
1378
+
1379
+ # TODO: move some of this crap into the else block above (block_or_string is proc)
1380
+
1381
+
1382
+ if $el
1383
+ buffer_changed = buffer_orig != View.buffer # Remember whether we left the buffer
1384
+
1385
+ ended_up = Location.new
1386
+ orig.go # Go back to where we were before running code
1387
+ end
1388
+
1389
+ # Move what they printed over to left margin initally, in case they haven't
1390
+ output = TextUtil.unindent(output) if output =~ /\A[ \n]/
1391
+ # Remove any double linebreaks at end
1392
+ output = CodeTree.returned_to_s output
1393
+
1394
+ if $el
1395
+ return View.prompt $1 if output =~ /\A\.prompt (.+)/
1396
+ return View.flash $1 if output =~ /\A\.flash (.+)/
1397
+ end
1398
+
1399
+ output.sub!(/\n\n\z/, "\n")
1400
+ output = "#{output}\n" if output !~ /\n\z/
1401
+
1402
+ return output if options[:just_return]
1403
+
1404
+
1405
+ # Add slash to end of line if not suppressed, and line isn't a quote
1406
+ line=options[:line]
1407
+ if !options[:no_slash] && ! ENV['no_slash'] && Line !~ /(^ *\||\/$)/
1408
+ Line << "/"
1409
+ end
1410
+ indent = Line.indent
1411
+ Line.to_left
1412
+ Line.next
1413
+ left = View.cursor
1414
+
1415
+ output.gsub! /^./, "#{indent} \\0" # Add indent, except for blank lines
1416
+
1417
+ View.<< output, :utf8=>1
1418
+ right = View.cursor
1419
+
1420
+ orig.go # Move cursor back <-- why doing this?
1421
+ ended_up.go # End up where script took us
1422
+ moved = View.cursor != orig_left
1423
+
1424
+ # Move to :line_found if any
1425
+ if options[:line_found] && options[:line_found] > 0
1426
+ Line.next(options[:line_found])
1427
+ Color.colorize :l
1428
+ end
1429
+
1430
+ if !error_happened && !$xiki_no_search &&!options[:no_search] && !buffer_changed && !moved
1431
+ Tree.search_appropriately left, right, output, options
1432
+ elsif ! options[:line_found]
1433
+ Line.to_beginning :down=>1
1434
+ end
1435
+ output
1436
+ end
1437
+
1438
+ def self.closest_dir
1439
+ dir = Xiki.trunk.reverse.find{|o| FileTree.matches_root_pattern? o}
1440
+
1441
+ dir = Bookmarks[dir]
1442
+ return nil if dir.nil?
1443
+
1444
+ File.expand_path dir
1445
+ end
1446
+
1447
+ # Tree.slashless("hey/you/").should == "hey/you"
1448
+ def self.slashless txt
1449
+ txt.sub /\/$/, ''
1450
+ end
1451
+
1452
+
1453
+ #
1454
+ # Extracts children from tree arg and target (path) arg.
1455
+ #
1456
+ # Or, if no tree passed in, delegates to Tree.children_at_cursor
1457
+ #
1458
+ # Tree.children "a\n b\n c", "a"
1459
+ #
1460
+ def self.children tree=nil, target=nil, options={}
1461
+ include_subitems = options[:include_subitems] # Include sub-items for all children
1462
+
1463
+ return self.children_at_cursor(tree) if tree.nil? || tree.is_a?(Hash) # tree is actually options
1464
+
1465
+ target = target.join("/") if target.is_a? Array
1466
+ target = "" if target == nil || target == "/" # Must be at root if nil
1467
+ tree = TextUtil.unindent tree
1468
+
1469
+ target.sub!(/^\//, '')
1470
+ target.sub!(/\/$/, '')
1471
+
1472
+ found = nil
1473
+ result = ""
1474
+
1475
+ found = -1 if target.empty?
1476
+
1477
+ @@under_preexpand = false # Will include sub-items of only some children
1478
+
1479
+ self.traverse tree do |branch, path|
1480
+ blank = branch[-1].nil?
1481
+
1482
+ if ! found
1483
+ target_match = Tree.target_match path, target
1484
+ next unless target_match == :shorter || target_match == :same
1485
+ found = branch.length - 1 # Found, remember indent
1486
+
1487
+ else
1488
+ current_indent = branch.length - 1
1489
+ # If found and still indented one deeper
1490
+ one_deeper = current_indent == found + 1
1491
+
1492
+ if one_deeper || ((include_subitems || @@under_preexpand) && current_indent > found)
1493
+
1494
+ next result << "\n" if blank
1495
+
1496
+ item = branch[-1]
1497
+ item.sub!(/^- /, '+ ') if item =~ /\/$/
1498
+ item.sub!(/^([<+-][<=]* )?\./, "\\1")
1499
+ next if item =~ /^[+-] \*\/$/ # Skip asterixes
1500
+
1501
+ # If @@under_preexpand, add on indent
1502
+ if include_subitems || @@under_preexpand
1503
+ item = "#{' ' * (branch.length - found - 2)}#{item}"
1504
+ end
1505
+
1506
+ @@under_preexpand = false if one_deeper
1507
+
1508
+ # Pre-expand if @... or doesn't end in slash
1509
+ @@under_preexpand = true if one_deeper && (item =~ /^([+-] )?@/ || item !~ /\/$/)
1510
+
1511
+ result << "#{item}\n" # Output
1512
+
1513
+ else # Otherwise, stop looking for children if indent is less
1514
+
1515
+ # # If blank line is at same level
1516
+ # if branch.empty? && found == current_indent
1517
+ # next result << "\n"
1518
+ # end
1519
+
1520
+ next if current_indent > found
1521
+ # No longer beneath found item
1522
+ found = nil
1523
+ @@under_preexpand = false
1524
+ end
1525
+ end
1526
+
1527
+ end
1528
+
1529
+ result.empty? ? nil : result
1530
+ end
1531
+
1532
+ #
1533
+ # Returns children indented underneath.
1534
+ #
1535
+ # Tree.children.inspect
1536
+ # Tree.children(:string=>1).inspect
1537
+ # Tree.children(:cross_blank_lines=>1).inspect
1538
+ # a
1539
+ # ab
1540
+ # b
1541
+
1542
+ # c
1543
+ #
1544
+ def self.children_at_cursor options={}
1545
+
1546
+ options ||= {}
1547
+ child = self.child
1548
+ return nil if child.nil? # Return if no child
1549
+
1550
+
1551
+ # If :cross_blank_lines, use Tree.after_children to find end
1552
+ if options[:cross_blank_lines]
1553
+ orig = Location.new
1554
+
1555
+ left = Line.left 2
1556
+ Tree.after_children
1557
+ right = Line.left
1558
+ orig.go
1559
+ # return
1560
+ return View.txt left, right
1561
+ end
1562
+
1563
+ indent = Line.indent(Line.value(2)).size
1564
+
1565
+ # :as_hash isn't used anywhere
1566
+ children = options[:as_hash] ? {} : []
1567
+ i = 2
1568
+
1569
+ # Add each child indented the same or more
1570
+ while(Line.indent(Line.value(i)).size >= indent)
1571
+ child = Line.value(i)
1572
+ if options[:as_hash]
1573
+ match = child.match(/ *([\w -]+): (.+)/)
1574
+ if match
1575
+ k, v = match[1..2]
1576
+ children[k] = v
1577
+ else
1578
+ i += 1
1579
+ next
1580
+ end
1581
+
1582
+ else
1583
+ children << child
1584
+ end
1585
+
1586
+ i += 1
1587
+ end
1588
+
1589
+ if options[:string]
1590
+ return children.join("\n")+"\n"
1591
+ end
1592
+
1593
+ children
1594
+ end
1595
+
1596
+ def self.children?
1597
+ # Whether next line is more indented
1598
+ Line.indent(Line.value(2)).size >
1599
+ Line.indent.size
1600
+ end
1601
+
1602
+ #
1603
+ # Returns the child indented underneath (but not its children).
1604
+ #
1605
+ # Tree.child
1606
+ # foo
1607
+ # bar
1608
+ #
1609
+ def self.child
1610
+ following_line = Line.value 2
1611
+ # If indent is one greater, it is a child
1612
+ if Line.indent.size + 2 == Line.indent(following_line).size
1613
+ return Line.without_label(:line=>following_line)
1614
+ end
1615
+ nil
1616
+ end
1617
+
1618
+ #
1619
+ # Returns subtree rooted at cursor.
1620
+ #
1621
+ def self.subtree
1622
+
1623
+ # Just return line if no children
1624
+ return Line.value if ! Tree.has_child?
1625
+
1626
+ orig = View.cursor
1627
+ left = Line.left
1628
+ Line.next
1629
+ ignore, right = Tree.sibling_bounds :cross_blank_lines=>1
1630
+ View.cursor = orig
1631
+
1632
+ txt = View.txt left, right
1633
+ txt
1634
+ end
1635
+
1636
+ #
1637
+ # Goes to spot (from as+spot) and grabs path?
1638
+ #
1639
+ def self.dir_at_spot options={}
1640
+ orig = Location.new # Save where we are
1641
+ Location.to_spot
1642
+
1643
+ path = Tree.construct_path
1644
+ # TODO: Make it use this instead:
1645
+ # path = Tree.construct_path :string=>1 instead
1646
+
1647
+ if options[:delete]
1648
+ Effects.glow :fade_out=>1
1649
+ Tree.kill_under
1650
+ Line.delete
1651
+
1652
+ # Adjust orig if in same file and above
1653
+ if orig.file_or_buffer == View.file_or_buffer && orig.line > View.line_number
1654
+ orig.line = orig.line - 1
1655
+ end
1656
+ end
1657
+
1658
+ orig.go # Go back to where we were
1659
+
1660
+ Bookmarks[path]
1661
+ end
1662
+
1663
+ # Returns the dir that a @menu is nested under, or says
1664
+ # must be nested under a dir.
1665
+ #
1666
+ # Tree.dir
1667
+ def self.dir options={}
1668
+ self.file options.merge(:require=>'dir')
1669
+ end
1670
+
1671
+ # Returns the dir or file that a @menu is nested under.
1672
+ #
1673
+ # Tree.file
1674
+ # Tree.file :require=>1 # Shows message if not nested under something
1675
+ # Tree.file :require=>'dir' # Shows message if not nested under a dir
1676
+ # Tree.file :require=>'file' # Shows message if not nested under a file
1677
+ def self.file options={}
1678
+ trunk = Xiki.trunk
1679
+
1680
+ # If tree we're in is a file tree, they probably just wanted that
1681
+
1682
+ return Bookmarks[trunk[-1]] if FileTree.handles? trunk[-1]
1683
+ # Just return it if we're under a dir
1684
+
1685
+ dir = trunk[-2]
1686
+ dir = "/" if dir == "" # Root dir will come across as blank
1687
+
1688
+ return Bookmarks[dir].sub('//', '/') if FileTree.handles?(dir)
1689
+
1690
+ # Dir wasn't found, raise message for certain options
1691
+ return File.expand_path("~/Desktop") if options[:or] == :desktop
1692
+ kind_required = options[:require]
1693
+ return nil if ! kind_required
1694
+
1695
+ guessed_menu = trunk.last.split('/').first
1696
+ example = options[:example] || (kind_required == "file" ? "/tmp/file.txt" : "/tmp/dir/")
1697
+
1698
+ if kind = options[:require]
1699
+ adjective = kind.is_a?(String) ? kind : "filesystem"
1700
+ raise "> This menu must be nested under a #{adjective} path, like:\n| - #{example}\n| @#{guessed_menu}"
1701
+ end
1702
+ nil
1703
+ end
1704
+
1705
+ def self.path options={}
1706
+ path = Tree.construct_path(:all=>1, :slashes=>1)
1707
+ options[:string] ? path : path.split(/\/@ ?/)
1708
+ end
1709
+
1710
+ def self.paths_to_tree paths
1711
+ result = ""
1712
+ stack = []
1713
+ paths.sort.each do |path| # For each path
1714
+
1715
+ beginning_slash = path =~ /^\//
1716
+ ending_slash = path =~ /\/$/
1717
+ split = path.sub(/^\//, '').split('/')
1718
+
1719
+ split[0].sub! /^/, '/' if beginning_slash # Restore beginning slash after split
1720
+ split[-1].sub! /$/, '/' if ending_slash # Restore beginning slash after split
1721
+
1722
+ # put all slashes back first!
1723
+
1724
+ # Pop from stack until path begins with stack
1725
+ while(stack.size > 0 && stack != split[0..(stack.size - 1)])
1726
+ stack.pop
1727
+ end
1728
+ indent = stack.length # Get remainder of path after stack
1729
+ remainder = split[indent..-1]
1730
+ remainder.each do |dir|
1731
+ result << (" " * indent) + dir
1732
+ result << "\n"
1733
+ indent += 1
1734
+ end
1735
+ stack = split
1736
+ end
1737
+ self.add_pluses_and_minuses result
1738
+ result
1739
+ end
1740
+
1741
+ # Prepend bullets (pluses and minuses) to lines
1742
+ def self.add_pluses_and_minuses tree, dirs='-', files='+'
1743
+ tree.gsub! /^( *)([^ \n|+-].*\/)$/, "\\1#{dirs} \\2"
1744
+ tree.gsub! /^( *)([^ \n|+-].*[^\/\n])$/, "\\1#{files} \\2"
1745
+ end
1746
+
1747
+ # Tree.to_html "p/\n hi\n"
1748
+ def self.to_html txt
1749
+ html = ""
1750
+
1751
+ txt = txt.gsub /^( *)([+-] )?(\w[\w ]*\/)(.+)/, "\\1\\3\n\\1 \\4" # Preprocess to break foo/Bar into separate lines
1752
+
1753
+ previous = []
1754
+ Tree.traverse(txt) do |l, path|
1755
+
1756
+ last = l.last
1757
+ next if !last # Blank lines
1758
+
1759
+ self.add_closing_tags html, l, previous # If lower than last, add any closing tags
1760
+
1761
+ last = Line.without_label :line=>last
1762
+ if last =~ /([^*\n]+)\/$/
1763
+ tag = $1
1764
+ html.<<(" " * (l.length-1)) unless l[-2] =~ /[ +-]*pre\/$/
1765
+
1766
+ next html << "<#{tag}>\n"
1767
+ end
1768
+
1769
+ last.sub! /^\| ?/, ''
1770
+
1771
+ if last =~ /\.\.\.$/ # If "Lorem..." or "Lorem ipsum..." etc. make progressively longer
1772
+ old_length = last.length
1773
+ last.gsub!(/\w+/){|o| @@lorem[o.downcase] || o}
1774
+ last.sub!(/\.\.\.$/, '') if last.length != old_length # Remove ... if we replaced something
1775
+ end
1776
+
1777
+ parent = l[-2]
1778
+ html.<<(" " * (l.length-1)) unless parent =~ /[ +-]*pre\/$/
1779
+
1780
+ html << "#{last}\n"
1781
+ end
1782
+
1783
+
1784
+ self.add_closing_tags html, [], previous
1785
+
1786
+ html
1787
+ end
1788
+
1789
+ @@lorem = {
1790
+ "lorem"=>"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
1791
+ "ipsum"=>"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
1792
+ "dolor"=>"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
1793
+ "sit"=>"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
1794
+ }
1795
+
1796
+ def self.add_closing_tags html, l, previous
1797
+
1798
+ if l.length <= previous.length
1799
+ left = l.length-1
1800
+ left = 0 if left < 0
1801
+ close_these = previous[left..-1]
1802
+ close_these.reverse.each_with_index do |tag, i|
1803
+
1804
+ tag.sub! /^\| ?/, ''
1805
+ tag = Line.without_label :line=>tag
1806
+ next if tag !~ /(.*\w)\/$/ && tag !~ /^<([^<\n]*[\w"'])>$/
1807
+ tag = $1
1808
+ tag = tag.sub(/ \w+=.+/, '')
1809
+ next if ["img"].member? tag
1810
+ html << " " * (previous.length - i - 1)
1811
+ html << "</#{tag}>\n"
1812
+ end
1813
+ end
1814
+ previous.replace l
1815
+ end
1816
+
1817
+
1818
+ # Replace last path item with this string.
1819
+ #
1820
+ # Tree.replace_item "replaces whole line"
1821
+ # Tree.replace_item "replaces after slash" # /hey
1822
+ # Tree.replace_item "replaces after slash" # /hey/
1823
+ def self.replace_item txt
1824
+
1825
+ new_ends_in_slash = txt =~ /\/$/ # If slash at end, remember to not add another one
1826
+
1827
+ # If there's a slash (not counting end of line), replace after last slash
1828
+
1829
+ if Line.value =~ /\/.+/
1830
+ (! new_ends_in_slash && Line.sub!(/(.+)\/.+\/$/, "\\1/#{txt}/")) ||
1831
+ Line.sub!(/(.+)\/.+/, "\\1/#{txt}")
1832
+ else # else, just replace whole line minus bullet
1833
+ (! new_ends_in_slash && Line.sub!(/^([ +-]*).+\//, "\\1#{txt}\/")) ||
1834
+ Line.sub!(/^([ +-]*).+/, "\\1#{txt}")
1835
+ end
1836
+
1837
+ nil
1838
+ end
1839
+
1840
+
1841
+ #
1842
+ # Grab last path item.
1843
+ #
1844
+ # Tree.last_item
1845
+ # Tree.last_item # /hey
1846
+ # Tree.last_item # /hey/
1847
+ #
1848
+ def self.last_item
1849
+
1850
+ # If there's a slash (not counting end of line), use after last slash
1851
+
1852
+ if Line.value =~ /\/.+/
1853
+ Line[/.+\/(.+)\/$/, 1] || Line[/.+\/(.+)/, 1]
1854
+ else # else, just replace whole line minus bullet
1855
+ Line[/^[ +-]*(.+)\//, 1] || Line[/^[ +-]*(.+)/, 1]
1856
+ end
1857
+
1858
+ end
1859
+
1860
+ #
1861
+ # Returns whether the next line is indented one lower
1862
+ #
1863
+ # p Tree.has_child?
1864
+ #
1865
+ def self.has_child?
1866
+
1867
+ Line.indent(Line.value)+" " == Line.indent(Line.value 2)
1868
+
1869
+ end
1870
+
1871
+ end