forked from mark-moseley/linecache
/
linecache19.rb
executable file
·407 lines (369 loc) · 12.7 KB
/
linecache19.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# $Id$
#
# Copyright (C) 2007, 2008 Rocky Bernstein <rockyb@rubyforge.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA.
#
# Author:: Rocky Bernstein (mailto:rockyb@rubyforge.net)
#
# = linecache
# A module to read and cache lines of a Ruby program.
# == SYNOPSIS
#
# The LineCache module allows one to get any line from any file,
# caching lines of the file on first access to the file. Although the
# file may be any file, the common use is when the file is a Ruby
# script since parsing of the file is done to figure out where the
# statement boundaries are.
#
# The routines here may be is useful when a small random sets of lines
# are read from a single file, in particular in a debugger to show
# source lines.
#
#
# require 'linecache19'
# lines = LineCache::getlines('/tmp/myruby.rb')
# # The following lines have same effect as the above.
# $: << '/tmp'
# Dir.chdir('/tmp') {lines = LineCache::getlines('myruby.rb')
#
# line = LineCache::getline('/tmp/myruby.rb', 6)
# # Note lines[6] == line (if /tmp/myruby.rb has 6 lines)
#
# LineCache::clear_file_cache
# LineCache::clear_file_cache('/tmp/myruby.rb')
# LineCache::update_cache # Check for modifications of all cached files.
#
# Some parts of the interface is derived from the Python module of the
# same name.
#
# Defining SCRIPT_LINES__ causes Ruby to cache the lines of files
# it reads. The key the setting of __FILE__ at the time when Ruby does
# its read. LineCache keeps a separate copy of the lines elsewhere
# and never destroys SCRIPT_LINES__
SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__
require 'digest/sha1'
require 'set'
require 'tracelines19'
# = module LineCache
# A module to read and cache lines of a Ruby program.
module LineCache
LineCacheInfo = Struct.new(:stat, :line_numbers, :lines, :path, :sha1) unless
defined?(LineCacheInfo)
# The file cache. The key is a name as would be given by Ruby for
# __FILE__. The value is a LineCacheInfo object.
@@file_cache = {}
# Maps a string filename (a String) to a key in @@file_cache (a
# String).
#
# One important use of @@file2file_remap is mapping the a full path
# of a file into the name stored in @@file_cache or given by Ruby's
# __FILE__. Applications such as those that get input from users,
# may want canonicalize a file name before looking it up. This map
# gives a way to do that.
#
# Another related use is when a template system is used. Here we'll
# probably want to remap not only the file name but also line
# ranges. Will probably use this for that, but I'm not sure.
@@file2file_remap = {}
@@file2file_remap_lines = {}
# Clear the file cache entirely.
def clear_file_cache()
@@file_cache = {}
@@file2file_remap = {}
@@file2file_remap_lines = {}
end
module_function :clear_file_cache
# Return an array of cached file names
def cached_files()
@@file_cache.keys
end
module_function :cached_files
# Discard cache entries that are out of date. If +filename+ is +nil+
# all entries in the file cache +@@file_cache+ are checked.
# If we don't have stat information about a file, which can happen
# if the file was read from SCRIPT_LINES__ but no corresponding file
# is found, it will be kept. Return a list of invalidated filenames.
# nil is returned if a filename was given but not found cached.
def checkcache(filename=nil, use_script_lines=false)
if !filename
filenames = @@file_cache.keys()
elsif @@file_cache.member?(filename)
filenames = [filename]
else
return nil
end
result = []
for filename in filenames
next unless @@file_cache.member?(filename)
path = @@file_cache[filename].path
if File.exist?(path)
cache_info = @@file_cache[filename].stat
stat = File.stat(path)
if stat &&
(cache_info.size != stat.size or cache_info.mtime != stat.mtime)
result << filename
update_cache(filename, use_script_lines)
end
end
end
return result
end
module_function :checkcache
# Cache filename if it's not already cached.
# Return the expanded filename for it in the cache
# or nil if we can't find the file.
def cache(filename, reload_on_change=false)
if @@file_cache.member?(filename)
checkcache(filename) if reload_on_change
else
update_cache(filename, true)
end
if @@file_cache.member?(filename)
@@file_cache[filename].path
else
nil
end
end
module_function :cache
# Return true if filename is cached
def cached?(filename)
@@file_cache.member?(unmap_file(filename))
end
module_function :cached?
def cached_script?(filename)
# In 1.8.6, the SCRIPT_LINES__ filename key can be unqualified
# In 1.9.1 it's the fully qualified name
if RUBY_VERSION < "1.9"
SCRIPT_LINES__.member?(unmap_file(filename))
else
SCRIPT_LINES__.member?(File.expand_path(unmap_file(filename)))
end
end
module_function :cached_script?
def empty?(filename)
filename=unmap_file(filename)
@@file_cache[filename].lines.empty?
end
module_function :empty?
# Get line +line_number+ from file named +filename+. Return nil if
# there was a problem. If a file named filename is not found, the
# function will look for it in the $: array.
#
# Examples:
#
# lines = LineCache::getline('/tmp/myfile.rb')
# # Same as above
# $: << '/tmp'
# lines = LineCache.getlines('myfile.rb')
#
def getline(filename, line_number, reload_on_change=true)
filename = unmap_file(filename)
filename, line_number = unmap_file_line(filename, line_number)
lines = getlines(filename, reload_on_change)
if lines and (1..lines.size) === line_number
return lines[line_number-1]
else
return nil
end
end
module_function :getline
# Read lines of +filename+ and cache the results. However +filename+ was
# previously cached use the results from the cache. Return nil
# if we can't get lines
def getlines(filename, reload_on_change=false)
filename = unmap_file(filename)
checkcache(filename) if reload_on_change
if @@file_cache.member?(filename)
return @@file_cache[filename].lines
else
update_cache(filename, true)
return @@file_cache[filename].lines if @@file_cache.member?(filename)
end
end
module_function :getlines
# Return full filename path for filename
def path(filename)
filename = unmap_file(filename)
return nil unless @@file_cache.member?(filename)
@@file_cache[filename].path
end
module_function :path
def remap_file(from_file, to_file)
@@file2file_remap[to_file] = from_file
end
module_function :remap_file
def remap_file_lines(from_file, to_file, range, start)
range = (range..range) if range.is_a?(Fixnum)
to_file = from_file unless to_file
if @@file2file_remap_lines[to_file]
# FIXME: need to check for overwriting ranges: whether
# they intersect or one encompasses another.
@@file2file_remap_lines[to_file] << [from_file, range, start]
else
@@file2file_remap_lines[to_file] = [[from_file, range, start]]
end
end
module_function :remap_file_lines
# Return SHA1 of filename.
def sha1(filename)
filename = unmap_file(filename)
return nil unless @@file_cache.member?(filename)
return @@file_cache[filename].sha1.hexdigest if
@@file_cache[filename].sha1
sha1 = Digest::SHA1.new
@@file_cache[filename].lines.each do |line|
sha1 << line
end
@@file_cache[filename].sha1 = sha1
sha1.hexdigest
end
module_function :sha1
# Return the number of lines in filename
def size(filename)
filename = unmap_file(filename)
return nil unless @@file_cache.member?(filename)
@@file_cache[filename].lines.length
end
module_function :size
# Return File.stat in the cache for filename.
def stat(filename)
return nil unless @@file_cache.member?(filename)
@@file_cache[filename].stat
end
module_function :stat
# Return an Array of breakpoints in filename.
# The list will contain an entry for each distinct line event call
# so it is possible (and possibly useful) for a line number appear more
# than once.
def trace_line_numbers(filename, reload_on_change=false)
fullname = cache(filename, reload_on_change)
return nil unless fullname
e = @@file_cache[filename]
unless e.line_numbers
e.line_numbers =
TraceLineNumbers.lnums_for_str_array(e.lines)
e.line_numbers = false unless e.line_numbers
end
e.line_numbers
end
module_function :trace_line_numbers
def unmap_file(file)
@@file2file_remap[file] ? @@file2file_remap[file] : file
end
module_function :unmap_file
def unmap_file_line(file, line)
if @@file2file_remap_lines[file]
@@file2file_remap_lines[file].each do |from_file, range, start|
if range === line
from_file = from_file || file
return [from_file, start+line-range.begin]
end
end
end
return [file, line]
end
module_function :unmap_file_line
# Update a cache entry. If something's
# wrong, return nil. Return true if the cache was updated and false
# if not. If use_script_lines is true, use that as the source for the
# lines of the file
def update_cache(filename, use_script_lines=false)
return nil unless filename
@@file_cache.delete(filename)
path = File.expand_path(filename)
if use_script_lines
list = [filename]
list << @@file2file_remap[path] if @@file2file_remap[path]
list.each do |name|
if !SCRIPT_LINES__[name].nil? && SCRIPT_LINES__[name] != true
begin
stat = File.stat(name)
rescue
stat = nil
end
lines = SCRIPT_LINES__[name]
if "ruby19".respond_to?(:force_encoding)
lines.each{|l| l.force_encoding(Encoding.default_external) }
end
@@file_cache[filename] = LineCacheInfo.new(stat, nil, lines, path, nil)
@@file2file_remap[path] = filename
return true
end
end
end
if File.exist?(path)
stat = File.stat(path)
elsif File.basename(filename) == filename
# try looking through the search path.
stat = nil
for dirname in $:
path = File.join(dirname, filename)
if File.exist?(path)
stat = File.stat(path)
break
end
end
return false unless stat
end
begin
fp = File.open(path, 'r')
lines = fp.readlines()
fp.close()
rescue
## print '*** cannot open', path, ':', msg
return nil
end
@@file_cache[filename] = LineCacheInfo.new(File.stat(path), nil, lines,
path, nil)
@@file2file_remap[path] = filename
return true
end
module_function :update_cache
end
# example usage
if __FILE__ == $0
def yes_no(var)
return var ? "" : "not "
end
lines = LineCache::getlines(__FILE__)
puts "#{__FILE__} has #{LineCache.size(__FILE__)} lines"
line = LineCache::getline(__FILE__, 6)
puts "The 6th line is\n#{line}"
line = LineCache::remap_file(__FILE__, 'another_name')
puts LineCache::getline('another_name', 7)
puts("Files cached: #{LineCache::cached_files.inspect}")
LineCache::update_cache(__FILE__)
LineCache::checkcache(__FILE__)
puts "#{__FILE__} has #{LineCache::size(__FILE__)} lines"
puts "#{__FILE__} trace line numbers:\n" +
"#{LineCache::trace_line_numbers(__FILE__).to_a.sort.inspect}"
puts("#{__FILE__} is %scached." %
yes_no(LineCache::cached?(__FILE__)))
puts LineCache::stat(__FILE__).inspect
puts "Full path: #{LineCache::path(__FILE__)}"
LineCache::checkcache # Check all files in the cache
LineCache::clear_file_cache
puts("#{__FILE__} is now %scached." %
yes_no(LineCache::cached?(__FILE__)))
digest = SCRIPT_LINES__.select{|k,v| k =~ /digest.rb$/}
puts digest.first[0] if digest
line = LineCache::getline(__FILE__, 7)
puts "The 7th line is\n#{line}"
LineCache::remap_file_lines(__FILE__, 'test2', (10..20), 6)
puts LineCache::getline('test2', 10)
puts "Remapped 10th line of test2 is\n#{line}"
end