]> git.cworth.org Git - sup/blobdiff - lib/sup/thread.rb
Merge branch 'hook-local-vars' into next
[sup] / lib / sup / thread.rb
index ab15493654acddff79be066082d44445ff5a0781..2300305c0710da7dfcb55891b5967abdc27e7f8f 100644 (file)
@@ -24,6 +24,8 @@
 ## a faked root object tying them all together into one tree
 ## structure.
 
+require 'set'
+
 module Redwood
 
 class Thread
@@ -47,8 +49,8 @@ class Thread
 
   ## unused
   def dump f=$stdout
-    f.puts "=== start thread #{self} with #{@containers.length} trees ==="
-    @containers.each { |c| c.dump_recursive f }
+    f.puts "=== start thread with #{@containers.length} trees ==="
+    @containers.each { |c| c.dump_recursive f; f.puts }
     f.puts "=== end thread ==="
   end
 
@@ -101,17 +103,16 @@ class Thread
   def toggle_label label
     if has_label? label
       remove_label label
-      return false
+      false
     else
       apply_label label
-      return true
+      true
     end
   end
 
   def set_labels l; each { |m, *o| m && m.labels = l }; end
-  
   def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
-  def save index; each { |m, *o| m && m.save(index) }; end
+  def save_state index; each { |m, *o| m && m.save_state(index) }; end
 
   def direct_participants
     map { |m, *o| [m.from] + m.to if m }.flatten.compact.uniq
@@ -123,15 +124,14 @@ class Thread
 
   def size; map { |m, *o| m ? 1 : 0 }.sum; end
   def subj; argfind { |m, *o| m && m.subj }; end
-  def labels
-      map { |m, *o| m && m.labels }.flatten.compact.uniq.sort_by { |t| t.to_s }
-  end
+  def labels; inject(Set.new) { |s, (m, *o)| m ? s | m.labels : s } end
   def labels= l
-    each { |m, *o| m && m.labels = l.clone }
+    raise ArgumentError, "not a set" unless l.is_a?(Set)
+    each { |m, *o| m && m.labels = l.dup }
   end
 
   def latest_message
-    inject(nil) do |a, b| 
+    inject(nil) do |a, b|
       b = b.first
       if a.nil?
         b
@@ -146,10 +146,6 @@ class Thread
   def to_s
     "<thread containing: #{@containers.join ', '}>"
   end
-
-  def prune_dangling_container_trees!
-    @containers.delete_if { |c| c.dangling? }
-  end
 end
 
 ## recursive structure used internally to represent message trees as
@@ -166,7 +162,7 @@ class Container
     @id = id
     @message, @parent, @thread = nil, nil, nil
     @children = []
-  end      
+  end
 
   def each_with_stuff parent=nil
     yield self, 0, parent
@@ -175,16 +171,6 @@ class Container
     end
   end
 
-  def dangling?
-    if @children.any? { |c| !c.empty? }
-      false
-    elsif @children.any? { |c| !c.dangling? }
-      false
-    else
-      true
-    end
-  end
-
   def descendant_of? o
     if o == self
       true
@@ -221,7 +207,7 @@ class Container
   def subj; find_attr :subj; end
   def date; find_attr :date; end
 
-  def is_reply?; subj && Message.subject_is_reply?(subj); end
+  def is_reply?; subj && Message.subj_is_reply?(subj); end
 
   def to_s
     [ "<#{id}",
@@ -236,9 +222,9 @@ class Container
       f.print " " * indent
       f.print "+->"
     end
-    line = #"[#{useful? ? 'U' : ' '}] " +
+    line = "[#{thread.nil? ? ' ' : '*'}] " + #"[#{useful? ? 'U' : ' '}] " +
       if @message
-        "[#{thread}] #{@message.subj} " ##{@message.refs.inspect} / #{@message.replytos.inspect}"
+        message.subj ##{@message.refs.inspect} / #{@message.replytos.inspect}"
       else
         "<no message>"
       end
@@ -256,6 +242,9 @@ end
 ## one thread, even if they don't reference each other. This is
 ## helpful for crappy MUAs that don't set In-reply-to: or References:
 ## headers, but means that messages may be threaded unnecessarily.
+##
+## The following invariants are maintained: every Thread has at least one
+## Container tree, and every Container tree has at least one Message.
 class ThreadSet
   attr_reader :num_messages
   bool_reader :thread_by_subj
@@ -270,16 +259,15 @@ class ThreadSet
     @thread_by_subj = thread_by_subj
   end
 
-  def thread_for_id mid; (c = @messages[mid]) && c.root.thread end
+  def thread_for_id mid; @messages.member?(mid) && @messages[mid].root.thread end
   def contains_id? id; @messages.member?(id) && !@messages[id].empty? end
   def thread_for m; thread_for_id m.id end
   def contains? m; contains_id? m.id end
 
-  def threads; prune_empty_threads.values end
-  def size; prune_empty_threads.size end
+  def threads; @threads.values end
+  def size; @threads.size end
 
-  ## unused
-  def dump f
+  def dump f=$stdout
     @threads.each do |s, t|
       f.puts "**********************"
       f.puts "** for subject #{s} **"
@@ -291,39 +279,46 @@ class ThreadSet
   ## link two containers
   def link p, c, overwrite=false
     if p == c || p.descendant_of?(c) || c.descendant_of?(p) # would create a loop
-#      puts "*** linking parent #{p} and child #{c} would create a loop"
+      #puts "*** linking parent #{p.id} and child #{c.id} would create a loop"
       return
     end
 
-    if c.parent.nil? || overwrite
-      remove_container c
-      p.children << c
-      c.parent = p
-    end
+    #puts "in link for #{p.id} to #{c.id}, perform? #{c.parent.nil?} || #{overwrite}"
+
+    return unless c.parent.nil? || overwrite
+    remove_container c
+    p.children << c
+    c.parent = p
+
+    ## if the child was previously a top-level container, it now ain't,
+    ## so ditch our thread and kill it if necessary
+    prune_thread_of c
   end
   private :link
 
   def remove_container c
     c.parent.children.delete c if c.parent # remove from tree
-    thread = c.root.thread # find containing thread
-    if thread
-      thread.prune_dangling_container_trees!
-      c.root.thread = nil
-    end
   end
   private :remove_container
 
-  def prune_empty_threads; @threads.delete_if { |k, t| t.empty? } end
-  private :prune_empty_threads
+  def prune_thread_of c
+    return unless c.thread
+    c.thread.drop c
+    @threads.delete_if { |k, v| v == c.thread } if c.thread.empty?
+    c.thread = nil
+  end
+  private :prune_thread_of
 
-  ## remove a single message id. not used anywhere, afaik.
   def remove_id mid
-    return unless(c = @messages[mid])
+    return unless @messages.member?(mid)
+    c = @messages[mid]
     remove_container c
+    prune_thread_of c
   end
 
   def remove_thread_containing_id mid
-    c = @messages[mid] or return
+    return unless @messages.member?(mid)
+    c = @messages[mid]
     t = c.root.thread
     @threads.delete_if { |key, thread| t == thread }
   end
@@ -331,7 +326,7 @@ class ThreadSet
   ## load in (at most) num number of threads from the index
   def load_n_threads num, opts={}
     @index.each_id_by_date opts do |mid, builder|
-      break if size >= num
+      break if size >= num unless num == -1
       next if contains_id? mid
 
       m = builder.call
@@ -356,6 +351,32 @@ class ThreadSet
     t.each { |m, *o| add_message m }
   end
 
+  ## merges two threads together. both must be members of this threadset.
+  ## does its best, heuristically, to determine which is the parent.
+  def join_threads threads
+    return if threads.size < 2
+
+    containers = threads.map do |t|
+      c = @messages.member?(t.first.id) ? @messages[t.first.id] : nil
+      raise "not in threadset: #{t.first.id}" unless c && c.message
+      c
+    end
+
+    ## use subject headers heuristically
+    parent = containers.find { |c| !c.is_reply? }
+
+    ## no thread was rooted by a non-reply, so make a fake parent
+    parent ||= @messages["joining-ref-" + containers.map { |c| c.id }.join("-")]
+
+    containers.each do |c|
+      next if c == parent
+      c.message.add_ref parent.id
+      link parent, c
+    end
+
+    true
+  end
+
   def is_relevant? m
     m.refs.any? { |ref_id| @messages.member? ref_id }
   end
@@ -365,17 +386,17 @@ class ThreadSet
     el = @messages[message.id]
     return if el.message # we've seen it before
 
+    #puts "adding: #{message.id}, refs #{message.refs.inspect}"
+
     el.message = message
     oldroot = el.root
 
     ## link via references:
-    prev = nil
-    message.refs.each do |ref_id|
+    (message.refs + [el.id]).inject(nil) do |prev, ref_id|
       ref = @messages[ref_id]
       link prev, ref if prev
-      prev = ref
+      ref
     end
-    link prev, el, true if prev
 
     ## link via in-reply-to:
     message.replytos.each do |ref_id|
@@ -385,13 +406,6 @@ class ThreadSet
     end
 
     root = el.root
-
-    ## new root. need to drop old one and put this one in its place
-    if root != oldroot && oldroot.thread
-      oldroot.thread.drop oldroot
-      oldroot.thread = nil
-    end
-
     key =
       if thread_by_subj?
         Message.normalize_subj root.subj
@@ -403,14 +417,8 @@ class ThreadSet
     ## that we first added a child message with a different
     ## subject)
     if root.thread
-      unless @threads[key] == root.thread
-        if @threads[key]
-          root.thread.empty!
-          @threads[key] << root
-          root.thread = @threads[key]
-        else
-          @threads[key] = root.thread
-        end
+      if @threads.member?(key) && @threads[key] != root.thread
+        @threads.delete key
       end
     else
       thread = @threads[key]