ihelp 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/ihelp.rb +420 -17
  2. data/lib/ihelp_reindex.rb +12 -0
  3. metadata +14 -6
data/lib/ihelp.rb CHANGED
@@ -1,5 +1,16 @@
1
1
  require 'rdoc/ri/ri_driver'
2
2
  require 'rexml/document'
3
+ begin
4
+ if $ihelp_full_text_search != false
5
+ require 'rubygems'
6
+ require 'ferret'
7
+ require 'fileutils'
8
+ $ihelp_full_text_search = true
9
+ end
10
+ rescue Exception
11
+ $ihelp_full_text_search = false
12
+ end
13
+
3
14
 
4
15
  # Ri bindings for interactive use from within Ruby.
5
16
  # Does a bit of second-guessing (Instance method? Class method?
@@ -23,10 +34,15 @@ require 'rexml/document'
23
34
  # String.instance_help :reverse
24
35
  # String.instance_help :new # => No help found.
25
36
  # a.help :new
26
- # help "String#reverse"
27
- # help "String.reverse"
37
+ # ihelp "String#reverse"
38
+ # ihelp "String.reverse"
28
39
  # a.method(:reverse).help # gets help for Method
29
- # help "Hash#map"
40
+ # ihelp "Hash#map"
41
+ #
42
+ # You can also search for terms in the documentation:
43
+ #
44
+ # ihelp 'domain name lookup'
45
+ # String.help 'byte order'
30
46
  #
31
47
  #
32
48
  # Custom help renderers:
@@ -81,6 +97,9 @@ require 'rexml/document'
81
97
  #
82
98
  # Changelog:
83
99
  #
100
+ # 0.4.0
101
+ # Full-text documentation search using Ferret.
102
+ #
84
103
  # 0.3.2
85
104
  # Added support for ruby 1.8.5, added emacs renderer from
86
105
  # rubykitch <rubykitch@ruby-lang.org>, made source renderer use the
@@ -91,7 +110,7 @@ require 'rexml/document'
91
110
  # Author: Ilmari Heikkinen <kig misfiring net>
92
111
  #
93
112
  module IHelp
94
- HELP_VERSION = "0.3.2"
113
+ HELP_VERSION = "0.4.0"
95
114
  end
96
115
 
97
116
 
@@ -188,14 +207,35 @@ module IHelp
188
207
  # Uses help_description(method_name, instance).
189
208
  #
190
209
  def help(method_name=nil, instance=nil)
210
+ if $ihelp_full_text_search and (
211
+ method_name and method_name.class == String and
212
+ self.class == Object and to_s == 'main' and
213
+ not method_name =~ /::|\.|#/ and
214
+ not method_name =~ /\A[A-Z][a-z0-9A-Z]*\Z/
215
+ ) # calling for main
216
+ IHelp.search method_name
217
+ return
218
+ end
219
+ if $ihelp_full_text_search and method_name and method_name.to_s.include? " " # phrase search
220
+ puts "Searching from docs..."
221
+ help_search method_name
222
+ return
223
+ end
191
224
  info = help_description(method_name, instance)
192
225
  if not info
193
- puts "No help found."
226
+ if $ihelp_full_text_search and method_name
227
+ puts "No help found for method of that name, searching from docs..."
228
+ help_search method_name
229
+ else
230
+ puts "No help found."
231
+ end
194
232
  return
195
233
  end
196
234
  IHelp.render(info)
197
235
  end
198
236
 
237
+ alias_method :ihelp, :help
238
+
199
239
  # Print out help for instance method method_name.
200
240
  # If no method_name given, behaves like #help.
201
241
  #
@@ -236,6 +276,28 @@ module IHelp
236
276
  IHelp.generate_help_description(self, method_name, instance)
237
277
  end
238
278
 
279
+ # Searches for str in the documentation of this object.
280
+ #
281
+ def help_search(str)
282
+ raise "No search capability, do you have ferret installed? (gem install ferret)" unless $ihelp_full_text_search
283
+ ms = if is_a? Module
284
+ instance_methods.map{|im| instance_method im } +
285
+ methods.map{|m| method m }
286
+ else
287
+ methods.map{|m| method m }
288
+ end
289
+ phrases = ms.map do |m|
290
+ mod, met = m.inspect.split(" ",2)[1][0..-2].split(/#|\./)
291
+ rmod = mod.scan(/\(([^\)]+)\)/).flatten[0]
292
+ rmod ||= mod
293
+ rmod.gsub(/[^a-z0-9]/i){|c| "\\"+c }
294
+ end.uniq
295
+ phrases.delete ""
296
+ name_query = phrases.join(" OR ")
297
+ query = "(name:(#{name_query})) AND (*:#{IHelp.intersect_search_query str})"
298
+ IHelp.search query, false
299
+ end
300
+
239
301
 
240
302
  class << self
241
303
 
@@ -245,6 +307,9 @@ module IHelp
245
307
  # Web browser to use for html and rubydoc renderers.
246
308
  attr_accessor :web_browser
247
309
 
310
+ # Don't use colors for highlighting search results.
311
+ attr_accessor :no_colors
312
+
248
313
  IHelp.renderer ||= :ri
249
314
  IHelp.web_browser ||= 'firefox'
250
315
 
@@ -253,6 +318,38 @@ module IHelp
253
318
  IHelp::RI_ARGS = ENV["RI"].split.concat(ARGV)
254
319
  end
255
320
 
321
+ # Searches for str from available documentation and prints out the
322
+ # results.
323
+ #
324
+ # Creates a search index if you don't already have one. Creating the index
325
+ # may take a couple of minutes.
326
+ #
327
+ # See IHelpIndex.
328
+ #
329
+ def search(str, escape_query=true)
330
+ raise "No search capability, do you have ferret installed? (gem install ferret)" unless $ihelp_full_text_search
331
+ if escape_query
332
+ help_index.search(intersect_search_query(str.to_s))
333
+ else
334
+ help_index.search(str)
335
+ end
336
+ nil
337
+ end
338
+
339
+ def intersect_search_query(str)
340
+ a = IHelpAnalyzer.new
341
+ t = a.token_stream :content, str.to_s
342
+ c = []
343
+ n = nil
344
+ c << n.text while n = t.next
345
+ "(#{c.join(" AND ")})"
346
+ end
347
+
348
+ attr_accessor :help_index
349
+ def help_index
350
+ @help_index ||= IHelpIndex.new
351
+ end
352
+
256
353
  # Render the RI info object a renderer method in IHelp::Renderer.
257
354
  # The name of the renderer method to use is returned by IHelp.renderer,
258
355
  # and can be set with IHelp.renderer=.
@@ -277,6 +374,11 @@ module IHelp
277
374
  meth_str, klass_name, instance_help, double_colon =
278
375
  get_help_klass_info_for_name(meth_str)
279
376
  klass_ancs = find_ancestors(klass_name, instance)
377
+ elsif (/\A[A-Z][a-zA-Z0-9_]*\Z/ === meth_str and # called with e.g. "Array"
378
+ not (klass.methods+Kernel.methods).include? meth_str)
379
+ meth_str, klass_name, instance_help, double_colon =
380
+ get_help_klass_info_for_name(meth_str)
381
+ klass_ancs = find_ancestors(klass_name, instance)
280
382
  else
281
383
  klass_name, klass_ancs, instance_help =
282
384
  get_help_klass_info(klass, instance)
@@ -388,6 +490,8 @@ module IHelp
388
490
  #
389
491
  class IHelpDriver < RiDriver
390
492
 
493
+ attr_accessor :ri_reader, :display
494
+
391
495
  # Create new IHelpDriver, with the given args
392
496
  # passed to @options, which is a RI::Options.instance
393
497
  #
@@ -418,7 +522,7 @@ module IHelp
418
522
  }
419
523
  return nil if namespaces.empty?
420
524
  if method_name.nil?
421
- get_class_info_str(namespaces)
525
+ get_class_info_str(namespaces, klass_name)
422
526
  else
423
527
  methods = @ri_reader.find_methods(
424
528
  method_name, is_class_method, namespaces)
@@ -441,11 +545,13 @@ module IHelp
441
545
 
442
546
  # Get info for the class in the given namespaces.
443
547
  #
444
- def get_class_info_str(namespaces)
548
+ def get_class_info_str(namespaces, klass_name)
445
549
  return nil if namespaces.empty?
550
+ klass_name_last = klass_name.split("::").last
446
551
  klass = nil
447
552
  namespaces.find{|ns|
448
553
  begin
554
+ ns.name == klass_name_last and
449
555
  klass = @ri_reader.get_class(ns)
450
556
  rescue TypeError
451
557
  nil
@@ -471,6 +577,240 @@ module IHelp
471
577
  end
472
578
 
473
579
 
580
+ # IHelpIndex uses Ferret to index all available RI documentation
581
+ # and lets you do full text searches over the index.
582
+ #
583
+ # E.g. IHelpIndex.new.search("domain name lookup")
584
+ #
585
+ # See Ferret::QueryParser for query string format.
586
+ #
587
+ class IHelpIndex
588
+
589
+ class << self
590
+ attr_accessor :reindex_when_needed
591
+ end
592
+
593
+ # The search index, is a Ferret::Index::Index
594
+ attr_accessor :index
595
+
596
+ attr_accessor :log_level
597
+
598
+ # Default place to save and load the index from.
599
+ GLOBAL_INDEX_PATH = File.join(File.dirname(__FILE__), "ihelp.index")
600
+ LOCAL_INDEX_PATH = File.join(ENV["HOME"], ".ihelp.index")
601
+ DEFAULT_LOG_LEVEL = 2
602
+
603
+ def initialize(use_global_index = (ENV["USER"] == "root"))
604
+ @log_level = DEFAULT_LOG_LEVEL
605
+ if use_global_index
606
+ @index_path = GLOBAL_INDEX_PATH
607
+ else
608
+ @index_path = LOCAL_INDEX_PATH
609
+ if not File.exist?(@index_path) and File.exist?(GLOBAL_INDEX_PATH)
610
+ FileUtils.cp_r(GLOBAL_INDEX_PATH, @index_path) rescue nil
611
+ end
612
+ end
613
+ analyzer = IHelpAnalyzer.new
614
+ have_index = File.exist? @index_path
615
+ if have_index
616
+ log "Loading existing index from #{@index_path}", 1
617
+ @index = Ferret::I.new(
618
+ :key => :full_name,
619
+ :path => @index_path,
620
+ :analyzer => analyzer
621
+ )
622
+ else
623
+ log "Creating new index in #{@index_path}", 2
624
+ field_infos = Ferret::Index::FieldInfos.new(:term_vector => :no)
625
+ field_infos.add_field(:name,
626
+ :store => :yes,
627
+ :index => :yes,
628
+ :boost => 10.0)
629
+ field_infos.add_field(:full_name,
630
+ :store => :yes,
631
+ :index => :untokenized,
632
+ :boost => 0.0)
633
+ field_infos.add_field(:content,
634
+ :boost => 1.0,
635
+ :store => :yes,
636
+ :index => :yes,
637
+ :term_vector => :with_positions_offsets)
638
+ @index = Ferret::I.new(
639
+ :key => :full_name,
640
+ :field_infos => field_infos,
641
+ :analyzer => analyzer
642
+ )
643
+ end
644
+ reindex! if self.class.reindex_when_needed and need_reindexing?
645
+ unless have_index
646
+ @index.persist(@index_path)
647
+ log "Saved index.", 2
648
+ end
649
+ end
650
+
651
+ def log str, level = 1
652
+ STDERR.puts str if level >= @log_level
653
+ end
654
+
655
+ def time
656
+ t = Time.now
657
+ rv = yield
658
+ log "Took #{Time.now - t}"
659
+ rv
660
+ end
661
+
662
+ # Returns true if there already is an object named full_name
663
+ #
664
+ def already_indexed? full_name
665
+ @index[full_name]
666
+ end
667
+
668
+ def format_time(t)
669
+ sec = t % 60
670
+ min = (t / 60) % 60
671
+ hour = (t / 3600)
672
+ str = sec.to_i.to_s.rjust(2,'0')
673
+ str = min.to_i.to_s.rjust(2,'0') + ":" + str
674
+ str = hour.to_i.to_s.rjust(2,'0') + ":" + str if hour >= 1
675
+ str
676
+ end
677
+
678
+ def all_names
679
+ IHelp.ri_driver.ri_reader.all_names.uniq
680
+ end
681
+
682
+ def need_reindexing?
683
+ all_names.size > @index.size
684
+ end
685
+
686
+ # Reindexes any non-indexed help documents.
687
+ #
688
+ def reindex!
689
+ start_size = @index.size
690
+ names = all_names
691
+ log "Indexing...", 2
692
+ nsz = names.size.to_s
693
+ i = 0
694
+ names.each{|n|
695
+ i += 1
696
+ next if (start_size > 0) and (already_indexed?(n))
697
+ if @log_level == DEFAULT_LOG_LEVEL
698
+ amt_done = (i.to_f / names.size)
699
+ pct = ("%.1f" % [100*amt_done]).rjust(5)
700
+ STDERR.write "\r #{pct}% (#{i.to_s.rjust(nsz.size)}/#{nsz}) #{n}".ljust(80)[0,80]
701
+ STDERR.flush
702
+ end
703
+ hd = if n.include? "#"
704
+ IHelp.ri_driver.get_info_str(*(n.split("#") + [true]))
705
+ elsif n.include? "::"
706
+ IHelp.ri_driver.get_info_str(n) or
707
+ IHelp.ri_driver.get_info_str(*n.reverse.split("::",2).reverse.map{|r| r.reverse })
708
+ else
709
+ IHelp.ri_driver.get_info_str n
710
+ end
711
+ if hd.nil?
712
+ log "skipping #{n} (not found)"
713
+ next
714
+ else
715
+ log "indexing #{n}"
716
+ end
717
+ @index << {
718
+ :name => hd.full_name,
719
+ :full_name => n,
720
+ :content => hd.to_text
721
+ }
722
+ }
723
+ if @log_level == DEFAULT_LOG_LEVEL
724
+ amt_done = (i.to_f / names.size)
725
+ pct = ("%.1f" % [100*amt_done]).rjust(5)
726
+ STDERR.write "\r #{pct}% (#{i.to_s.rjust(nsz.size)}/#{nsz}) #{names.last}".ljust(80)[0,80]
727
+ STDERR.flush
728
+ end
729
+ if start_size != @index.size
730
+ log "Optimizing index... (old size #{start_size}, current size #{@index.size})"
731
+ @index.optimize
732
+ end
733
+ end
734
+
735
+ def display
736
+ return @display if @display
737
+ @display = RI::Options.instance.displayer.clone
738
+ if ENV["PAGER"].to_s.size > 0
739
+ unless ENV["PAGER"] =~ /(^|\/)less\b.* -[a-zA-Z]*[rR]/
740
+ IHelp.no_colors = true
741
+ end
742
+ else
743
+ ENV["PAGER"] = "less -R"
744
+ end
745
+ class << @display
746
+ public :page
747
+ attr_accessor :formatter
748
+ end
749
+ @display
750
+ end
751
+
752
+ def page(*a,&b)
753
+ display.page(*a,&b)
754
+ end
755
+
756
+ def wrap(str)
757
+ display.formatter.wrap(str)
758
+ end
759
+
760
+ def init_display
761
+ display
762
+ end
763
+
764
+ # Searches for the terms in query and prints out first ten hits
765
+ #
766
+ # See Ferret::QueryParser for query string format.
767
+ #
768
+ def search(query, *a)
769
+ result = @index.search(query, *a)
770
+ init_display
771
+ if IHelp.no_colors or `which less`.strip.empty?
772
+ pre_tag = "<<"
773
+ post_tag = ">>"
774
+ else
775
+ name_start = "\033[32m"
776
+ name_end = "\033[0m"
777
+ pre_tag = "\033[36m"
778
+ post_tag = "\033[0m"
779
+ end
780
+ page do
781
+ puts
782
+ puts "=== #{result.total_hits} hits#{", showing first 10" if result.total_hits > 10} ".ljust(72,"=")
783
+ puts
784
+ result.hits.each_with_index do |hit, i|
785
+ id, score = hit.doc, hit.score
786
+ #puts hit.score
787
+ puts "#{name_start}#{@index[id][:full_name]}#{name_end}"
788
+ puts
789
+ wrap @index.highlight(query, id, :field => :content,
790
+ :pre_tag => pre_tag,
791
+ :post_tag => post_tag).to_s
792
+ puts
793
+ display.formatter.draw_line
794
+ puts
795
+ end
796
+ puts
797
+ end
798
+ p result.hits.map{|hit| @index[hit.doc][:full_name] }
799
+ result
800
+ end
801
+
802
+ end
803
+
804
+
805
+ if defined? Ferret
806
+ class IHelpAnalyzer < Ferret::Analysis::StandardAnalyzer
807
+ def token_stream(*a)
808
+ Ferret::Analysis::StemFilter.new(super)
809
+ end
810
+ end
811
+ end
812
+
813
+
474
814
  end
475
815
 
476
816
 
@@ -490,15 +830,16 @@ module RI
490
830
  header = root.add_element(header_tag)
491
831
  header.add_text(full_name)
492
832
  comment.each{|c|
493
- tag = c.class.to_s.split("::").last
494
- tag = "PRE" if tag == "VERB"
495
- xmlstr = "<#{tag}>#{c.body}</#{tag}>"
496
- c_doc = REXML::Document.new(xmlstr)
497
- root.add_element( c_doc.root )
833
+ root.add_element( c.to_html.root )
498
834
  }
499
835
  doc
500
836
  end
501
837
 
838
+ def to_text
839
+ name + " -- " + params.to_s + block_params.to_s + "\n" +
840
+ (comment || []).map{|c| c.to_text }.join("\n")
841
+ end
842
+
502
843
  end
503
844
 
504
845
 
@@ -520,11 +861,7 @@ module RI
520
861
  header = root.add_element(header_tag)
521
862
  header.add_text(full_name)
522
863
  comment.each{|c|
523
- tag = c.class.to_s.split("::").last
524
- tag = "PRE" if tag == "VERB"
525
- xmlstr = "<#{tag}>#{c.body}</#{tag}>"
526
- c_doc = REXML::Document.new(xmlstr)
527
- root.add_element( c_doc.root )
864
+ root.add_element( c.to_html.root )
528
865
  }
529
866
  root.add_element(methods_header_tag).add_text("Class Methods")
530
867
  cmethods = root.add_element(methods_tag)
@@ -543,6 +880,34 @@ module RI
543
880
  doc
544
881
  end
545
882
 
883
+ def to_text
884
+ segs = [full_name]
885
+ segs += (comment || []).map{|c| c.to_text }
886
+ segs << "" << "Class methods: " << (class_methods || []).map{|m| m.to_text }.join(", ")
887
+ segs << "" << "Instance methods: " << (instance_methods || []).map{|m| m.to_text }.join(", ")
888
+ segs << "" << "Constants: " << (constants || []).map{|m| m.to_text }.join("\n")
889
+ segs << "" << "Attributes: " << (attributes || []).map{|m| m.to_text }.join("\n")
890
+ segs.join("\n")
891
+ end
892
+
893
+ end
894
+
895
+
896
+ class Attribute
897
+
898
+ def to_text
899
+ "#{name} [#{rw}] #{(comment || []).map{|c| c.to_text }.join("\n")}"
900
+ end
901
+
902
+ end
903
+
904
+
905
+ class Constant
906
+
907
+ def to_text
908
+ "#{name} = #{value} #{(comment || []).map{|c| c.to_text }.join("\n")}"
909
+ end
910
+
546
911
  end
547
912
 
548
913
 
@@ -560,6 +925,44 @@ module RI
560
925
  doc
561
926
  end
562
927
 
928
+ def to_text
929
+ name
930
+ end
931
+
932
+ end
933
+
934
+
935
+ end
936
+
937
+
938
+ module SM::Flow
939
+ constants.each{|c|
940
+ const_get(c).__send__(:include, self)
941
+ }
942
+
943
+ def to_text
944
+ RI::TextFormatter.allocate.conv_html(
945
+ if respond_to? :body
946
+ body
947
+ else
948
+ ""
949
+ end +
950
+ if respond_to? :contents
951
+ contents.map{|c| c.to_text }.join("\n\n")
952
+ else
953
+ ""
954
+ end
955
+ ).gsub(/<\/?..>/, '')
956
+ end
957
+
958
+ def to_html
959
+ tag = self.class.to_s.split("::").last
960
+ tag = "PRE" if tag == "VERB"
961
+ xmlstr = %Q(<#{tag}>
962
+ #{body if respond_to? :body}
963
+ #{contents.map{|c|c.to_html} if respond_to? :content}
964
+ </#{tag}>)
965
+ REXML::Document.new(xmlstr)
563
966
  end
564
967
 
565
968
  end
@@ -0,0 +1,12 @@
1
+ load File.join(File.expand_path(File.dirname(__FILE__)), 'ihelp.rb')
2
+
3
+ IHelp::IHelpIndex.reindex_when_needed = true
4
+ IHelp::IHelpIndex.new(true)
5
+ begin
6
+ path = IHelp::IHelpIndex::GLOBAL_INDEX_PATH
7
+ STDERR.puts
8
+ STDERR.puts 'Making global index at '+path+' readable'
9
+ Dir[File.join(path,"*")].each{|n| File.chmod 0644, n }
10
+ File.open("Makefile",'w'){|f| f.puts "install:" }
11
+ end
12
+
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
3
3
  specification_version: 1
4
4
  name: ihelp
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.3.2
7
- date: 2006-11-13 00:00:00 +02:00
6
+ version: 0.4.0
7
+ date: 2006-11-16 00:00:00 +02:00
8
8
  summary: Interactive help
9
9
  require_paths:
10
10
  - lib
@@ -37,9 +37,17 @@ extra_rdoc_files: []
37
37
 
38
38
  executables: []
39
39
 
40
- extensions: []
41
-
40
+ extensions:
41
+ - lib/ihelp_reindex.rb
42
42
  requirements: []
43
43
 
44
- dependencies: []
45
-
44
+ dependencies:
45
+ - !ruby/object:Gem::Dependency
46
+ name: ferret
47
+ version_requirement:
48
+ version_requirements: !ruby/object:Gem::Version::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 0.10.13
53
+ version: