tk_inspect 0.1.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.
@@ -0,0 +1,6 @@
1
+ require_relative './class_browser/base'
2
+ require_relative './class_browser/root_component'
3
+ require_relative './class_browser/code_component'
4
+ require_relative './class_browser/class_tree_data_source'
5
+ require_relative './class_browser/class_namespace_data_source'
6
+ require_relative './class_browser/module_method_data_source'
@@ -0,0 +1,80 @@
1
+ module TkInspect
2
+ module ClassBrowser
3
+ class Base
4
+ attr_accessor :tk_root
5
+ attr_accessor :main_component
6
+ attr_accessor :selected_class_path
7
+ attr_accessor :selected_module
8
+ attr_accessor :selected_method
9
+ attr_accessor :browsing_method
10
+ attr_accessor :class_data_sources
11
+ attr_accessor :module_method_data_source
12
+
13
+ def initialize
14
+ @tk_root = nil
15
+ @main_component = nil
16
+ @selected_class_path = []
17
+ @loaded_code = {}
18
+ @class_filter = nil
19
+ @browsing_method = 'hierarchy'
20
+ @class_data_sources = {
21
+ hierarchy: ClassTreeDataSource.new,
22
+ namespaces: ClassNamespaceDataSource.new
23
+ }.with_indifferent_access
24
+ @module_method_data_source = ModuleMethodDataSource.new
25
+ end
26
+
27
+ def refresh
28
+ @main_component.nil? ? create_root : @main_component.regenerate
29
+ end
30
+
31
+ def create_root
32
+ @tk_root = TkComponent::Window.new(title: "Class Browser")
33
+ @main_component = RootComponent.new
34
+ @main_component.class_browser = self
35
+ @tk_root.place_root_component(@main_component)
36
+ end
37
+
38
+ def class_data_source
39
+ @class_data_sources[browsing_method]
40
+ end
41
+
42
+ def select_class_name(class_name)
43
+ self.browsing_method = 'hierarchy'
44
+ class_path = class_data_source.path_for_class(class_name)
45
+ select_class_path(class_path)
46
+ end
47
+
48
+ def show_current_path
49
+ @main_component.show_current_selection
50
+ end
51
+
52
+ def select_class_path(class_path)
53
+ self.selected_class_path = class_path
54
+ module_method_data_source.selected_class = selected_class_name
55
+ end
56
+
57
+ def selected_class_name
58
+ browsing_method.to_s == 'hierarchy' ? self.selected_class_path.last : self.selected_class_path.join('::')
59
+ end
60
+
61
+ def code_for_method(meth)
62
+ return nil unless meth.present?
63
+ class_name = selected_class_name
64
+ return nil unless class_name.present?
65
+ klass = Object.const_get(class_name)
66
+ method = klass.instance_method(meth)
67
+ file, line = method.source_location
68
+ return nil unless file && line
69
+ return code_for_file(file), line, file
70
+ end
71
+
72
+ def code_for_file(file)
73
+ @loaded_code[file] || begin
74
+ @loaded_code[file] = IO.read(file)
75
+ @loaded_code[file]
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,55 @@
1
+ module TkInspect
2
+ module ClassBrowser
3
+ class ClassNamespaceDataSource
4
+ attr_accessor :class_filter
5
+
6
+ def items_for_path(path)
7
+ path = [] if path.blank?
8
+ namespace = path.join('::')
9
+ classes = classes_in_namespace(namespace)
10
+ class_names = classes
11
+ .map { |k| name_for_class(k).gsub(/^#{namespace}(::)?/, '').gsub(/::.*/, '') }
12
+ .reject { |class_name| class_name.blank? }
13
+ .uniq
14
+ .sort
15
+ class_names
16
+ end
17
+
18
+ def title_for_path(path, items)
19
+ "#{items.count} #{'class'.pluralize(items.count)}"
20
+ end
21
+
22
+ private
23
+
24
+ def classes_in_namespace(namespace)
25
+ filtered_classes.select do |k|
26
+ name_for_class(k).match(/^#{namespace}/)
27
+ end
28
+ end
29
+
30
+ def filtered_classes
31
+ list = ObjectSpace.each_object(Class).select do |k|
32
+ k.is_a?(Class) && !black_list?(k)
33
+ end
34
+ return list if @class_filter.blank?
35
+ list.select! do |k|
36
+ name_for_class(k).match(/#{@class_filter}/i)
37
+ end
38
+ ancestors = list.reduce(Set.new) do |acum, k|
39
+ acum += name_for_class(k).split('::')
40
+ acum
41
+ end
42
+ (list.to_set + ancestors).to_a
43
+ end
44
+
45
+ def name_for_class(klass)
46
+ klass.to_s
47
+ end
48
+
49
+ def black_list?(k)
50
+ k.to_s.blank? || k.to_s.match(/^#<Class/) || !k.to_s.match(/^[A-Z]/)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,57 @@
1
+ module TkInspect
2
+ module ClassBrowser
3
+ class ClassTreeDataSource
4
+ attr_accessor :class_filter
5
+
6
+ def items_for_path(path)
7
+ path = [] if path.blank?
8
+ parent_class = path.last
9
+ subclasses_of(parent_class).sort
10
+ end
11
+
12
+ def title_for_path(path, items)
13
+ "#{items.count} #{'class'.pluralize(items.count)}"
14
+ end
15
+
16
+ def path_for_class(class_name)
17
+ return unless (klass = Object.const_get(class_name))
18
+ path = [class_name]
19
+ while (klass = klass.superclass) do
20
+ path.unshift(name_for_class(klass))
21
+ end
22
+ path
23
+ end
24
+
25
+ private
26
+
27
+ def subclasses_of(class_name)
28
+ filtered_classes.select do |k|
29
+ k.superclass&.to_s == class_name
30
+ end.map { |k| name_for_class(k) }
31
+ end
32
+
33
+ def filtered_classes
34
+ list = ObjectSpace.each_object(Class).select do |k|
35
+ !k.name.nil?
36
+ end
37
+ return list if @class_filter.blank?
38
+ list.select! do |k|
39
+ k.name.match(/#{@class_filter}/i)
40
+ end
41
+ ancestors = list.reduce(Set.new) do |acum, k|
42
+ an = k
43
+ while !(an = an.superclass).nil? do
44
+ acum << an
45
+ end
46
+ acum
47
+ end
48
+ (list.to_set + ancestors).to_a
49
+ end
50
+
51
+ def name_for_class(klass)
52
+ klass.respond_to?(:name) ? klass.name : klass.to_s
53
+ end
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,75 @@
1
+ require 'active_support/all'
2
+ require 'rouge'
3
+
4
+ module TkInspect
5
+ module ClassBrowser
6
+ TOKEN_TAGS = {
7
+ 'Rouge::Token::Tokens::Comment::Single' => :comment,
8
+ 'Rouge::Token::Tokens::Keyword' => :keyword,
9
+ 'Rouge::Token::Tokens::Name::Class' => :constant,
10
+ 'Rouge::Token::Tokens::Name::Constant' => :constant,
11
+ 'Rouge::Token::Tokens::Name::Function' => :method,
12
+ 'Rouge::Token::Tokens::Str::Double' => :string,
13
+ 'Rouge::Token::Tokens::Str::Single' => :string,
14
+ 'Rouge::Token::Tokens::Str::Interpol' => :string,
15
+ 'Rouge::Token::Tokens::Str::Symbol' => :symbol,
16
+ 'Rouge::Token::Tokens::Str::Heredoc' => :string,
17
+ 'Rouge::Token::Tokens::Name::Variable::Instance' => :ivar
18
+ }
19
+
20
+ TAG_STYLES = {
21
+ string: { foreground: 'red' },
22
+ comment: { foreground: 'gray' },
23
+ keyword: { foreground: 'blue' },
24
+ constant: { foreground: 'green' },
25
+ symbol: { foreground: 'purple' },
26
+ method: { foreground: 'brown' },
27
+ ivar: { foreground: 'brown' }
28
+ }
29
+
30
+ class CodeComponent < TkComponent::Base
31
+ attr_accessor :code
32
+ attr_accessor :filename
33
+ attr_accessor :method_name
34
+ attr_accessor :method_line
35
+
36
+ def generate(parent_component, options = {})
37
+ parse_component(parent_component, options) do |p|
38
+ p.vframe(padding: "0 0 0 0", sticky: 'nsew', x_flex: 1, y_flex: 1) do |vf|
39
+ @filename_label = vf.label(font: 'TkSmallCaptionFont', sticky: 'ewn', x_flex: 1, y_flex: 0)
40
+ @code_text = vf.text(sticky: 'nswe', x_flex: 1, y_flex: 1, wrap: 'none', scrollers: 'xy')
41
+ end
42
+ end
43
+ end
44
+
45
+ def component_did_build
46
+ TAG_STYLES.each do |k,v|
47
+ @code_text.native_item.tag_configure(k.to_s, v)
48
+ end
49
+ end
50
+
51
+ def update
52
+ if @code && @method_line
53
+ highlight_code(@code_text.native_item, @code, @filename)
54
+ @code_text.tk_item.select_range("#{@method_line}.0", "#{@method_line}.end")
55
+ @code_text.native_item.see("#{@method_line}.0")
56
+ @filename_label.native_item.text(@filename)
57
+ else
58
+ @filename_label.native_item.text('')
59
+ @code_text.tk_item.value = "Source code not available for #{@method_name}"
60
+ end
61
+ end
62
+
63
+ @@lexers = {}
64
+
65
+ def highlight_code(native_item, code, filename)
66
+ lexer = (@@lexers[filename] ||= Rouge::Lexers::Ruby.lex(code))
67
+ native_item.replace('1.0', 'end', '')
68
+ lexer.each do |token, chunk|
69
+ tag = TOKEN_TAGS[token.to_s]
70
+ native_item.insert('end', chunk, tag.to_s)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,86 @@
1
+ module TkInspect
2
+ module ClassBrowser
3
+ class ModuleMethodDataSource
4
+ attr_accessor :selected_class
5
+
6
+ def items_for_path(path)
7
+ path = [] if path.blank?
8
+ case path.size
9
+ when 0
10
+ return modules_of(selected_class)
11
+ when 1
12
+ return methods_of(path.first)
13
+ else
14
+ return []
15
+ end
16
+ end
17
+
18
+ def title_for_path(path, items)
19
+ path = [] if path.blank?
20
+ case path.size
21
+ when 0
22
+ return "#{selected_class} -> #{items.size} #{'module'.pluralize(items.size)}"
23
+ when 1
24
+ if (module_name = path.first).present?
25
+ return "#{module_name} -> #{items.size} #{'method'.pluralize(items.size)}"
26
+ else
27
+ "#{items.size} #{'method'.pluralize(items.size)} in total"
28
+ end
29
+ else
30
+ return ''
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def modules_of(class_name)
37
+ return [] unless class_name.present?
38
+ begin
39
+ klass = Object.const_get(class_name)
40
+ rescue NameError
41
+ return []
42
+ end
43
+ return [] unless klass.is_a?(Class)
44
+ # We start with the empty module (to show all methods) and the class itself
45
+ modules = [ label_for_all_modules, klass ]
46
+ # Now we add the ancestor classes
47
+ parent = klass.superclass
48
+ while parent do
49
+ modules << parent
50
+ parent = parent.superclass
51
+ end
52
+ # Last, we add the included modules
53
+ (modules + klass.singleton_class.included_modules).map { |m| name_for_module(m) }
54
+ end
55
+
56
+ def methods_of(mod)
57
+ methods = nil
58
+ if mod == label_for_all_modules
59
+ # We want all the methods of the selected class
60
+ mod = Object.const_get(selected_class)
61
+ methods = mod&.instance_methods(true)
62
+ else
63
+ mod = Object.const_get(mod)
64
+ methods = mod&.instance_methods(false)
65
+ end
66
+ methods
67
+ .map { |m| name_for_method(m) }
68
+ .sort
69
+ end
70
+
71
+ def name_for_module(mod)
72
+ return mod if mod.nil?
73
+ mod.respond_to?(:name) ? mod.name : mod.to_s
74
+ end
75
+
76
+ def name_for_method(meth)
77
+ meth.to_s
78
+ end
79
+
80
+ def label_for_all_modules
81
+ '<all>'
82
+ end
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,88 @@
1
+ require 'active_support/all'
2
+
3
+ module TkInspect
4
+ module ClassBrowser
5
+ class RootComponent < TkComponent::Base
6
+ attr_accessor :class_browser
7
+
8
+ def generate(parent_component, options = {})
9
+ parse_component(parent_component, options) do |p|
10
+ p.vframe(sticky: 'nsew', x_flex: 1, y_flex: 1) do |vf|
11
+ vf.hframe(padding: "12", sticky: 'nsew', x_flex: 1, y_flex: 0) do |hf|
12
+ hf.hframe(sticky: 'nw') do |f|
13
+ f.label(text: "Browse by: ")
14
+ f.radio_set(value: "hierarchy", on_change: :browse_by_changed) do |rs|
15
+ rs.radio_button(text: "Class hierarchy", value: 'hierarchy')
16
+ rs.radio_button(text: "Namespaces", value: 'namespaces')
17
+ end
18
+ end
19
+ hf.hframe(sticky: 'ne', x_flex: 1) do |f|
20
+ f.label(text: "Filter")
21
+ f.entry(value: class_browser.class_data_source.class_filter, on_change: :filter_changed)
22
+ end
23
+ end
24
+ vf.vpaned(sticky: 'nsew', x_flex: 1, y_flex: 1) do |vp|
25
+ @class_browser_comp = vp.insert_component(TkComponent::BrowserComponent, self,
26
+ data_source: class_browser.class_data_source,
27
+ selected_path: class_browser.selected_class_path,
28
+ paned: true,
29
+ sticky: 'nsew', x_flex: 1, y_flex: 1) do |bc|
30
+ bc.on_event'PathChanged', ->(e) { class_selected(e) }
31
+ end
32
+ @module_method_browser_component = vp.insert_component(TkComponent::RBrowserComponent, self,
33
+ data_source: class_browser.module_method_data_source,
34
+ paned: true,
35
+ max_columns: 2,
36
+ sticky: 'nsew', x_flex: 1, y_flex: 1) do |bc|
37
+ bc.on_event'PathChanged', ->(e) { module_method_selected(e) }
38
+ end
39
+ @code = vp.insert_component(CodeComponent, self, sticky: 'nsew', x_flex: 1, y_flex: 1)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def show_current_selection
46
+ @class_browser_comp.show_current_selection
47
+ end
48
+
49
+ def class_selected(e)
50
+ @class_browser.select_class_path(e.data_object.selected_path)
51
+ @module_method_browser_component.selected_path = []
52
+ @module_method_browser_component.regenerate
53
+ end
54
+
55
+ def module_method_selected(e)
56
+ path = e.data_object.selected_path || []
57
+ module_name = path[0]
58
+ method_name = path[1]
59
+ code, line, file = @class_browser.code_for_method(method_name)
60
+ @code.method_name = method_name
61
+ @code.code = code
62
+ @code.method_line = line
63
+ @code.filename = file
64
+ @code.update
65
+ end
66
+
67
+ def filter_changed(e)
68
+ return if class_browser.class_data_source.class_filter == e.sender.s_value
69
+ class_browser.class_data_source.class_filter = e.sender.s_value
70
+ @class_browser.select_class_path([])
71
+ @class_browser_comp.selected_path = []
72
+ @module_method_browser_component.selected_path = []
73
+ @class_browser_comp.regenerate
74
+ @module_method_browser_component.regenerate
75
+ end
76
+
77
+ def browse_by_changed(e)
78
+ @class_browser.browsing_method = e.sender.value
79
+ @class_browser_comp.data_source = @class_browser.class_data_source
80
+ @class_browser.select_class_path([])
81
+ @class_browser_comp.selected_path = []
82
+ @module_method_browser_component.selected_path = []
83
+ @class_browser_comp.regenerate
84
+ @module_method_browser_component.regenerate
85
+ end
86
+ end
87
+ end
88
+ end