- (while (setq header (pop headers))
- (insert (int-to-string (mail-header-number header)) "\t"
- (or (mail-header-subject header) "") "\t"
- (or (mail-header-from header) "") "\t"
- (or (mail-header-date header) "") "\t"
- (or (mail-header-id header) "") "\t"
- (or (mail-header-references header) "") "\t"
- (int-to-string (or (mail-header-chars header) 0)) "\t"
- (int-to-string (or (mail-header-lines header) 0)) "\t"
- (if (mail-header-xref header)
- (concat "Xref: " (mail-header-xref header) "\t")
- "") "\n")))))
-
-(defun nnvirtual-possibly-change-group (group regexp &optional dont-check)
- (let ((inf t))
- (unless (equal group nnvirtual-current-group)
- (and (setq inf (assoc group nnvirtual-group-alist))
- regexp
- (string= (nth 3 inf) regexp)
- (progn
- (setq nnvirtual-current-group (car inf))
- (setq nnvirtual-component-groups (nth 1 inf))
- (setq nnvirtual-mapping (nth 2 inf)))))
- (when (and regexp
- (or (not inf)
- (not dont-check)))
- (and inf (setq nnvirtual-group-alist
- (delq inf nnvirtual-group-alist)))
- (setq nnvirtual-mapping nil)
- (setq nnvirtual-current-group group)
- (let ((newsrc gnus-newsrc-alist)
- (virt-group (gnus-group-prefixed-name
- nnvirtual-current-group '(nnvirtual ""))))
- (setq nnvirtual-component-groups nil)
- (while newsrc
- (and (string-match regexp (caar newsrc))
- (not (string= (caar newsrc) virt-group))
- (setq nnvirtual-component-groups
- (cons (caar newsrc) nnvirtual-component-groups)))
- (setq newsrc (cdr newsrc))))
- (if nnvirtual-component-groups
- (progn
- (nnvirtual-create-mapping)
- (setq nnvirtual-group-alist
- (cons (list group nnvirtual-component-groups
- nnvirtual-mapping regexp)
- nnvirtual-group-alist)))
- (nnheader-report 'nnvirtual "No component groups: %s" group))))
- nnvirtual-component-groups)
-
-(defun nnvirtual-update-marked ()
- "Copy marks from the virtual group to the component groups."
- (let ((mark-lists gnus-article-mark-lists)
- (uncompressed '(score bookmark))
- type list calist mart cgroups)
- (while mark-lists
- (setq type (cdar mark-lists))
- (setq list (symbol-value (intern (format "gnus-newsgroup-%s"
- (car (pop mark-lists))))))
- (setq cgroups
- (mapcar (lambda (g) (list g)) nnvirtual-component-groups))
- (while list
- (nconc (assoc (cadr (setq mart (assq (pop list) nnvirtual-mapping)))
- cgroups)
- (list (caddr mart))))
- (while cgroups
- (gnus-add-marked-articles
- (caar cgroups) type (cdar cgroups) nil t)
- (gnus-group-update-group (car (pop cgroups)) t)))))
-
-(defun nnvirtual-marks (article marks)
- "Return a list of mark types for ARTICLE."
- (let (out)
- (while marks
- (when (memq article (cdar marks))
- (push (caar marks) out))
- (setq marks (cdr marks)))
- out))
-
-(defun nnvirtual-create-mapping ()
- "Create an article mapping for the current group."
- (let* (div
- (map (sort
- (apply
- 'nconc
- (mapcar
- (lambda (g)
- (let* ((active (or (gnus-active g) (gnus-activate-group g)))
- (unreads (and active (gnus-list-of-unread-articles
- g)))
- (marks (gnus-uncompress-marks
- (gnus-info-marks (gnus-get-info g)))))
- (when active
- (when gnus-use-cache
- (push (cons 'cache (gnus-cache-articles-in-group g))
- marks))
- (when active
- (setq div (/ (float (car active))
- (if (zerop (cdr active))
- 1 (cdr active))))
- (mapcar (lambda (n)
- (list (* div (- n (car active)))
- g n (and (memq n unreads) t)
- (nnvirtual-marks n marks)))
- (gnus-uncompress-range active))))))
- nnvirtual-component-groups))
- (lambda (m1 m2)
- (< (car m1) (car m2)))))
- (i 0))
- (setq nnvirtual-mapping map)
- (while map
- (setcar (pop map) (incf i)))))
+ (mapc 'nnheader-insert-nov headers))))
+
+
+(defun nnvirtual-update-xref-header (group article prefix system-name)
+ "Edit current NOV header in current buffer to have an xref to the component group, and also server prefix any existing xref lines."
+ ;; Move to beginning of Xref field, creating a slot if needed.
+ (beginning-of-line)
+ (looking-at
+ "[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t")
+ (goto-char (match-end 0))
+ (unless (search-forward "\t" (point-at-eol) 'move)
+ (insert "\t"))
+
+ ;; Remove any spaces at the beginning of the Xref field.
+ (while (eq (char-after (1- (point))) ? )
+ (forward-char -1)
+ (delete-char 1))
+
+ (insert "Xref: " system-name " " group ":")
+ (princ article (current-buffer))
+ (insert " ")
+
+ ;; If there were existing xref lines, clean them up to have the correct
+ ;; component server prefix.
+ (save-restriction
+ (narrow-to-region (point)
+ (or (search-forward "\t" (point-at-eol) t)
+ (point-at-eol)))
+ (goto-char (point-min))
+ (when (re-search-forward "Xref: *[^\n:0-9 ]+ *" nil t)
+ (replace-match "" t t))
+ (goto-char (point-min))
+ (when (re-search-forward
+ (concat (regexp-quote (gnus-group-real-name group)) ":[0-9]+")
+ nil t)
+ (replace-match "" t t))
+ (unless (eobp)
+ (insert " ")
+ (when (not (string= "" prefix))
+ (while (re-search-forward "[^ ]+:[0-9]+" nil t)
+ (save-excursion
+ (goto-char (match-beginning 0))
+ (insert prefix))))))
+
+ ;; Ensure a trailing \t.
+ (end-of-line)
+ (or (eq (char-after (1- (point))) ?\t)
+ (insert ?\t)))
+
+
+(defun nnvirtual-possibly-change-server (server)
+ (or (not server)
+ (nnoo-current-server-p 'nnvirtual server)
+ (nnvirtual-open-server server)))
+
+
+(defun nnvirtual-update-read-and-marked (read-p update-p)
+ "Copy marks from the virtual group to the component groups.
+If READ-P is not nil, update the (un)read status of the components.
+If UPDATE-P is not nil, call gnus-group-update-group on the components."
+ (when nnvirtual-current-group
+ (let ((unreads (and read-p
+ (nnvirtual-partition-sequence
+ (gnus-list-of-unread-articles
+ (nnvirtual-current-group)))))
+ (type-marks
+ (delq nil
+ (mapcar (lambda (ml)
+ (if (eq (car ml) 'score)
+ nil
+ (cons (car ml)
+ (nnvirtual-partition-sequence (cdr ml)))))
+ (gnus-info-marks (gnus-get-info
+ (nnvirtual-current-group))))))
+ type groups info)
+
+ ;; Ok, atomically move all of the (un)read info, clear any old
+ ;; marks, and move all of the current marks. This way if someone
+ ;; hits C-g, you won't leave the component groups in a half-way state.
+ (progn
+ ;; move (un)read
+ ;; bind for workaround guns-update-read-articles
+ (let ((gnus-newsgroup-active nil))
+ (dolist (entry unreads)
+ (gnus-update-read-articles (car entry) (cdr entry))))
+
+ ;; clear all existing marks on the component groups
+ (dolist (group nnvirtual-component-groups)
+ (when (and (setq info (gnus-get-info group))
+ (gnus-info-marks info))
+ (gnus-info-set-marks
+ info
+ (if (assq 'score (gnus-info-marks info))
+ (list (assq 'score (gnus-info-marks info)))
+ nil))))
+
+ ;; Ok, currently type-marks is an assq list with keys of a mark type,
+ ;; with data of an assq list with keys of component group names
+ ;; and the articles which correspond to that key/group pair.
+ (dolist (mark type-marks)
+ (setq type (car mark))
+ (setq groups (cdr mark))
+ (dolist (carticles groups)
+ (gnus-add-marked-articles (car carticles) type (cdr carticles)
+ nil t))))
+
+ ;; possibly update the display, it is really slow
+ (when update-p
+ (dolist (group nnvirtual-component-groups)
+ (gnus-group-update-group group t))))))
+
+
+(defun nnvirtual-current-group ()
+ "Return the prefixed name of the current nnvirtual group."
+ (concat "nnvirtual:" nnvirtual-current-group))
+
+
+
+;;; This is currently O(kn^2) to merge n lists of length k.
+;;; You could do it in O(knlogn), but we have a small n, and the
+;;; overhead of the other approach is probably greater.
+(defun nnvirtual-merge-sorted-lists (&rest lists)
+ "Merge many sorted lists of numbers."
+ (if (null (cdr lists))
+ (car lists)
+ (sort (apply 'nconc lists) '<)))
+
+
+;;; We map between virtual articles and real articles in a manner
+;;; which keeps the size of the virtual active list the same as the
+;;; sum of the component active lists.
+
+;;; To achieve fair mixing of the groups, the last article in each of
+;;; N component groups will be in the last N articles in the virtual
+;;; group.
+
+;;; If you have 3 components A, B and C, with articles 1-8, 1-5, and
+;;; 6-7 respectively, then the virtual article numbers look like:
+;;;
+;;; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+;;; A1 A2 A3 A4 B1 A5 B2 A6 B3 A7 B4 C6 A8 B5 C7
+
+;;; To compute these mappings we generate a couple tables and then
+;;; do some fast operations on them. Tables for the example above:
+;;;
+;;; Offsets - [(A 0) (B -3) (C -1)]
+;;;
+;;; a b c d e
+;;; Mapping - ([ 3 0 1 3 0 ]
+;;; [ 6 3 2 9 3 ]
+;;; [ 8 6 3 15 9 ])
+;;;
+;;; (note column 'e' is different in real algorithm, which is slightly
+;;; different than described here, but this gives you the methodology.)
+;;;
+;;; The basic idea is this, when going from component->virtual, apply
+;;; the appropriate offset to the article number. Then search the first
+;;; column of the table for a row where 'a' is less than or equal to the
+;;; modified number. You can see that only group A can therefore go to
+;;; the first row, groups A and B to the second, and all to the last.
+;;; The third column of the table is telling us the number of groups
+;;; which might be able to reach that row (it might increase by more than
+;;; 1 if several groups have the same size).
+;;; Then column 'b' provides an additional offset you apply when you have
+;;; found the correct row. You then multiply by 'c' and add on the groups
+;;; _position_ in the offset table. The basic idea here is that on
+;;; any given row we are going to map back and forth using X'=X*c+Y and
+;;; X=(X'/c), Y=(X' mod c). Then once you've done this transformation,
+;;; you apply a final offset from column 'e' to give the virtual article.
+;;;
+;;; Going the other direction, you instead search on column 'd' instead
+;;; of 'a', and apply everything in reverse order.
+
+;;; Convert component -> virtual:
+;;; set num = num - Offset(group)
+;;; find first row in Mapping where num <= 'a'
+;;; num = (num-'b')*c + Position(group) + 'e'
+
+;;; Convert virtual -> component:
+;;; find first row in Mapping where num <= 'd'
+;;; num = num - 'e'
+;;; group_pos = num mod 'c'
+;;; num = (num / 'c') + 'b' + Offset(group_pos)
+
+;;; Easy no? :)
+;;;
+;;; Well actually, you need to keep column e offset smaller by the 'c'
+;;; column for that line, and always add 1 more when going from
+;;; component -> virtual. Otherwise you run into a problem with
+;;; unique reverse mapping.
+
+(defun nnvirtual-map-article (article)
+ "Return a cons of the component group and article corresponding to the given virtual ARTICLE."
+ (let ((table nnvirtual-mapping-table)
+ entry group-pos)
+ (while (and table
+ (> article (aref (car table) 3)))
+ (setq table (cdr table)))
+ (when (and table
+ (> article 0))
+ (setq entry (car table))
+ (setq article (- article (aref entry 4) 1))
+ (setq group-pos (mod article (aref entry 2)))
+ (cons (car (aref nnvirtual-mapping-offsets group-pos))
+ (+ (/ article (aref entry 2))
+ (aref entry 1)
+ (cdr (aref nnvirtual-mapping-offsets group-pos)))
+ ))
+ ))
+
+
+
+(defun nnvirtual-reverse-map-article (group article)
+ "Return the virtual article number corresponding to the given component GROUP and ARTICLE."
+ (when (numberp article)
+ (let ((table nnvirtual-mapping-table)
+ (group-pos 0)
+ entry)
+ (while (not (string= group (car (aref nnvirtual-mapping-offsets
+ group-pos))))
+ (setq group-pos (1+ group-pos)))
+ (setq article (- article (cdr (aref nnvirtual-mapping-offsets
+ group-pos))))
+ (while (and table
+ (> article (aref (car table) 0)))
+ (setq table (cdr table)))
+ (setq entry (car table))
+ (when (and entry
+ (> article 0)
+ (< group-pos (aref entry 2))) ; article not out of range below
+ (+ (aref entry 4)
+ group-pos
+ (* (- article (aref entry 1))
+ (aref entry 2))
+ 1))
+ )))
+
+
+(defsubst nnvirtual-reverse-map-sequence (group articles)
+ "Return list of virtual article numbers for all ARTICLES in GROUP.
+The ARTICLES should be sorted, and can be a compressed sequence.
+If any of the article numbers has no corresponding virtual article,
+then it is left out of the result."
+ (when (numberp (cdr-safe articles))
+ (setq articles (list articles)))
+ (let (result a i j new-a)
+ (while (setq a (pop articles))
+ (if (atom a)
+ (setq i a
+ j a)
+ (setq i (car a)
+ j (cdr a)))
+ (while (<= i j)
+ ;; If this is slow, you can optimize by moving article checking
+ ;; into here. You don't have to recompute the group-pos,
+ ;; nor scan the table every time.
+ (when (setq new-a (nnvirtual-reverse-map-article group i))
+ (push new-a result))
+ (setq i (1+ i))))
+ (nreverse result)))
+
+
+(defun nnvirtual-partition-sequence (articles)
+ "Return an association list of component article numbers.
+These are indexed by elements of nnvirtual-component-groups, based on
+the sequence ARTICLES of virtual article numbers. ARTICLES should be
+sorted, and can be a compressed sequence. If any of the article
+numbers has no corresponding component article, then it is left out of
+the result."
+ (when (numberp (cdr-safe articles))
+ (setq articles (list articles)))
+ (let ((carticles (mapcar 'list nnvirtual-component-groups))
+ a i j article entry)
+ (while (setq a (pop articles))
+ (if (atom a)
+ (setq i a
+ j a)
+ (setq i (car a)
+ j (cdr a)))
+ (while (<= i j)
+ (when (setq article (nnvirtual-map-article i))
+ (setq entry (assoc (car article) carticles))
+ (setcdr entry (cons (cdr article) (cdr entry))))
+ (setq i (1+ i))))
+ (mapc (lambda (x) (setcdr x (nreverse (cdr x))))
+ carticles)
+ carticles))
+
+
+(defun nnvirtual-create-mapping (dont-check)
+ "Build the tables necessary to map between component (group, article) to virtual article.
+Generate the set of read messages and marks for the virtual group
+based on the marks on the component groups."
+ (let ((cnt 0)
+ (tot 0)
+ (M 0)
+ (i 0)
+ actives all-unreads all-marks
+ active min max size unreads marks
+ next-M next-tot
+ reads beg)
+ ;; Ok, we loop over all component groups and collect a lot of
+ ;; information:
+ ;; Into actives we place (g size max), where size is max-min+1.
+ ;; Into all-unreads we put (g unreads).
+ ;; Into all-marks we put (g marks).
+ ;; We also increment cnt and tot here, and compute M (max of sizes).
+ (mapc (lambda (g)
+ (setq active (or (and dont-check
+ (gnus-active g))
+ (gnus-activate-group g))
+ min (car active)
+ max (cdr active))
+ (when (and active (>= max min) (not (zerop max)))
+ ;; store active information
+ (push (list g (- max min -1) max) actives)
+ ;; collect unread/mark info for later
+ (setq unreads (gnus-list-of-unread-articles g))
+ (setq marks (gnus-info-marks (gnus-get-info g)))
+ (when gnus-use-cache
+ (push (cons 'cache
+ (gnus-cache-articles-in-group g))
+ marks))
+ (push (cons g unreads) all-unreads)
+ (push (cons g marks) all-marks)
+ ;; count groups, total #articles, and max size
+ (setq size (- max min -1))
+ (setq cnt (1+ cnt)
+ tot (+ tot size)
+ M (max M size))))
+ nnvirtual-component-groups)
+
+ ;; Number of articles in the virtual group.
+ (setq nnvirtual-mapping-len tot)
+
+
+ ;; We want the actives list sorted by size, to build the tables.
+ (setq actives (sort actives (lambda (g1 g2) (< (nth 1 g1) (nth 1 g2)))))
+
+ ;; Build the offset table. Largest sized groups are at the front.
+ (setq nnvirtual-mapping-offsets
+ (vconcat
+ (nreverse
+ (mapcar (lambda (entry)
+ (cons (nth 0 entry)
+ (- (nth 2 entry) M)))
+ actives))))
+
+ ;; Build the mapping table.
+ (setq nnvirtual-mapping-table nil)
+ (setq actives (mapcar (lambda (entry) (nth 1 entry)) actives))
+ (while actives
+ (setq size (car actives))
+ (setq next-M (- M size))
+ (setq next-tot (- tot (* cnt size)))
+ ;; make current row in table
+ (push (vector M next-M cnt tot (- next-tot cnt))
+ nnvirtual-mapping-table)
+ ;; update M and tot
+ (setq M next-M)
+ (setq tot next-tot)
+ ;; subtract the current size from all entries.
+ (setq actives (mapcar (lambda (x) (- x size)) actives))
+ ;; remove anything that went to 0.
+ (while (and actives
+ (= (car actives) 0))
+ (pop actives)
+ (setq cnt (- cnt 1))))
+
+
+ ;; Now that the mapping tables are generated, we can convert
+ ;; and combine the separate component unreads and marks lists
+ ;; into single lists of virtual article numbers.
+ (setq unreads (apply 'nnvirtual-merge-sorted-lists
+ (mapcar (lambda (x)
+ (nnvirtual-reverse-map-sequence
+ (car x) (cdr x)))
+ all-unreads)))
+ (setq marks (mapcar
+ (lambda (type)
+ (cons (cdr type)
+ (gnus-compress-sequence
+ (apply
+ 'nnvirtual-merge-sorted-lists
+ (mapcar (lambda (x)
+ (nnvirtual-reverse-map-sequence
+ (car x)
+ (cdr (assq (cdr type) (cdr x)))))
+ all-marks)))))
+ gnus-article-mark-lists))
+
+ ;; Remove any empty marks lists, and store.
+ (setq nnvirtual-mapping-marks nil)
+ (dolist (mark marks)
+ (when (cdr mark)
+ (push mark nnvirtual-mapping-marks)))
+
+ ;; We need to convert the unreads to reads. We compress the
+ ;; sequence as we go, otherwise it could be huge.
+ (while (and (<= (incf i) nnvirtual-mapping-len)
+ unreads)
+ (if (= i (car unreads))
+ (setq unreads (cdr unreads))
+ ;; try to get a range.
+ (setq beg i)
+ (while (and (<= (incf i) nnvirtual-mapping-len)
+ (not (= i (car unreads)))))
+ (setq i (- i 1))
+ (if (= i beg)
+ (push i reads)
+ (push (cons beg i) reads))
+ ))
+ (when (<= i nnvirtual-mapping-len)
+ (if (= i nnvirtual-mapping-len)
+ (push i reads)
+ (push (cons i nnvirtual-mapping-len) reads)))
+
+ ;; Store the reads list for later use.
+ (setq nnvirtual-mapping-reads (nreverse reads))
+
+ ;; Throw flag to show we changed the info.
+ (setq nnvirtual-info-installed nil)
+ ))