*** empty log message ***
[gnus] / lisp / nnvirtual.el
index 9e900eb..41964dc 100644 (file)
@@ -1,7 +1,8 @@
-;;;; nnvirtual.el --- virtual newsgroups access for (ding) Gnus
-;; Copyright (C) 1994,95 Free Software Foundation, Inc.
+;;; nnvirtual.el --- virtual newsgroups access for Gnus
+;; Copyright (C) 1994,95,96,97,98 Free Software Foundation, Inc.
 
 
-;; Author: Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
+;; Author: David Moore <dmoore@ucsd.edu>
+;;     Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
 ;;     Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
 ;; Keywords: news
 
 ;;     Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
 ;; Keywords: news
 
 ;; GNU General Public License for more details.
 
 ;; You should have received a copy of the GNU General Public License
 ;; GNU General Public License for more details.
 
 ;; You should have received a copy of the GNU General Public License
-;; along with GNU Emacs; see the file COPYING.  If not, write to
-;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+;; along with GNU Emacs; see the file COPYING.  If not, write to the
+;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+;; Boston, MA 02111-1307, USA.
 
 ;;; Commentary:
 
 ;; The other access methods (nntp, nnspool, etc) are general news
 
 ;;; Commentary:
 
 ;; The other access methods (nntp, nnspool, etc) are general news
-;; access methods. This module relies on Gnus and can not be used
+;; access methods.  This module relies on Gnus and can not be used
 ;; separately.
 
 ;;; Code:
 ;; separately.
 
 ;;; Code:
 (require 'nntp)
 (require 'nnheader)
 (require 'gnus)
 (require 'nntp)
 (require 'nnheader)
 (require 'gnus)
+(require 'nnoo)
+(require 'gnus-util)
+(require 'gnus-start)
+(require 'gnus-sum)
+(require 'gnus-msg)
+(require 'cl)
+
+(nnoo-declare nnvirtual)
+
+(defvoo nnvirtual-always-rescan nil
+  "*If non-nil, always scan groups for unread articles when entering a group.
+If this variable is nil (which is the default) and you read articles
+in a component group after the virtual group has been activated, the
+read articles from the component group will show up when you enter the
+virtual group.")
+
+(defvoo nnvirtual-component-regexp nil
+  "*Regexp to match component groups.")
+
+(defvoo nnvirtual-component-groups nil
+  "Component group in this nnvirtual group.")
 
 \f
 
 
 \f
 
-(defconst nnvirtual-version "nnvirtual 0.0"
-  "Version number of this version of nnvirtual.")
+(defconst nnvirtual-version "nnvirtual 1.1")
+
+(defvoo nnvirtual-current-group nil)
+
+(defvoo nnvirtual-mapping-table nil
+  "Table of rules on how to map between component group and article number
+to virtual article number.")
+
+(defvoo nnvirtual-mapping-offsets nil
+  "Table indexed by component group to an offset to be applied to article numbers in that group.")
+
+(defvoo nnvirtual-mapping-len 0
+  "Number of articles in this virtual group.")
 
 
-(defvar nnvirtual-group-alist nil)
-(defvar nnvirtual-current-group nil)
-(defvar nnvirtual-current-groups nil)
-(defvar nnvirtual-current-mapping nil)
+(defvoo nnvirtual-mapping-reads nil
+  "Compressed sequence of read articles on the virtual group as computed from the unread status of individual component groups.")
 
 
-(defvar nnvirtual-do-not-open nil)
+(defvoo nnvirtual-mapping-marks nil
+  "Compressed marks alist for the virtual group as computed from the marks of individual component groups.")
 
 
-(defvar nnvirtual-status-string "")
+(defvoo nnvirtual-info-installed nil
+  "T if we have already installed the group info for this group, and shouldn't blast over it again.")
+
+(defvoo nnvirtual-status-string "")
+
+(eval-and-compile
+  (autoload 'gnus-cache-articles-in-group "gnus-cache"))
 
 \f
 
 ;;; Interface functions.
 
 
 \f
 
 ;;; Interface functions.
 
-(defun nnvirtual-retrieve-headers (sequence &optional newsgroup server)
-  "Retrieve the headers for the articles in SEQUENCE."
-  (nnvirtual-possibly-change-newsgroups newsgroup server t)
-  (save-excursion
-    (set-buffer (get-buffer-create "*virtual headers*"))
-    (buffer-disable-undo (current-buffer))
-    (erase-buffer)
-    (let ((map nnvirtual-current-mapping)
-         (offset 0)
-         articles beg group active top article result prefix)
-      (while sequence
-       (while (< (car (car map)) (car sequence))
-         (setq offset (car (car map)))
-         (setq map (cdr map)))
-       (setq top (car (car map)))
-       (setq group (nth 1 (car map)))
-       (setq prefix (gnus-group-real-prefix group))
-       (setq active (nth 2 (car map)))
-       (setq articles nil)
-       (while (and sequence (<= (car sequence) top))
-         (setq articles (cons (- (+ active (car sequence)) offset) articles))
-         (setq sequence (cdr sequence)))
-       (setq articles (nreverse articles))
-       (if (and articles
-                (setq result (gnus-retrieve-headers articles group)))
-           (save-excursion
-             (set-buffer nntp-server-buffer)
-             ;; If we got HEAD headers, we convert them into NOV
-             ;; headers. This is slow, inefficient and, come to think
-             ;; of it, downright evil. So sue me. I couldn't be
-             ;; bothered to write a header parse routine that could
-             ;; parse a mixed HEAD/NOV buffer.
-             (and (eq result 'headers) (nnvirtual-convert-headers))
-             (goto-char (point-min))
-             (while (not (eobp))
-               (setq beg (point))
-               (setq article (read nntp-server-buffer))
-               (delete-region beg (point))
-               (insert (int-to-string (+ (- article active) offset)))
-               (beginning-of-line)
-               (looking-at "[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t")
-               (goto-char (match-end 0))
-               (or (search-forward 
-                    "\t" (save-excursion (end-of-line) (point)) t)
-                   (end-of-line))
-               (while (= (char-after (1- (point))) ? )
-                 (forward-char -1)
-                 (delete-char 1))
-               (if (eolp)
-                   (progn
-                     (end-of-line)
-                     (or (= (char-after (1- (point))) ?\t)
-                         (insert ?\t))
-                     (insert (format "Xref: %s %s:%d\t" (system-name) 
-                                     group article)))
-                 (if (not (string= "" prefix))
-                     (while (re-search-forward 
-                             "[^ ]+:[0-9]+"
-                             (save-excursion (end-of-line) (point)) t)
-                       (save-excursion
-                         (goto-char (match-beginning 0))
-                         (insert prefix))))
-                 (end-of-line)
-                 (or (= (char-after (1- (point))) ?\t)
-                     (insert ?\t)))
-               (forward-line 1))))
-       (goto-char (point-max))
-       (insert-buffer-substring nntp-server-buffer))
-      ;; The headers are ready for reading, so they are inserted into
-      ;; the nntp-server-buffer, which is where Gnus expects to find
-      ;; them.
-      (prog1
-         (save-excursion
-           (set-buffer nntp-server-buffer)
-           (erase-buffer)
-           (insert-buffer-substring "*virtual headers*")
-           'nov)
-       (kill-buffer (current-buffer))))))
+(nnoo-define-basics nnvirtual)
 
 
-(defun nnvirtual-open-server (newsgroups &optional something)
-  "Open a virtual newsgroup that contains NEWSGROUPS."
-  (nnheader-init-server-buffer))
 
 
-(defun nnvirtual-close-server (&rest dum)
-  "Close news server."
+(deffoo nnvirtual-retrieve-headers (articles &optional newsgroup
+                                            server fetch-old)
+  (when (nnvirtual-possibly-change-server server)
+    (save-excursion
+      (set-buffer nntp-server-buffer)
+      (erase-buffer)
+      (if (stringp (car articles))
+         'headers
+       (let ((vbuf (nnheader-set-temp-buffer
+                    (get-buffer-create " *virtual headers*")))
+             (carticles (nnvirtual-partition-sequence articles))
+             (system-name (system-name))
+             cgroup carticle article result prefix)
+         (while carticles
+           (setq cgroup (caar carticles))
+           (setq articles (cdar carticles))
+           (pop carticles)
+           (when (and articles
+                      (gnus-check-server
+                       (gnus-find-method-for-group cgroup) t)
+                      (gnus-request-group cgroup t)
+                      (setq prefix (gnus-group-real-prefix cgroup))
+                      ;; FIX FIX FIX we want to check the cache!
+                      ;; This is probably evil if people have set
+                      ;; gnus-use-cache to nil themselves, but I
+                      ;; have no way of finding the true value of it.
+                      (let ((gnus-use-cache t))
+                        (setq result (gnus-retrieve-headers
+                                      articles cgroup nil))))
+           (set-buffer nntp-server-buffer)
+           ;; If we got HEAD headers, we convert them into NOV
+           ;; headers.  This is slow, inefficient and, come to think
+           ;; of it, downright evil.  So sue me.  I couldn't be
+           ;; bothered to write a header parse routine that could
+           ;; parse a mixed HEAD/NOV buffer.
+           (when (eq result 'headers)
+             (nnvirtual-convert-headers))
+           (goto-char (point-min))
+           (while (not (eobp))
+             (delete-region (point)
+                            (progn
+                              (setq carticle (read nntp-server-buffer))
+                              (point)))
+
+             ;; We remove this article from the articles list, if
+             ;; anything is left in the articles list after going through
+             ;; the entire buffer, then those articles have been
+             ;; expired or canceled, so we appropriately update the
+             ;; component group below.  They should be coming up
+             ;; generally in order, so this shouldn't be slow.
+             (setq articles (delq carticle articles))
+
+             (setq article (nnvirtual-reverse-map-article cgroup carticle))
+             (if (null article)
+                 ;; This line has no reverse mapping, that means it
+                 ;; was an extra article reference returned by nntp.
+                 (progn
+                   (beginning-of-line)
+                   (delete-region (point) (progn (forward-line 1) (point))))
+               ;; Otherwise insert the virtual article number,
+               ;; and clean up the xrefs.
+               (princ article nntp-server-buffer)
+               (nnvirtual-update-xref-header cgroup carticle
+                                             prefix system-name)
+               (forward-line 1))
+             )
+
+           (set-buffer vbuf)
+           (goto-char (point-max))
+           (insert-buffer-substring nntp-server-buffer))
+           ;; Anything left in articles is expired or canceled.
+           ;; Could be smart and not tell it about articles already known?
+           (when articles
+             (gnus-group-make-articles-read cgroup articles))
+           )
+
+         ;; The headers are ready for reading, so they are inserted into
+         ;; the nntp-server-buffer, which is where Gnus expects to find
+         ;; them.
+         (prog1
+             (save-excursion
+               (set-buffer nntp-server-buffer)
+               (erase-buffer)
+               (insert-buffer-substring vbuf)
+               ;; FIX FIX FIX, we should be able to sort faster than
+               ;; this if needed, since each cgroup is sorted, we just
+               ;; need to merge
+               (sort-numeric-fields 1 (point-min) (point-max))
+               'nov)
+           (kill-buffer vbuf)))))))
+
+
+(defvoo nnvirtual-last-accessed-component-group nil)
+
+(deffoo nnvirtual-request-article (article &optional group server buffer)
+  (when (nnvirtual-possibly-change-server server)
+    (if (stringp article)
+       ;; This is a fetch by Message-ID.
+       (cond
+        ((not nnvirtual-last-accessed-component-group)
+         (nnheader-report
+          'nnvirtual "Don't know what server to request from"))
+        (t
+         (save-excursion
+           (when buffer
+             (set-buffer buffer))
+           (let ((method (gnus-find-method-for-group
+                          nnvirtual-last-accessed-component-group)))
+             (funcall (gnus-get-function method 'request-article)
+                      article nil (nth 1 method) buffer)))))
+      ;; This is a fetch by number.
+      (let* ((amap (nnvirtual-map-article article))
+            (cgroup (car amap)))
+       (cond
+        ((not amap)
+         (nnheader-report 'nnvirtual "No such article: %s" article))
+        ((not (gnus-check-group cgroup))
+         (nnheader-report
+          'nnvirtual "Can't open server where %s exists" cgroup))
+        ((not (gnus-request-group cgroup t))
+         (nnheader-report 'nnvirtual "Can't open component group %s" cgroup))
+        (t
+         (setq nnvirtual-last-accessed-component-group cgroup)
+         (if buffer
+             (save-excursion
+               (set-buffer buffer)
+               (gnus-request-article-this-buffer (cdr amap) cgroup))
+           (gnus-request-article (cdr amap) cgroup))))))))
+
+
+(deffoo nnvirtual-open-server (server &optional defs)
+  (unless (assq 'nnvirtual-component-regexp defs)
+    (push `(nnvirtual-component-regexp ,server)
+         defs))
+  (nnoo-change-server 'nnvirtual server defs)
+  (if nnvirtual-component-groups
+      t
+    (setq nnvirtual-mapping-table nil
+         nnvirtual-mapping-offsets nil
+         nnvirtual-mapping-len 0
+         nnvirtual-mapping-reads nil
+         nnvirtual-mapping-marks nil
+         nnvirtual-info-installed nil)
+    (when nnvirtual-component-regexp
+      ;; Go through the newsrc alist and find all component groups.
+      (let ((newsrc (cdr gnus-newsrc-alist))
+           group)
+       (while (setq group (car (pop newsrc)))
+         (when (string-match nnvirtual-component-regexp group) ; Match
+           ;; Add this group to the list of component groups.
+           (setq nnvirtual-component-groups
+                 (cons group (delete group nnvirtual-component-groups)))))))
+    (if (not nnvirtual-component-groups)
+       (nnheader-report 'nnvirtual "No component groups: %s" server)
+      t)))
+
+
+(deffoo nnvirtual-request-group (group &optional server dont-check)
+  (nnvirtual-possibly-change-server server)
+  (setq nnvirtual-component-groups
+       (delete (nnvirtual-current-group) nnvirtual-component-groups))
+  (cond
+   ((null nnvirtual-component-groups)
+    (setq nnvirtual-current-group nil)
+    (nnheader-report 'nnvirtual "No component groups in %s" group))
+   (t
+    (when (or (not dont-check)
+             nnvirtual-always-rescan)
+      (nnvirtual-create-mapping))
+    (setq nnvirtual-current-group group)
+    (nnheader-insert "211 %d 1 %d %s\n"
+                    nnvirtual-mapping-len nnvirtual-mapping-len group))))
+
+
+(deffoo nnvirtual-request-type (group &optional article)
+  (if (not article)
+      'unknown
+    (let ((mart (nnvirtual-map-article article)))
+      (when mart
+       (gnus-request-type (car mart) (cdr mart))))))
+
+(deffoo nnvirtual-request-update-mark (group article mark)
+  (let* ((nart (nnvirtual-map-article article))
+        (cgroup (car nart))
+        ;; The component group might be a virtual group.
+        (nmark (gnus-request-update-mark cgroup (cdr nart) mark)))
+    (when (and nart
+              (= mark nmark)
+              (gnus-group-auto-expirable-p cgroup))
+      (setq mark gnus-expirable-mark)))
+  mark)
+
+
+(deffoo nnvirtual-close-group (group &optional server)
+  (when (and (nnvirtual-possibly-change-server server)
+            (not (gnus-ephemeral-group-p (nnvirtual-current-group))))
+    (nnvirtual-update-read-and-marked t t))
   t)
 
   t)
 
-(defun nnvirtual-server-opened (&optional server)
-  "Return server process status, T or NIL.
-If the stream is opened, return T, otherwise return NIL."
-  (and nntp-server-buffer
-       (get-buffer nntp-server-buffer)))
-
-(defun nnvirtual-status-message (&optional server)
-  "Return server status response as string."
-  nnvirtual-status-string)
-
-(defun nnvirtual-request-article (article &optional newsgroup server buffer)
-  "Select article by message number."
-  (nnvirtual-possibly-change-newsgroups newsgroup server t)
-  (and (numberp article)
-       (let ((map nnvirtual-current-mapping)
-            (offset 0))
-        (while (< (car (car map)) article)
-          (setq offset (car (car map)))
-          (setq map (cdr map)))
-        (gnus-request-group (nth 1 (car map)) t)
-        (gnus-request-article (- (+ (nth 2 (car map)) article) offset)
-                              (nth 1 (car map)) buffer))))
-
-(defun nnvirtual-request-group (group &optional server dont-check)
-  "Make GROUP the current newsgroup."
-  (nnvirtual-possibly-change-newsgroups group server dont-check)
-  (if (not dont-check)
-      (let ((map nnvirtual-current-mapping))
-       (while (cdr map)
-         (setq map (cdr map)))
-       (save-excursion
-         (set-buffer nntp-server-buffer)
-         (erase-buffer)
-         (insert (format "211 %d 1 %d %s\n" (car (car map)) 
-                         (car (car map)) group)))))
-  t)
-    
-(defun nnvirtual-close-group (group &optional server)
-  (nnvirtual-possibly-change-newsgroups group server t)
-  (nnvirtual-update-marked)
-  (setq nnvirtual-current-group nil
-       nnvirtual-current-groups nil
-       nnvirtual-current-mapping nil)
-  (setq nnvirtual-group-alist 
-       (delq (assoc group nnvirtual-group-alist) nnvirtual-group-alist)))
-
-(defun nnvirtual-request-list (&optional server) 
-  (setq nnvirtual-status-string "nnvirtual: LIST is not implemented.")
-  nil)
-
-(defun nnvirtual-request-newgroups (date &optional server)
-  "List new groups."
-  (setq nnvirtual-status-string "NEWGROUPS is not supported.")
-  nil)
-
-(defun nnvirtual-request-list-newsgroups (&optional server)
-  (setq nnvirtual-status-string
-       "nnvirtual: LIST NEWSGROUPS is not implemented.")
-  nil)
-
-(fset 'nnvirtual-request-post 'nntp-request-post)
-
-(fset 'nnvirtual-request-post-buffer 'nntp-request-post-buffer)
+
+(deffoo nnvirtual-request-list (&optional server)
+  (nnheader-report 'nnvirtual "LIST is not implemented."))
+
+
+(deffoo nnvirtual-request-newgroups (date &optional server)
+  (nnheader-report 'nnvirtual "NEWGROUPS is not supported."))
+
+
+(deffoo nnvirtual-request-list-newsgroups (&optional server)
+  (nnheader-report 'nnvirtual "LIST NEWSGROUPS is not implemented."))
+
+
+(deffoo nnvirtual-request-update-info (group info &optional server)
+  (when (and (nnvirtual-possibly-change-server server)
+            (not nnvirtual-info-installed))
+    ;; Install the precomputed lists atomically, so the virtual group
+    ;; is not left in a half-way state in case of C-g.
+    (gnus-atomic-progn
+      (setcar (cddr info) nnvirtual-mapping-reads)
+      (if (nthcdr 3 info)
+         (setcar (nthcdr 3 info) nnvirtual-mapping-marks)
+       (when nnvirtual-mapping-marks
+         (setcdr (nthcdr 2 info) (list nnvirtual-mapping-marks))))
+      (setq nnvirtual-info-installed t))
+    t))
+
+
+(deffoo nnvirtual-catchup-group (group &optional server all)
+  (when (and (nnvirtual-possibly-change-server server)
+            (not (gnus-ephemeral-group-p (nnvirtual-current-group))))
+    ;; copy over existing marks first, in case they set