vimmate 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/.autotest +10 -0
  2. data/CHANGELOG +108 -0
  3. data/COPYING +20 -0
  4. data/README +221 -0
  5. data/Rakefile +31 -0
  6. data/TODO +21 -0
  7. data/bin/vimmate +105 -0
  8. data/config/environment.rb +35 -0
  9. data/controllers/file_filter_controller.rb +101 -0
  10. data/controllers/file_popup_menu_controller.rb +40 -0
  11. data/controllers/vim_controller.rb +28 -0
  12. data/controllers/vim_mate_controller.rb +76 -0
  13. data/images/file.png +0 -0
  14. data/images/file_green.png +0 -0
  15. data/images/file_orange.png +0 -0
  16. data/images/file_red.png +0 -0
  17. data/images/folder.png +0 -0
  18. data/images/folder_green.png +0 -0
  19. data/images/folder_orange.png +0 -0
  20. data/images/folder_red.png +0 -0
  21. data/images/processing.png +0 -0
  22. data/images/svn_added.png +0 -0
  23. data/images/svn_conflict.png +0 -0
  24. data/images/svn_deleted.png +0 -0
  25. data/images/svn_locked.png +0 -0
  26. data/images/svn_modified.png +0 -0
  27. data/images/svn_normal.png +0 -0
  28. data/images/svn_readonly.png +0 -0
  29. data/images/vimmate16.png +0 -0
  30. data/images/vimmate32.png +0 -0
  31. data/images/vimmate48.png +0 -0
  32. data/lib/active_window/active_column.rb +218 -0
  33. data/lib/active_window/active_tree_store/columns.rb +88 -0
  34. data/lib/active_window/active_tree_store/extentions.rb +81 -0
  35. data/lib/active_window/active_tree_store/index.rb +53 -0
  36. data/lib/active_window/active_tree_store.rb +26 -0
  37. data/lib/active_window/application.rb +137 -0
  38. data/lib/active_window/controller.rb +58 -0
  39. data/lib/active_window/dot_file.rb +29 -0
  40. data/lib/active_window/filtered_active_tree_store.rb +113 -0
  41. data/lib/active_window/listed_item.rb +127 -0
  42. data/lib/active_window/signal.rb +46 -0
  43. data/lib/active_window.rb +8 -0
  44. data/lib/config_window.rb +90 -0
  45. data/lib/file_tree_store.rb +74 -0
  46. data/lib/filtered_file_tree_store.rb +34 -0
  47. data/lib/gtk_thread_helper.rb +73 -0
  48. data/lib/listed_directory.rb +45 -0
  49. data/lib/listed_file.rb +67 -0
  50. data/lib/try.rb +9 -0
  51. data/lib/vim/buffers.rb +18 -0
  52. data/lib/vim/integration.rb +38 -0
  53. data/lib/vim/netbeans.rb +154 -0
  54. data/lib/vim/source.vim +18 -0
  55. data/lib/vim_mate/config.rb +132 -0
  56. data/lib/vim_mate/dummy_window.rb +14 -0
  57. data/lib/vim_mate/files_menu.rb +110 -0
  58. data/lib/vim_mate/icons.rb +156 -0
  59. data/lib/vim_mate/nice_singleton.rb +53 -0
  60. data/lib/vim_mate/plugins/inotify/init.rb +4 -0
  61. data/lib/vim_mate/plugins/inotify/lib/INotify.rb +208 -0
  62. data/lib/vim_mate/plugins/inotify/lib/directory.rb +58 -0
  63. data/lib/vim_mate/plugins/subversion/init.rb +7 -0
  64. data/lib/vim_mate/plugins/subversion/lib/file.rb +59 -0
  65. data/lib/vim_mate/plugins/subversion/lib/menu.rb +96 -0
  66. data/lib/vim_mate/plugins/subversion/lib/subversion.rb +157 -0
  67. data/lib/vim_mate/plugins.rb +6 -0
  68. data/lib/vim_mate/requirer.rb +68 -0
  69. data/lib/vim_mate/search_window.rb +227 -0
  70. data/lib/vim_mate/tags_window.rb +167 -0
  71. data/lib/vim_mate/terminals_window.rb +163 -0
  72. data/lib/vim_mate/version.rb +29 -0
  73. data/lib/vim_mate/vim_widget.rb +143 -0
  74. data/spec/active_window/active_column_spec.rb +41 -0
  75. data/spec/active_window/active_tree_store_spec.rb +312 -0
  76. data/spec/active_window/controller_spec.rb +6 -0
  77. data/spec/lib/file_tree_store_spec.rb +40 -0
  78. data/spec/lib/listed_directory_spec.rb +26 -0
  79. data/spec/lib/listed_file_spec.rb +53 -0
  80. data/spec/nice_singleton_spec.rb +23 -0
  81. data/spec/spec.opts +6 -0
  82. data/spec/spec_helper.rb +10 -0
  83. data/views/vim_mate.glade +500 -0
  84. data/vimmate.gemspec +138 -0
  85. metadata +146 -0
@@ -0,0 +1,34 @@
1
+ class FilteredFileTreeStore < ActiveWindow::FilteredActiveTreeStore
2
+ # Fuzzy search by String
3
+ # 'foo' => matches la/lu/foo, f/lala/o/gaga/o
4
+ # 'foo/bar' => matches la/afoo/gnarz/barz, but not the above
5
+ def iter_visible?(iter)
6
+ case iter[OBJECT]
7
+ when ListedDirectory; false
8
+ when ListedFile
9
+ iter[filter_column] =~ filter_regexp
10
+ else
11
+ false
12
+ end
13
+ end
14
+
15
+ def filter=(new_filter_string)
16
+ @filter_regexp = nil
17
+ @filter_column = nil
18
+ super
19
+ end
20
+
21
+
22
+ def filter_regexp
23
+ @filter_regexp ||= Regexp.new(
24
+ filter_string.split('/').map { |t|
25
+ Regexp.escape(t).split(//).join('.*')
26
+ }.join('.*/.*')
27
+ )
28
+ end
29
+
30
+ def filter_column
31
+ @filter_column ||= filter_string.index('/') ? FULL_PATH : NAME
32
+ end
33
+
34
+ end
@@ -0,0 +1,73 @@
1
+ # From http://ruby-gnome2.sourceforge.jp/hiki.cgi?tips_threads
2
+ #
3
+ # This is a crucial "tip," and in fact qualifies, I believe, as a major warning
4
+ # or fix for using Ruby with the Gtk library.
5
+ #
6
+ # The warning is this: If you call Gtk methods from outside of Gtk's main thread,
7
+ # there is a chance that the Ruby interpreter (not merely your Ruby application!)
8
+ # will crash with a segmentation fault (seg fault) error, seemingly at random.
9
+ # This is not an exception you can catch. It is fatal and crippling, and very
10
+ # difficult to debug.
11
+ #
12
+ # You might be using Ruby threads even without knowing! For example, if you
13
+ # receive events or callbacks to your Ruby methods or blocks from another library
14
+ # or service, these calls may very well be occurring on other threads. If that's
15
+ # the case, and you call Gtk from within your callbacks, you risk a seg fault.
16
+ #
17
+ # Otherwise you might have your own worker thread in the background doing some
18
+ # work, which occasionally needs to call Gtk to update a widget.
19
+ #
20
+ # (Side note: If you are using Ruby threads, be sure to protect your
21
+ # thread-shared resources with either a Monitor or a Mutex! The difference:
22
+ # monitors are safer, in that they are re-entrable from the same thread;
23
+ # mutexes aren't, but perform better.)
24
+ #
25
+ # Usage is very simple:
26
+
27
+ # Start your Gtk application by calling Gtk.main_with_queue rather than
28
+ # Gtk.main. The "timeout" argument is in milliseconds, and it is the maximum
29
+ # time that can pass until queued blocks get called: 100 should be fine.
30
+ #
31
+ # Whenever you need to queue a call, use Gtk.queue. For example:
32
+ # def my_event_callback
33
+ # Gtk.queue do
34
+ # @image.pixbuf = Gdk::Pixbuf.new @image_path, width, height
35
+ # end
36
+ # end
37
+ #
38
+ # Issues:
39
+ # 1. Keep your queued blocks lean. Do all your CPU-intensive work outside of
40
+ # the queued block. This will help keep Gtk responsive.
41
+
42
+ require 'monitor'
43
+ module Gtk
44
+ GTK_PENDING_BLOCKS = []
45
+ GTK_PENDING_BLOCKS_LOCK = ::Monitor.new
46
+
47
+ GTK_MAX_PENDING_BLOCKS_PER_ITERATION = 42
48
+
49
+ def Gtk.queue &block
50
+ if Thread.current == Thread.main
51
+ block.call
52
+ else
53
+ GTK_PENDING_BLOCKS_LOCK.synchronize do
54
+ GTK_PENDING_BLOCKS << block
55
+ end
56
+ end
57
+ end
58
+
59
+ def Gtk.main_with_queue timeout = 100
60
+ Gtk.timeout_add timeout do
61
+ GTK_PENDING_BLOCKS_LOCK.synchronize do
62
+ length = GTK_PENDING_BLOCKS.length
63
+ if length > GTK_MAX_PENDING_BLOCKS_PER_ITERATION
64
+ GTK_PENDING_BLOCKS.shift(GTK_MAX_PENDING_BLOCKS_PER_ITERATION).each(&:call)
65
+ elsif length > 0
66
+ GTK_PENDING_BLOCKS.each(&:call).clear
67
+ end
68
+ end
69
+ true
70
+ end
71
+ Gtk.main
72
+ end
73
+ end
@@ -0,0 +1,45 @@
1
+ class ListedDirectory < ListedFile
2
+ def sort
3
+ "1-#{name}-1"
4
+ end
5
+ def icon_name
6
+ 'folder'
7
+ end
8
+ def file?
9
+ false # yeah..!
10
+ end
11
+ def directory?
12
+ true
13
+ end
14
+ def exists?
15
+ full_path && ::File.directory?(full_path)
16
+ end
17
+ def refresh
18
+ super
19
+ #remove_not_existing_files
20
+ add_new_files
21
+ end
22
+
23
+ def children_paths
24
+ children_names.map {|n| File.join(full_path, n)}
25
+ rescue Errno::ENOENT, Errno::EACCES
26
+ []
27
+ end
28
+
29
+ def children_names
30
+ Dir.entries(full_path).select {|p| p !~ /^\./ }
31
+ end
32
+
33
+
34
+ # Find files to add
35
+ def add_new_files
36
+ begin
37
+ children_paths.each do |file_path|
38
+ tree << file_path
39
+ end
40
+ rescue Errno::ENOENT, Errno::EACCES
41
+ end
42
+ @traversed = true
43
+ end
44
+
45
+ end
@@ -0,0 +1,67 @@
1
+ class ListedFile < ActiveWindow::ListedItem
2
+ attr_accessor :full_path, :name, :status
3
+
4
+ def self.create(opts = {})
5
+ if fp = opts[:full_path]
6
+ if File.directory?(fp)
7
+ ListedDirectory.new opts
8
+ elsif File.file?(fp)
9
+ ListedFile.new opts
10
+ else
11
+ raise ArgumentError, "does not exist: #{fp}"
12
+ end
13
+ else
14
+ raise ArgumentError, "please give a :full_path, not only #{opts.inspect}"
15
+ end
16
+ end
17
+
18
+ def initialize(opts = {})
19
+ super
20
+ if fp = opts[:full_path]
21
+ self.full_path = fp
22
+ self.status = "normal" if VimMate::Config[:files_show_status]
23
+ end
24
+ end
25
+ def icon_name
26
+ 'file'
27
+ end
28
+ def refresh
29
+ Gtk.queue do
30
+ self.icon = VimMate::Icons.by_name icon_name
31
+ self.status = "normal" if VimMate::Config[:files_show_status]
32
+ end
33
+ end
34
+
35
+ # sets #name AND #fullpath
36
+ def full_path=(new_full_path)
37
+ unless new_full_path.empty?
38
+ @full_path = File.expand_path new_full_path
39
+ self.name = File.basename new_full_path
40
+ end
41
+ end
42
+ def sort
43
+ "2-#{name}-1"
44
+ end
45
+ def file?
46
+ true
47
+ end
48
+ def directory?
49
+ false
50
+ end
51
+ def exists?
52
+ full_path && ::File.file?(full_path)
53
+ end
54
+ def file_or_directory?
55
+ file? || directory?
56
+ end
57
+
58
+ def show!
59
+ i = iter
60
+ while i = i.parent
61
+ i[VISIBLE] = true
62
+ end
63
+ super
64
+ end
65
+
66
+ end
67
+
data/lib/try.rb ADDED
@@ -0,0 +1,9 @@
1
+ class Object
2
+ ##
3
+ # @person.name unless @person.nil?
4
+ # vs
5
+ # @person.try(:name)
6
+ def try(method)
7
+ self.send(method) unless self.nil?
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module Vim
2
+ module Buffers
3
+
4
+ def buffers
5
+ @buffers ||= [ nil ]
6
+ end
7
+ def new_buffer(path=nil)
8
+ buffers << path
9
+ buffers.length - 1
10
+ end
11
+
12
+ def last_buffer
13
+ @buffers.length - 1
14
+ end
15
+
16
+ end
17
+ end
18
+
@@ -0,0 +1,38 @@
1
+ require 'socket'
2
+
3
+ module Vim
4
+ module Integration
5
+
6
+ Executable = 'gvim'
7
+ Password = 'donthasslethehoff'
8
+
9
+ include Buffers
10
+ include Netbeans
11
+
12
+ private
13
+ def listen
14
+ Thread.new do
15
+ while true
16
+ send_function(0, 'getCursor')
17
+ sleep 1
18
+ end
19
+ end
20
+ Thread.new do
21
+ while true
22
+ if data = vim.gets
23
+ interpret_message(data)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def exec_gvim(cmd)
30
+ command = %Q[#{Executable} --servername #{@vim_server_name} #{cmd}]
31
+ system(command)
32
+ end
33
+
34
+ def remote_send(command)
35
+ exec_gvim %Q~--remote-send '#{command.gsub(%q~'~,%q~\\'~)}'~
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,154 @@
1
+ module Vim
2
+ module Netbeans
3
+
4
+ SERVER_MUTEX = Mutex.new
5
+
6
+ class Message
7
+ attr_reader :message
8
+ def initialize(message)
9
+ @message = message
10
+ end
11
+
12
+ def to_s
13
+ @message
14
+ end
15
+ end
16
+ class Event < Message
17
+ attr_reader :buffer, :name, :value, :rest
18
+ def initialize(message)
19
+ super
20
+ if match = message.match(/^(\d+):(\w+)=(\d)\s*/)
21
+ @buffer = match[1].to_i
22
+ @name = match[2]
23
+ @value = match[3]
24
+ @rest = match.post_match.split(/\s/)
25
+ @rest = @rest.map do |arg|
26
+ case arg
27
+ when /^"(.*)"$/
28
+ $1
29
+ when 'T'
30
+ true
31
+ when 'F'
32
+ false
33
+ when /^\d+$/
34
+ arg.to_i
35
+ else
36
+ arg
37
+ end
38
+ end
39
+ end
40
+ end
41
+ def to_s
42
+ %Q~#{@buffer}:#{@name}=#{@value} #{@rest.inspect}~
43
+ end
44
+ end
45
+ class Reply < Message
46
+ attr_reader :seqno, :value
47
+ def initialize(message)
48
+ super
49
+ @seqno, @value = message.split(/\s/)
50
+ @seqno = @seqno.to_i
51
+ end
52
+ end
53
+
54
+ def replies
55
+ @replies ||= {}
56
+ end
57
+
58
+ def remember_reply(reply)
59
+ replies[reply.seqno] = reply
60
+ end
61
+
62
+ def interpret_message(mess)
63
+ if mess.index ':'
64
+ event = Event.new(mess)
65
+ case event.name
66
+ when 'fileOpened'
67
+ path = event.rest.first
68
+ if event.buffer == 0
69
+ send_command(new_buffer(path), 'putBufferNumber', path)
70
+ send_command(last_buffer, 'startDocumentListen')
71
+ else
72
+ @buffers[event.buffer] = path
73
+ send_command(event.buffer, 'startDocumentListen')
74
+ end
75
+ else
76
+ mess
77
+ end
78
+ event
79
+ else
80
+ reply = Reply.new(mess)
81
+ remember_reply reply
82
+ reply
83
+ end
84
+ end
85
+
86
+ attr_reader :port
87
+ def server
88
+ if @server
89
+ @server
90
+ else
91
+ @server = TCPServer.open('localhost', 0)
92
+ @port = @server.addr[1]
93
+ @server
94
+ end
95
+ end
96
+
97
+ def vim
98
+ begin
99
+ @vim_session ||= server.accept_nonblock
100
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
101
+ IO.select([server])
102
+ retry
103
+ end
104
+ return @vim_session
105
+ end
106
+
107
+ attr_reader :seqno
108
+ def new_seqno
109
+ if @seqno
110
+ @seqno += 1
111
+ else
112
+ @seqno = 1
113
+ end
114
+ end
115
+ def send_command(buf,name,*args)
116
+ command = %Q~#{buf}:#{name}!#{new_seqno}~
117
+ unless args.empty?
118
+ command << ' ' + args.map do |arg|
119
+ case arg
120
+ when String
121
+ %Q~"#{arg.gsub('"','\\"')}"~
122
+ when Array # lnum/col
123
+ arg.length == 2 ? arg.join('/') : arg.join('-')
124
+ when true
125
+ 'T'
126
+ when false
127
+ 'F'
128
+ else
129
+ arg
130
+ end
131
+ end.join(' ')
132
+ end
133
+ send_message command
134
+ end
135
+
136
+ # TODO timeout
137
+ def send_function(buf,name)
138
+ seq = new_seqno
139
+ send_message %Q~#{buf}:#{name}/#{seq}~
140
+ while not reply = replies[seq]
141
+ sleep 0.005
142
+ end
143
+ replies.delete(seq)
144
+ reply
145
+ end
146
+
147
+ def send_message(message)
148
+ SERVER_MUTEX.synchronize do
149
+ vim.puts message
150
+ end
151
+ end
152
+
153
+ end
154
+ end
@@ -0,0 +1,18 @@
1
+ function! GetOpenBufferPaths()
2
+ let tablist = []
3
+ let pathlist = []
4
+ let cwd = getcwd()
5
+ for i in range(tabpagenr('$'))
6
+ call extend(tablist, tabpagebuflist(i + 1))
7
+ endfor
8
+ for i in tablist
9
+ let bufn = bufname(i)
10
+ if (bufn =~ '^/')==0
11
+ call add(pathlist, cwd . '/' . bufn)
12
+ else
13
+ call add(pathlist, bufn)
14
+ end
15
+ endfor
16
+ call filter(pathlist, "v:val != cwd.'/'" )
17
+ return pathlist
18
+ endfunction
@@ -0,0 +1,132 @@
1
+ =begin
2
+ = VimMate: Vim graphical add-on
3
+ Copyright (c) 2006 Guillaume Benny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+ =end
23
+
24
+ require 'yaml'
25
+
26
+ module VimMate
27
+
28
+ # Holds the configurations for VimMate. Also read and write this
29
+ # configuration so the user can change it.
30
+ class Config
31
+ include NiceSingleton
32
+
33
+ BASE_FILENAME = '.vimmaterc'
34
+ DEFAULT_CONFIG = {
35
+ :window_title => 'VimMate',
36
+ :window_width => 950,
37
+ :window_height => 600,
38
+ :layout_big_terminals => false,
39
+ :files_opened_width => 250,
40
+ :files_closed_width => 25,
41
+ :files_expanded => true,
42
+ :file_headers_visible => false,
43
+ :file_hover_selection => false,
44
+ :file_directory_separator => true,
45
+ :files_filter_active => true,
46
+ :files_auto_expand_on_filter => false,
47
+ :files_refresh_interval => 10,
48
+ :files_default_open_in_tabs => true,
49
+ :files_use_ellipsis => true,
50
+ :files_use_search => true,
51
+ :files_search_ignore_case => true,
52
+ :files_search_separator_position => 400,
53
+ :files_warn_too_many_files => 300,
54
+ :files_warn_too_many_files_each_step => true,
55
+ :files_show_status => true,
56
+ :tags_refresh_interval => 5,
57
+ :terminals_enabled => true,
58
+ :terminals_height => 50,
59
+ :terminals_font => "10",
60
+ :terminals_foreground_color => "#000000",
61
+ :terminals_background_color => "#FFFFDD",
62
+ :terminals_audible_bell => false,
63
+ :terminals_visible_bell => false,
64
+ :terminals_autoexec => "",
65
+ :terminals_login_shell => false,
66
+ :subversion_enabled => true,
67
+ }.freeze
68
+
69
+ # Create the Config class. Cannot be called directly
70
+ def initialize
71
+ # Set the full path to the configuration file. In the user's
72
+ # HOME or the current directory
73
+ if ENV['HOME']
74
+ self.class.const_set(:FILENAME, File.join(ENV['HOME'], BASE_FILENAME))
75
+ else
76
+ self.class.const_set(:FILENAME, BASE_FILENAME)
77
+ end
78
+ @config = DEFAULT_CONFIG.dup
79
+ end
80
+
81
+ # Access the configuration hash
82
+ def config
83
+ read_config
84
+ #@config.freeze
85
+ # Once read, we only need a simple reader
86
+ self.class.send(:attr_reader, :config)
87
+ config
88
+ end
89
+
90
+ # Easy access to the configuration hash
91
+ def [](symbol)
92
+ config[symbol.to_sym]
93
+ end
94
+
95
+ # Get the lib path
96
+ def lib_path
97
+ File.dirname(File.expand_path(__FILE__))
98
+ end
99
+
100
+ def images_path
101
+ File.expand_path(File.dirname(__FILE__) + '/../../images')
102
+ end
103
+
104
+ private
105
+
106
+ # Read the configuration file
107
+ def read_config
108
+ # Write the default if it doesn't exist
109
+ unless File.exist? FILENAME
110
+ write_config
111
+ return
112
+ end
113
+ # Read the configuration file and merge it with the default
114
+ # so if the user doesn't specify an option, it's set to the default
115
+ @config.merge!(YAML.load_file(FILENAME))
116
+ write_config
117
+ rescue StandardError => e
118
+ $stderr.puts e.to_s
119
+ $stderr.puts "Problem reading config file #{FILENAME}, using default"
120
+ end
121
+
122
+ # Write the configuration file
123
+ def write_config
124
+ File.open(FILENAME, 'w') do |file|
125
+ YAML.dump(@config, file)
126
+ end
127
+ rescue StandardError => e
128
+ $stderr.puts e.to_s
129
+ $stderr.puts "Problem writing config file #{FILENAME}"
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,14 @@
1
+ module VimMate
2
+
3
+ # Represents a dummy window used when a feature is missing
4
+ class DummyWindow
5
+
6
+ attr_reader :gtk_window
7
+
8
+ # Create a DummyWindow
9
+ def initialize
10
+ @gtk_window = Gtk::EventBox.new
11
+ end
12
+ end
13
+ end
14
+