sup 0.0.8 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sup might be problematic. Click here for more details.
- data/HACKING +6 -36
- data/History.txt +11 -0
- data/Manifest.txt +5 -0
- data/README.txt +13 -31
- data/Rakefile +3 -3
- data/bin/sup +167 -89
- data/bin/sup-add +39 -29
- data/bin/sup-config +57 -31
- data/bin/sup-sync +60 -54
- data/bin/sup-sync-back +143 -0
- data/doc/FAQ.txt +56 -19
- data/doc/Philosophy.txt +34 -33
- data/doc/TODO +76 -46
- data/doc/UserGuide.txt +142 -122
- data/lib/sup.rb +76 -36
- data/lib/sup/account.rb +27 -19
- data/lib/sup/buffer.rb +130 -44
- data/lib/sup/contact.rb +1 -1
- data/lib/sup/draft.rb +1 -2
- data/lib/sup/imap.rb +64 -19
- data/lib/sup/index.rb +95 -16
- data/lib/sup/keymap.rb +1 -1
- data/lib/sup/label.rb +31 -5
- data/lib/sup/maildir.rb +7 -5
- data/lib/sup/mbox.rb +34 -15
- data/lib/sup/mbox/loader.rb +30 -12
- data/lib/sup/mbox/ssh-loader.rb +7 -5
- data/lib/sup/message.rb +93 -44
- data/lib/sup/modes/buffer-list-mode.rb +1 -1
- data/lib/sup/modes/completion-mode.rb +55 -0
- data/lib/sup/modes/compose-mode.rb +6 -25
- data/lib/sup/modes/contact-list-mode.rb +1 -1
- data/lib/sup/modes/edit-message-mode.rb +119 -29
- data/lib/sup/modes/file-browser-mode.rb +108 -0
- data/lib/sup/modes/forward-mode.rb +3 -20
- data/lib/sup/modes/inbox-mode.rb +9 -12
- data/lib/sup/modes/label-list-mode.rb +28 -46
- data/lib/sup/modes/label-search-results-mode.rb +1 -16
- data/lib/sup/modes/line-cursor-mode.rb +44 -5
- data/lib/sup/modes/person-search-results-mode.rb +1 -16
- data/lib/sup/modes/reply-mode.rb +18 -31
- data/lib/sup/modes/resume-mode.rb +6 -6
- data/lib/sup/modes/scroll-mode.rb +6 -5
- data/lib/sup/modes/search-results-mode.rb +6 -17
- data/lib/sup/modes/thread-index-mode.rb +70 -28
- data/lib/sup/modes/thread-view-mode.rb +65 -29
- data/lib/sup/person.rb +71 -30
- data/lib/sup/poll.rb +13 -4
- data/lib/sup/rfc2047.rb +61 -0
- data/lib/sup/sent.rb +7 -5
- data/lib/sup/source.rb +12 -9
- data/lib/sup/suicide.rb +36 -0
- data/lib/sup/tagger.rb +6 -6
- data/lib/sup/textfield.rb +76 -14
- data/lib/sup/thread.rb +97 -123
- data/lib/sup/util.rb +167 -1
- metadata +30 -5
data/lib/sup/thread.rb
CHANGED
@@ -1,24 +1,28 @@
|
|
1
|
-
## Herein
|
2
|
-
## online version of the JWZ threading algorithm:
|
1
|
+
## Herein lies all the code responsible for threading messages. It's
|
2
|
+
## basically an online version of the JWZ threading algorithm:
|
3
3
|
## http://www.jwz.org/doc/threading.html
|
4
4
|
##
|
5
|
-
## I
|
6
|
-
##
|
7
|
-
##
|
8
|
-
|
9
|
-
## At the top level, we have a ThreadSet
|
10
|
-
##
|
11
|
-
##
|
12
|
-
##
|
13
|
-
##
|
14
|
-
##
|
15
|
-
##
|
16
|
-
##
|
17
|
-
##
|
18
|
-
##
|
19
|
-
##
|
20
|
-
##
|
21
|
-
##
|
5
|
+
## I didn't implement it for efficiency, but thanks to our search
|
6
|
+
## engine backend, it's typically not applied to very many messages at
|
7
|
+
## once.
|
8
|
+
##
|
9
|
+
## At the top level, we have a ThreadSet, which represents a set of
|
10
|
+
## threads, e.g. a message folder or an inbox. Each ThreadSet contains
|
11
|
+
## zero or more Threads. A Thread represents all the message related
|
12
|
+
## to a particular subject. Each Thread has one or more Containers. A
|
13
|
+
## Container is a recursive structure that holds the message tree as
|
14
|
+
## determined by the references: and in-reply-to: headers. EAch
|
15
|
+
## Container holds zero or one messages. In the case of zero messages,
|
16
|
+
## it means we've seen a reference to the message but haven't (yet)
|
17
|
+
## seen the message itself.
|
18
|
+
##
|
19
|
+
## A Thread can have multiple top-level Containers if we decide to
|
20
|
+
## group them together independent of tree structure, typically if
|
21
|
+
## (e.g. due to someone using a primitive MUA) the messages have the
|
22
|
+
## same subject but we don't have evidence from in-reply-to: or
|
23
|
+
## references: headers. In this case Thread#each can optionally yield
|
24
|
+
## a faked root object tying them all together into one tree
|
25
|
+
## structure.
|
22
26
|
|
23
27
|
module Redwood
|
24
28
|
|
@@ -38,20 +42,20 @@ class Thread
|
|
38
42
|
end
|
39
43
|
|
40
44
|
def empty?; @containers.empty?; end
|
41
|
-
|
42
|
-
def drop c
|
43
|
-
|
44
|
-
|
45
|
+
def empty!; @containers.clear; end
|
46
|
+
def drop c; @containers.delete(c) or raise "bad drop"; end
|
47
|
+
|
48
|
+
## unused
|
49
|
+
def dump f=$stdout
|
50
|
+
f.puts "=== start thread #{self} with #{@containers.length} trees ==="
|
51
|
+
@containers.each { |c| c.dump_recursive f }
|
52
|
+
f.puts "=== end thread ==="
|
45
53
|
end
|
46
54
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
52
|
-
|
53
|
-
## yields each message, its depth, and its parent. note that the
|
54
|
-
## message can be a Message object, or :fake_root, or nil.
|
55
|
+
## yields each message, its depth, and its parent. the message yield
|
56
|
+
## parameter can be a Message object, or :fake_root, or nil (no
|
57
|
+
## message found but the presence of one induced from other
|
58
|
+
## messages).
|
55
59
|
def each fake_root=false
|
56
60
|
adj = 0
|
57
61
|
root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date || 0 }
|
@@ -201,11 +205,11 @@ class Container
|
|
201
205
|
].compact.join(" ") + ">"
|
202
206
|
end
|
203
207
|
|
204
|
-
def dump_recursive indent=0, root=true, parent=nil
|
208
|
+
def dump_recursive f=$stdout, indent=0, root=true, parent=nil
|
205
209
|
raise "inconsistency" unless parent.nil? || parent.children.include?(self)
|
206
210
|
unless root
|
207
|
-
print " " * indent
|
208
|
-
print "+->"
|
211
|
+
f.print " " * indent
|
212
|
+
f.print "+->"
|
209
213
|
end
|
210
214
|
line = #"[#{useful? ? 'U' : ' '}] " +
|
211
215
|
if @message
|
@@ -214,22 +218,31 @@ class Container
|
|
214
218
|
"<no message>"
|
215
219
|
end
|
216
220
|
|
217
|
-
puts "#{id} #{line}"#[0 .. (105 - indent)]
|
221
|
+
f.puts "#{id} #{line}"#[0 .. (105 - indent)]
|
218
222
|
indent += 3
|
219
|
-
@children.each { |c| c.dump_recursive indent, false, self }
|
223
|
+
@children.each { |c| c.dump_recursive f, indent, false, self }
|
220
224
|
end
|
221
225
|
end
|
222
226
|
|
223
|
-
##
|
224
|
-
##
|
227
|
+
## A set of threads (so a forest). Builds thread structures by reading
|
228
|
+
## messages from an index.
|
229
|
+
##
|
230
|
+
## If 'thread_by_subj' is true, puts messages with the same subject in
|
231
|
+
## one thread, even if they don't reference each other. This is
|
232
|
+
## helpful for crappy MUAs that don't set In-reply-to: or References:
|
233
|
+
## headers, but means that messages may be threaded unnecessarily.
|
225
234
|
class ThreadSet
|
226
235
|
attr_reader :num_messages
|
236
|
+
bool_reader :thread_by_subj
|
227
237
|
|
228
|
-
def initialize index
|
238
|
+
def initialize index, thread_by_subj=true
|
229
239
|
@index = index
|
230
240
|
@num_messages = 0
|
231
|
-
|
232
|
-
@
|
241
|
+
## map from message ids to container objects
|
242
|
+
@messages = SavingHash.new { |id| Container.new id }
|
243
|
+
## map from subject strings or (or root message ids) to thread objects
|
244
|
+
@threads = SavingHash.new { Thread.new }
|
245
|
+
@thread_by_subj = thread_by_subj
|
233
246
|
end
|
234
247
|
|
235
248
|
def contains_id? id; @messages.member?(id) && !@messages[id].empty?; end
|
@@ -238,19 +251,20 @@ class ThreadSet
|
|
238
251
|
end
|
239
252
|
|
240
253
|
def delete_cruft
|
241
|
-
@
|
254
|
+
@threads.each { |k, v| @threads.delete(k) if v.empty? }
|
242
255
|
end
|
243
256
|
private :delete_cruft
|
244
257
|
|
245
|
-
def threads; delete_cruft; @
|
246
|
-
def size; delete_cruft; @
|
258
|
+
def threads; delete_cruft; @threads.values; end
|
259
|
+
def size; delete_cruft; @threads.size; end
|
247
260
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
puts "
|
252
|
-
puts "
|
253
|
-
|
261
|
+
## unused
|
262
|
+
def dump f
|
263
|
+
@threads.each do |s, t|
|
264
|
+
f.puts "**********************"
|
265
|
+
f.puts "** for subject #{s} **"
|
266
|
+
f.puts "**********************"
|
267
|
+
t.dump f
|
254
268
|
end
|
255
269
|
end
|
256
270
|
|
@@ -291,7 +305,7 @@ class ThreadSet
|
|
291
305
|
m = builder.call
|
292
306
|
add_message m
|
293
307
|
load_thread_for_message m, :load_killed => opts[:load_killed]
|
294
|
-
yield
|
308
|
+
yield size if block_given?
|
295
309
|
end
|
296
310
|
end
|
297
311
|
|
@@ -305,19 +319,18 @@ class ThreadSet
|
|
305
319
|
|
306
320
|
## merges in a pre-loaded thread
|
307
321
|
def add_thread t
|
308
|
-
raise "duplicate" if @
|
322
|
+
raise "duplicate" if @threads.values.member? t
|
309
323
|
t.each { |m, *o| add_message m }
|
310
324
|
end
|
311
325
|
|
312
326
|
def is_relevant? m
|
313
|
-
m.refs.any? { |ref_id| @messages
|
327
|
+
m.refs.any? { |ref_id| @messages.member? ref_id }
|
314
328
|
end
|
315
329
|
|
316
|
-
##
|
330
|
+
## the heart of the threading code
|
317
331
|
def add_message message
|
318
|
-
|
319
|
-
el
|
320
|
-
return if @messages[id].message # we've seen it before
|
332
|
+
el = @messages[message.id]
|
333
|
+
return if el.message # we've seen it before
|
321
334
|
|
322
335
|
el.message = message
|
323
336
|
oldroot = el.root
|
@@ -325,8 +338,7 @@ class ThreadSet
|
|
325
338
|
## link via references:
|
326
339
|
prev = nil
|
327
340
|
message.refs.each do |ref_id|
|
328
|
-
|
329
|
-
ref = (@messages[ref_id] ||= Container.new ref_id)
|
341
|
+
ref = @messages[ref_id]
|
330
342
|
link prev, ref if prev
|
331
343
|
prev = ref
|
332
344
|
end
|
@@ -334,81 +346,43 @@ class ThreadSet
|
|
334
346
|
|
335
347
|
## link via in-reply-to:
|
336
348
|
message.replytos.each do |ref_id|
|
337
|
-
ref =
|
349
|
+
ref = @messages[ref_id]
|
338
350
|
link ref, el, true
|
339
351
|
break # only do the first one
|
340
352
|
end
|
341
353
|
|
342
|
-
## update subject grouping
|
343
354
|
root = el.root
|
344
|
-
# puts "> have #{el}, root #{root}, oldroot #{oldroot}"
|
345
|
-
# el.dump_recursive
|
346
|
-
|
347
|
-
if root == oldroot
|
348
|
-
if oldroot.thread
|
349
|
-
## check to see if the subject is still the same (in the case
|
350
|
-
## that we first added a child message with a different
|
351
|
-
## subject)
|
352
|
-
|
353
|
-
## this code is duplicated below. sorry! TODO: refactor
|
354
|
-
s = Message.normalize_subj(root.subj)
|
355
|
-
unless @subj_thread[s] == root.thread
|
356
|
-
## Redwood::log "[1] moving thread to new subject #{root.subj}"
|
357
|
-
if @subj_thread[s]
|
358
|
-
@subj_thread[s] << root
|
359
|
-
root.thread = @subj_thread[s]
|
360
|
-
else
|
361
|
-
@subj_thread[s] = root.thread
|
362
|
-
end
|
363
|
-
end
|
364
355
|
|
356
|
+
## new root. need to drop old one and put this one in its place
|
357
|
+
if root != oldroot && oldroot.thread
|
358
|
+
oldroot.thread.drop oldroot
|
359
|
+
oldroot.thread = nil
|
360
|
+
end
|
361
|
+
|
362
|
+
key =
|
363
|
+
if thread_by_subj?
|
364
|
+
Message.normalize_subj root.subj
|
365
365
|
else
|
366
|
-
|
367
|
-
## (and the same for below)
|
368
|
-
#Redwood::log "[1] for #{root}, subject #{Message.normalize_subj(root.subj)} has #{@subj_thread[Message.normalize_subj(root.subj)] ? 'a' : 'no'} thread"
|
369
|
-
thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
|
370
|
-
#thread = (@subj_thread[root.id] ||= Thread.new)
|
371
|
-
|
372
|
-
thread << root
|
373
|
-
root.thread = thread
|
374
|
-
# Redwood::log "[1] added #{root} to #{thread}"
|
375
|
-
end
|
376
|
-
else
|
377
|
-
if oldroot.thread
|
378
|
-
## new root. need to drop old one and put this one in its place
|
379
|
-
oldroot.thread.drop oldroot
|
380
|
-
oldroot.thread = nil
|
366
|
+
root.id
|
381
367
|
end
|
382
368
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
@subj_thread[s] = root.thread
|
395
|
-
end
|
369
|
+
## check to see if the subject is still the same (in the case
|
370
|
+
## that we first added a child message with a different
|
371
|
+
## subject)
|
372
|
+
if root.thread
|
373
|
+
unless @threads[key] == root.thread
|
374
|
+
if @threads[key]
|
375
|
+
root.thread.empty!
|
376
|
+
@threads[key] << root
|
377
|
+
root.thread = @threads[key]
|
378
|
+
else
|
379
|
+
@threads[key] = root.thread
|
396
380
|
end
|
397
|
-
|
398
|
-
else
|
399
|
-
## to disable subject grouping, use the next line instead
|
400
|
-
## (and the same above)
|
401
|
-
|
402
|
-
## this code is duplicated above. sorry! TODO: refactor
|
403
|
-
# Redwood::log "[2] for #{root}, subject '#{Message.normalize_subj(root.subj)}' has #{@subj_thread[Message.normalize_subj(root.subj)] ? 'a' : 'no'} thread"
|
404
|
-
|
405
|
-
thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
|
406
|
-
#thread = (@subj_thread[root.id] ||= Thread.new)
|
407
|
-
|
408
|
-
thread << root
|
409
|
-
root.thread = thread
|
410
|
-
# Redwood::log "[2] added #{root} to #{thread}"
|
411
381
|
end
|
382
|
+
else
|
383
|
+
thread = @threads[key]
|
384
|
+
thread << root
|
385
|
+
root.thread = thread
|
412
386
|
end
|
413
387
|
|
414
388
|
## last bit
|
data/lib/sup/util.rb
CHANGED
@@ -1,3 +1,98 @@
|
|
1
|
+
require 'lockfile'
|
2
|
+
require 'mime/types'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
## time for some monkeypatching!
|
6
|
+
class Lockfile
|
7
|
+
def gen_lock_id
|
8
|
+
Hash[
|
9
|
+
'host' => "#{ Socket.gethostname }",
|
10
|
+
'pid' => "#{ Process.pid }",
|
11
|
+
'ppid' => "#{ Process.ppid }",
|
12
|
+
'time' => timestamp,
|
13
|
+
'pname' => $0,
|
14
|
+
'user' => ENV["USER"]
|
15
|
+
]
|
16
|
+
end
|
17
|
+
|
18
|
+
def dump_lock_id lock_id = @lock_id
|
19
|
+
"host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
|
20
|
+
lock_id.values_at('host','pid','ppid','time','user', 'pname')
|
21
|
+
end
|
22
|
+
|
23
|
+
def lockinfo_on_disk
|
24
|
+
h = load_lock_id IO.read(path)
|
25
|
+
h['mtime'] = File.mtime path
|
26
|
+
h
|
27
|
+
end
|
28
|
+
|
29
|
+
def touch_yourself; touch path end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Pathname
|
33
|
+
def human_size
|
34
|
+
s =
|
35
|
+
begin
|
36
|
+
size
|
37
|
+
rescue SystemCallError
|
38
|
+
return "?"
|
39
|
+
end
|
40
|
+
|
41
|
+
if s < 1024
|
42
|
+
s.to_s + "b"
|
43
|
+
elsif s < (1024 * 1024)
|
44
|
+
(s / 1024).to_s + "k"
|
45
|
+
elsif s < (1024 * 1024 * 1024)
|
46
|
+
(s / 1024 / 1024).to_s + "m"
|
47
|
+
else
|
48
|
+
(s / 1024 / 1024 / 1024).to_s + "g"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def human_time
|
53
|
+
begin
|
54
|
+
ctime.strftime("%Y-%m-%d %H:%M")
|
55
|
+
rescue SystemCallError
|
56
|
+
"?"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
## more monkeypatching!
|
62
|
+
module RMail
|
63
|
+
class EncodingUnsupportedError < StandardError; end
|
64
|
+
|
65
|
+
class Message
|
66
|
+
def add_attachment fn
|
67
|
+
bfn = File.basename fn
|
68
|
+
a = Message.new
|
69
|
+
t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
|
70
|
+
|
71
|
+
a.header.add "Content-Disposition", "attachment; filename=#{bfn}"
|
72
|
+
a.header.add "Content-Type", "#{t.content_type}; name=#{bfn}"
|
73
|
+
a.header.add "Content-Transfer-Encoding", t.encoding
|
74
|
+
a.body =
|
75
|
+
case t.encoding
|
76
|
+
when "base64"
|
77
|
+
[IO.read(fn)].pack "m"
|
78
|
+
when "quoted-printable"
|
79
|
+
[IO.read(fn)].pack "M"
|
80
|
+
else
|
81
|
+
raise EncodingUnsupportedError, t.encoding
|
82
|
+
end
|
83
|
+
|
84
|
+
add_part a
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Range
|
90
|
+
## only valid for integer ranges (unless I guess it's exclusive)
|
91
|
+
def size
|
92
|
+
last - first + (exclude_end? ? 0 : 1)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
1
96
|
class Module
|
2
97
|
def bool_reader *args
|
3
98
|
args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
|
@@ -9,7 +104,10 @@ class Module
|
|
9
104
|
end
|
10
105
|
|
11
106
|
def defer_all_other_method_calls_to obj
|
12
|
-
class_eval %{
|
107
|
+
class_eval %{
|
108
|
+
def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
|
109
|
+
def respond_to? meth; @#{obj}.respond_to?(meth); end
|
110
|
+
}
|
13
111
|
end
|
14
112
|
end
|
15
113
|
|
@@ -38,6 +136,8 @@ class Object
|
|
38
136
|
##
|
39
137
|
## i'm sure there's pithy comment i could make here about the
|
40
138
|
## superiority of lisp, but fuck lisp.
|
139
|
+
##
|
140
|
+
## addendum: apparently this is a "k combinator". whoda thunk it?
|
41
141
|
def returning x; yield x; x; end
|
42
142
|
|
43
143
|
## clone of java-style whole-method synchronization
|
@@ -174,6 +274,24 @@ module Enumerable
|
|
174
274
|
end
|
175
275
|
best
|
176
276
|
end
|
277
|
+
|
278
|
+
## returns the maximum shared prefix of an array of strings
|
279
|
+
## optinally excluding a prefix
|
280
|
+
def shared_prefix exclude=""
|
281
|
+
return "" if empty?
|
282
|
+
prefix = ""
|
283
|
+
(0 ... first.length).each do |i|
|
284
|
+
c = first[i]
|
285
|
+
break unless all? { |s| s[i] == c }
|
286
|
+
next if exclude[i] == c
|
287
|
+
prefix += c.chr
|
288
|
+
end
|
289
|
+
prefix
|
290
|
+
end
|
291
|
+
|
292
|
+
def max_of
|
293
|
+
map { |e| yield e }.max
|
294
|
+
end
|
177
295
|
end
|
178
296
|
|
179
297
|
class Array
|
@@ -185,6 +303,8 @@ class Array
|
|
185
303
|
def rest; self[1..-1]; end
|
186
304
|
|
187
305
|
def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
|
306
|
+
|
307
|
+
def last= e; self[-1] = e end
|
188
308
|
end
|
189
309
|
|
190
310
|
class Time
|
@@ -271,6 +391,13 @@ module Singleton
|
|
271
391
|
def deinstantiate!; @instance = nil; end
|
272
392
|
def method_missing meth, *a, &b
|
273
393
|
raise "no instance defined!" unless defined? @instance
|
394
|
+
|
395
|
+
## if we've been deinstantiated, just drop all calls. this is
|
396
|
+
## useful because threads that might be active during the
|
397
|
+
## cleanup process (e.g. polling) would otherwise have to
|
398
|
+
## special-case every call to a Singleton object
|
399
|
+
return nil if @instance.nil?
|
400
|
+
|
274
401
|
@instance.send meth, *a, &b
|
275
402
|
end
|
276
403
|
def i_am_the_instance o
|
@@ -301,6 +428,9 @@ class Recoverable
|
|
301
428
|
def id; __pass :id; end
|
302
429
|
def to_s; __pass :to_s; end
|
303
430
|
def to_yaml x; __pass :to_yaml, x; end
|
431
|
+
def is_a? c; @o.is_a? c; end
|
432
|
+
|
433
|
+
def respond_to? m; @o.respond_to? m end
|
304
434
|
|
305
435
|
def __pass m, *a, &b
|
306
436
|
begin
|
@@ -311,3 +441,39 @@ class Recoverable
|
|
311
441
|
end
|
312
442
|
end
|
313
443
|
end
|
444
|
+
|
445
|
+
## acts like a hash with an initialization block, but saves any
|
446
|
+
## newly-created value even upon lookup.
|
447
|
+
##
|
448
|
+
## for example:
|
449
|
+
##
|
450
|
+
## class C
|
451
|
+
## attr_accessor :val
|
452
|
+
## def initialize; @val = 0 end
|
453
|
+
## end
|
454
|
+
##
|
455
|
+
## h = Hash.new { C.new }
|
456
|
+
## h[:a].val # => 0
|
457
|
+
## h[:a].val = 1
|
458
|
+
## h[:a].val # => 0
|
459
|
+
##
|
460
|
+
## h2 = SavingHash.new { C.new }
|
461
|
+
## h2[:a].val # => 0
|
462
|
+
## h2[:a].val = 1
|
463
|
+
## h2[:a].val # => 1
|
464
|
+
##
|
465
|
+
## important note: you REALLY want to use #member? to test existence,
|
466
|
+
## because just checking h[anything] will always evaluate to true
|
467
|
+
## (except for degenerate constructor blocks that return nil or false)
|
468
|
+
class SavingHash
|
469
|
+
def initialize &b
|
470
|
+
@constructor = b
|
471
|
+
@hash = Hash.new
|
472
|
+
end
|
473
|
+
|
474
|
+
def [] k
|
475
|
+
@hash[k] ||= @constructor.call(k)
|
476
|
+
end
|
477
|
+
|
478
|
+
defer_all_other_method_calls_to :hash
|
479
|
+
end
|