ihelp 0.3.2 → 0.4.0

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 (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: