* gnus-sum.el (gnus-summary-refer-parent-article): Extract Message-ID from
[elisp/gnus.git-] / lisp / gnus-score.el
index 43e688b..865ecdc 100644 (file)
@@ -1,5 +1,5 @@
 ;;; gnus-score.el --- scoring code for Gnus
-;; Copyright (C) 1995, 1996, 1997, 1998, 1999, 2000
+;; Copyright (C) 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003
 ;;        Free Software Foundation, Inc.
 
 ;; Author: Per Abrahamsen <amanda@iesd.auc.dk>
 (require 'gnus)
 (require 'gnus-sum)
 (require 'gnus-range)
+(require 'gnus-win)
 (require 'message)
 (require 'score-mode)
 
+(autoload 'ffap-string-at-point "ffap")
+
 (defcustom gnus-global-score-files nil
   "List of global score files and directories.
 Set this variable if you want to use people's score files.  One entry
@@ -48,7 +51,7 @@ score files in the \"/ftp.some-where:/pub/score\" directory.
 
  (setq gnus-global-score-files
        '(\"/ftp.gnus.org:/pub/larsi/ding/score/soc.motss.SCORE\"
-         \"/ftp.some-where:/pub/score\"))"
+        \"/ftp.some-where:/pub/score\"))"
   :group 'gnus-score-files
   :type '(repeat file))
 
@@ -60,10 +63,10 @@ Each element of this alist should be of the form
 If the name of a group is matched by REGEXP, the corresponding scorefiles
 will be used for that group.
 The first match found is used, subsequent matching entries are ignored (to
-use multiple matches, see gnus-score-file-multiple-match-alist).
+use multiple matches, see `gnus-score-file-multiple-match-alist').
 
 These score files are loaded in addition to any files returned by
-gnus-score-find-score-files-function (which see)."
+`gnus-score-find-score-files-function'."
   :group 'gnus-score-files
   :type '(repeat (cons regexp (repeat file))))
 
@@ -76,10 +79,10 @@ If the name of a group is matched by REGEXP, the corresponding scorefiles
 will be used for that group.
 If multiple REGEXPs match a group, the score files corresponding to each
 match will be used (for only one match to be used, see
-gnus-score-file-single-match-alist).
+`gnus-score-file-single-match-alist').
 
 These score files are loaded in addition to any files returned by
-gnus-score-find-score-files-function (which see)."
+`gnus-score-find-score-files-function'."
   :group 'gnus-score-files
   :type '(repeat (cons regexp (repeat file))))
 
@@ -102,9 +105,9 @@ files do not actually have to exist.
 
 Predefined values are:
 
-gnus-score-find-single: Only apply the group's own score file.
-gnus-score-find-hierarchical: Also apply score files from parent groups.
-gnus-score-find-bnews: Apply score files whose names matches.
+`gnus-score-find-single': Only apply the group's own score file.
+`gnus-score-find-hierarchical': Also apply score files from parent groups.
+`gnus-score-find-bnews': Apply score files whose names matches.
 
 See the documentation to these functions for more information.
 
@@ -233,6 +236,11 @@ This variable allows the same syntax as `gnus-home-score-file'."
                                             (symbol :tag "other"))
                                     (integer :tag "Score"))))))
 
+(defcustom gnus-adaptive-word-length-limit nil
+  "*Words of a length lesser than this limit will be ignored when doing adaptive scoring."
+  :group 'gnus-score-adapt
+  :type 'integer)
+
 (defcustom gnus-ignored-adaptive-words nil
   "List of words to be ignored when doing adaptive word scoring."
   :group 'gnus-score-adapt
@@ -385,7 +393,7 @@ If nil, the user will be asked for a duration."
 (defcustom gnus-score-after-write-file-function nil
   "Function called with the name of the score file just written to disk."
   :group 'gnus-score-files
-  :type 'function)
+  :type '(choice (const nil) function))
 
 (defcustom gnus-score-thread-simplify nil
   "If non-nil, subjects will simplified as in threading."
@@ -672,7 +680,7 @@ used as score."
          (and gnus-extra-headers
               (equal (nth 1 entry) "extra")
               (intern                  ; need symbol
-               (gnus-completing-read
+               (gnus-completing-read-with-default
                 (symbol-name (car gnus-extra-headers)) ; default response
                 "Score extra header:"  ; prompt
                 (mapcar (lambda (x)    ; completion list
@@ -764,13 +772,16 @@ used as score."
        (insert (format format (caar alist) (nth idx (car alist))))
        (setq alist (cdr alist))
        (setq i (1+ i))))
+    (goto-char (point-min))
     ;; display ourselves in a small window at the bottom
     (gnus-appt-select-lowest-window)
-    (split-window)
-    (pop-to-buffer "*Score Help*")
+    (if (< (/ (window-height) 2) window-min-height)
+       (switch-to-buffer "*Score Help*")
+      (split-window)
+      (pop-to-buffer "*Score Help*"))
     (let ((window-min-height 1))
       (shrink-window-if-larger-than-buffer))
-    (select-window (get-buffer-window gnus-summary-buffer t))))
+    (select-window (gnus-get-buffer-window gnus-summary-buffer t))))
 
 (defun gnus-summary-header (header &optional no-err extra)
   ;; Return HEADER for current articles, or error.
@@ -843,11 +854,11 @@ If optional argument `EXTRA' is non-nil, it's a non-standard overview header."
                       (int-to-string match)
                     match))))
 
-    (set-text-properties 0 (length match) nil match)
-
     ;; If this is an integer comparison, we transform from string to int.
-    (when (eq (nth 2 (assoc header gnus-header-index)) 'gnus-score-integer)
-      (setq match (string-to-int match)))
+    (if (eq (nth 2 (assoc header gnus-header-index)) 'gnus-score-integer)
+       (if (stringp match)
+           (setq match (string-to-int match)))
+      (set-text-properties 0 (length match) nil match))
 
     (unless (eq date 'now)
       ;; Add the score entry to the score file.
@@ -961,7 +972,6 @@ EXTRA is the possible non-standard header."
 
 ;; All score code written by Per Abrahamsen <abraham@iesd.auc.dk>.
 
-;; Added by Per Abrahamsen <amanda@iesd.auc.dk>.
 (defun gnus-score-set-mark-below (score)
   "Automatically mark articles with score below SCORE as read."
   (interactive
@@ -1128,6 +1138,22 @@ EXTRA is the possible non-standard header."
    4 (substitute-command-keys
       "\\<gnus-score-mode-map>\\[gnus-score-edit-exit] to save edits")))
 
+(defun gnus-score-edit-file-at-point ()
+  "Edit score file at point.  Useful especially after `V t'."
+  (interactive)
+  (let* ((string (ffap-string-at-point))
+        ;; FIXME: Should be the full `match element', not just string at
+        ;; point.
+        file)
+    (save-excursion
+      (end-of-line)
+      (setq file (ffap-string-at-point)))
+    (gnus-score-edit-file file)
+    (unless (string= string file)
+      (goto-char (point-min))
+      ;; Goto first match
+      (search-forward string nil t))))
+
 (defun gnus-score-load-file (file)
   ;; Load score file FILE.  Returns a list a retrieved score-alists.
   (let* ((file (expand-file-name
@@ -1178,7 +1204,7 @@ EXTRA is the possible non-standard header."
          (mark-and-expunge (car (gnus-score-get 'mark-and-expunge alist)))
          (files (gnus-score-get 'files alist))
          (exclude-files (gnus-score-get 'exclude-files alist))
-          (orphan (car (gnus-score-get 'orphan alist)))
+         (orphan (car (gnus-score-get 'orphan alist)))
          (adapt (gnus-score-get 'adapt alist))
          (thread-mark-and-expunge
           (car (gnus-score-get 'thread-mark-and-expunge alist)))
@@ -1237,7 +1263,6 @@ EXTRA is the possible non-standard header."
                   (setq gnus-newsgroup-adaptive t)
                   adapt)
                  (t
-                  ;;(setq gnus-newsgroup-adaptive gnus-use-adaptive-scoring)
                   gnus-default-adaptive-score-alist)))
       (setq gnus-thread-expunge-below
            (or thread-mark-and-expunge gnus-thread-expunge-below))
@@ -1463,7 +1488,7 @@ EXTRA is the possible non-standard header."
               (headers gnus-newsgroup-headers)
               (current-score-file gnus-current-score-file)
               entry header new)
-         (gnus-message 5 "Scoring...")
+         (gnus-message 7 "Scoring...")
          ;; Create articles, an alist of the form `(HEADER . SCORE)'.
          (while (setq header (pop headers))
            ;; WARNING: The assq makes the function O(N*S) while it could
@@ -1505,7 +1530,7 @@ EXTRA is the possible non-standard header."
                (with-current-buffer gnus-summary-buffer
                  (setq gnus-newsgroup-scored scored))))
            ;; Remove the buffer.
-           (kill-buffer (current-buffer)))
+           (gnus-kill-buffer (current-buffer)))
 
          ;; Add articles to `gnus-newsgroup-scored'.
          (while gnus-scores-articles
@@ -1524,15 +1549,15 @@ EXTRA is the possible non-standard header."
                  (gnus-score-advanced (car score) trace))
                (pop score))))
 
-         (gnus-message 5 "Scoring...done"))))))
+         (gnus-message 7 "Scoring...done"))))))
 
 (defun gnus-score-lower-thread (thread score-adjust)
-  "Lower the socre on THREAD with SCORE-ADJUST.
+  "Lower the score on THREAD with SCORE-ADJUST.
 THREAD is expected to contain a list of the form `(PARENT [CHILD1
 CHILD2 ...])' where PARENT is a header array and each CHILD is a list
-of the same form as THREAD.  The empty list `nil' is valid.  For each
+of the same form as THREAD.  The empty list nil is valid.  For each
 article in the tree, the score of the corresponding entry in
-GNUS-NEWSGROUP-SCORED is adjusted by SCORE-ADJUST."
+`gnus-newsgroup-scored' is adjusted by SCORE-ADJUST."
   (while thread
     (let ((head (car thread)))
       (if (listp head)
@@ -1550,22 +1575,20 @@ GNUS-NEWSGROUP-SCORED is adjusted by SCORE-ADJUST."
 A root is an article with no references.  An orphan is an article
 which has references, but is not connected via its references to a
 root article.  This function finds all the orphans, and adjusts their
-score in GNUS-NEWSGROUP-SCORED by SCORE."
-  (let ((threads (gnus-make-threads)))
-    ;; gnus-make-threads produces a list, where each entry is a "thread"
-    ;; as described in the gnus-score-lower-thread docs.  This function
-    ;; will be called again (after limiting has been done) if the display
-    ;; is threaded.  It would be nice to somehow save this info and use
-    ;; it later.
-    (while threads
-      (let* ((thread (car threads))
-            (id (aref (car thread) gnus-score-index)))
-       ;; If the parent of the thread is not a root, lower the score of
-       ;; it and its descendants.  Note that some roots seem to satisfy
-       ;; (eq id nil) and some (eq id "");  not sure why.
-       (if (and id (not (string= id "")))
-           (gnus-score-lower-thread thread score)))
-      (setq threads (cdr threads)))))
+score in `gnus-newsgroup-scored' by SCORE."
+  ;; gnus-make-threads produces a list, where each entry is a "thread"
+  ;; as described in the gnus-score-lower-thread docs.  This function
+  ;; will be called again (after limiting has been done) if the display
+  ;; is threaded.  It would be nice to somehow save this info and use
+  ;; it later.
+  (dolist (thread (gnus-make-threads))
+    (let ((id (aref (car thread) gnus-score-index)))
+      ;; If the parent of the thread is not a root, lower the score of
+      ;; it and its descendants.  Note that some roots seem to satisfy
+      ;; (eq id nil) and some (eq id "");  not sure why.
+      (when (and id
+                (not (string= id "")))
+       (gnus-score-lower-thread thread score)))))
 
 (defun gnus-score-integer (scores header now expire &optional trace)
   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
@@ -1694,7 +1717,7 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
               entries alist ofunc article last)
          (when articles
            (setq last (mail-header-number (caar (last articles))))
-         ;; Not all backends support partial fetching.  In that case,
+           ;; Not all backends support partial fetching.  In that case,
            ;; we just fetch the entire article.
            (unless (gnus-check-backend-function
                     (and (string-match "^gnus-" (symbol-name request-func))
@@ -1709,8 +1732,8 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
              (widen)
              (when (funcall request-func article gnus-newsgroup-name)
                (goto-char (point-min))
-           ;; If just parts of the article is to be searched, but the
-           ;; backend didn't support partial fetching, we just narrow
+               ;; If just parts of the article is to be searched, but the
+               ;; backend didn't support partial fetching, we just narrow
                ;; to the relevant parts.
                (when ofunc
                  (if (eq ofunc 'gnus-request-head)
@@ -1753,7 +1776,8 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
                        (setq found t)
                        (when trace
                          (push
-                          (cons (car-safe (rassq alist gnus-score-cache)) kill)
+                          (cons (car-safe (rassq alist gnus-score-cache))
+                                kill)
                           gnus-score-trace)))
                      ;; Update expire date
                      (unless trace
@@ -1785,7 +1809,7 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
          ;; gnus-score-index is used as a free variable.
          alike last this art entries alist articles
          new news)
-      
+
       ;; Change score file to the adaptive score file.  All entries that
       ;; this function makes will be put into this file.
       (save-excursion
@@ -1795,7 +1819,7 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
             (gnus-score-file-name
              gnus-newsgroup-name gnus-adaptive-file-suffix))))
 
-      (setq gnus-scores-articles (sort gnus-scores-articles 
+      (setq gnus-scores-articles (sort gnus-scores-articles
                                       'gnus-score-string<)
            articles gnus-scores-articles)
 
@@ -1840,7 +1864,7 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
            (goto-char (point-min))
            (if (= dmt ?e)
                (while (funcall search-func match nil t)
-                 (and (= (progn (beginning-of-line) (point))
+                 (and (= (gnus-point-at-bol)
                          (match-beginning 0))
                       (= (progn (end-of-line) (point))
                          (match-end 0))
@@ -1859,13 +1883,19 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
                (setq found (setq arts (get-text-property (point) 'articles)))
                ;; Found a match, update scores.
                (while (setq art (pop arts))
+                 (setcdr art (+ score (cdr art)))
+                 (when trace
+                   (push (cons
+                          (car-safe (rassq alist gnus-score-cache))
+                          kill)
+                         gnus-score-trace))
                  (when (setq new (gnus-score-add-followups
                                   (car art) score all-scores thread))
                    (push new news)))))
            ;; Update expire date
            (cond ((null date))         ;Permanent entry.
-                 ((and found gnus-update-score-entry-dates) 
-                                       ;Match, update date.
+                 ((and found gnus-update-score-entry-dates)
+                  ;Match, update date.
                   (gnus-score-set 'touched '(t) alist)
                   (setcar (nthcdr 2 kill) now))
                  ((and expire (< date expire)) ;Old entry, remove.
@@ -1906,8 +1936,8 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
   ;; Insert the unique article headers in the buffer.
   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
        ;; gnus-score-index is used as a free variable.
-        (simplify (and gnus-score-thread-simplify
-                       (string= "subject" header)))
+       (simplify (and gnus-score-thread-simplify
+                      (string= "subject" header)))
        alike last this art entries alist articles
        fuzzies arts words kill)
 
@@ -1932,7 +1962,7 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
       ;; with working on them as a group.  What a hassle.
       ;; Just wait 'til you see what horrors we commit against `match'...
       (if (= gnus-score-index 9)
-         (setq this (prin1-to-string this))) ; ick.
+         (setq this (gnus-prin1-to-string this))) ; ick.
 
       (if simplify
          (setq this (gnus-map-function gnus-simplify-subject-functions this)))
@@ -1971,10 +2001,10 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
               (dmt (downcase mt))
               ;; Assume user already simplified regexp and fuzzies
               (match (if (and simplify (not (memq dmt '(?f ?r))))
-                          (gnus-map-function
-                           gnus-simplify-subject-functions
-                           (nth 0 kill))
-                        (nth 0 kill)))
+                         (gnus-map-function
+                          gnus-simplify-subject-functions
+                          (nth 0 kill))
+                       (nth 0 kill)))
               (search-func
                (cond ((= dmt ?r) 're-search-forward)
                      ((or (= dmt ?e) (= dmt ?s) (= dmt ?f)) 'search-forward)
@@ -1984,7 +2014,7 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
          ;; Evil hackery to make match usable in non-standard headers.
          (when extra
            (setq match (concat "[ (](" extra " \\. \"[^)]*"
-                               match "[^(]*\")[ )]")
+                               match "[^\"]*\")[ )]")
                  search-func 're-search-forward)) ; XXX danger?!?
 
          (cond
@@ -2310,11 +2340,14 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
                      ;; Put the word and score into the hashtb.
                      (setq val (gnus-gethash (setq word (match-string 0))
                                              hashtb))
-                     (setq val (+ score (or val 0)))
-                     (if (and gnus-adaptive-word-minimum
-                              (< val gnus-adaptive-word-minimum))
-                         (setq val gnus-adaptive-word-minimum))
-                     (gnus-sethash word val hashtb))
+                     (when (or (not gnus-adaptive-word-length-limit)
+                               (> (length word)
+                                  gnus-adaptive-word-length-limit))
+                       (setq val (+ score (or val 0)))
+                       (if (and gnus-adaptive-word-minimum
+                                (< val gnus-adaptive-word-minimum))
+                           (setq val gnus-adaptive-word-minimum))
+                       (gnus-sethash word val hashtb)))
                    (erase-buffer))))
            (set-syntax-table syntab))
          ;; Make all the ignorable words ignored.
@@ -2363,6 +2396,13 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
           1 "No score rules apply to the current article (default score %d)."
           gnus-summary-default-score)
        (set-buffer "*Score Trace*")
+       ;; ToDo: Use a keymap instead?
+       (local-set-key "q"
+                      (lambda ()
+                        (interactive)
+                        (bury-buffer nil)
+                        (gnus-summary-expand-window)))
+       (local-set-key "e" 'gnus-score-edit-file-at-point)
        (setq truncate-lines t)
        (while trace
          (insert (format "%S  ->  %s\n" (cdar trace)
@@ -2495,7 +2535,7 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
 (defun gnus-summary-lower-thread (&optional score)
   "Lower score of articles in the current thread with SCORE."
   (interactive "P")
-  (gnus-summary-raise-thread (- (1- (gnus-score-delta-default score)))))
+  (gnus-summary-raise-thread (- (gnus-score-delta-default score))))
 
 ;;; Finding score files.
 
@@ -2557,7 +2597,8 @@ score in GNUS-NEWSGROUP-SCORED by SCORE."
        (push file out))))
     (or out
        ;; Return a dummy value.
-       (list "~/News/this.file.does.not.exist.SCORE"))))
+       (list (expand-file-name "this.file.does.not.exist.SCORE"
+                               gnus-kill-files-directory)))))
 
 (defun gnus-score-file-regexp ()
   "Return a regexp that match all score files."
@@ -2595,12 +2636,14 @@ GROUP using BNews sys file syntax."
              ;; too much.
              (delete-char (min (1- (point-max)) klen))
            (goto-char (point-max))
-           (search-backward (char-to-string directory-sep-char))
-           (delete-region (1+ (point)) (point-min)))
+           (if (re-search-backward gnus-directory-sep-char-regexp nil t)
+               (delete-region (1+ (point)) (point-min))
+             (gnus-message 1 "Can't find directory separator in %s"
+                           (car sfiles))))
          ;; If short file names were used, we have to translate slashes.
          (goto-char (point-min))
          (let ((regexp (concat
-                        "[/:" (if trans (char-to-string trans) "") "]")))
+                        "[/:" (if trans (char-to-string trans)) "]")))
            (while (re-search-forward regexp nil t)
              (replace-match "." t t)))
          ;; Kludge to get rid of "nntp+" problems.
@@ -2630,13 +2673,13 @@ GROUP using BNews sys file syntax."
          ;; we add this score file to the list of score files
          ;; applicable to this group.
          (when (or (and not-match
-                        (ignore-errors
+                        (ignore-errors
                           (not (string-match regexp group-trans))))
-                   (and (not not-match)
-                        (ignore-errors (string-match regexp group-trans))))
+                   (and (not not-match)
+                        (ignore-errors (string-match regexp group-trans))))
            (push (car sfiles) ofiles)))
        (setq sfiles (cdr sfiles)))
-      (kill-buffer (current-buffer))
+      (gnus-kill-buffer (current-buffer))
       ;; Slight kludge here - the last score file returned should be
       ;; the local score file, whether it exists or not.  This is so
       ;; that any score commands the user enters will go to the right
@@ -2713,7 +2756,7 @@ Destroys the current buffer."
 
 (defun gnus-score-find-alist (group)
   "Return list of score files for GROUP.
-The list is determined from the variable gnus-score-file-alist."
+The list is determined from the variable `gnus-score-file-alist'."
   (let ((alist gnus-score-file-multiple-match-alist)
        score-files)
     ;; if this group has been seen before, return the cached entry
@@ -2768,9 +2811,10 @@ The list is determined from the variable gnus-score-file-alist."
       ;; Go through all the functions for finding score files (or actual
       ;; scores) and add them to a list.
       (while funcs
-       (when (gnus-functionp (car funcs))
+       (when (functionp (car funcs))
          (setq score-files
-               (nconc score-files (nreverse (funcall (car funcs) group)))))
+               (append score-files
+                       (nreverse (funcall (car funcs) group)))))
        (setq funcs (cdr funcs)))
       (when gnus-score-use-all-scores
        ;; Add any home score files.
@@ -2835,7 +2879,7 @@ The list is determined from the variable gnus-score-file-alist."
   (let (out)
     (while files
       ;; #### /$ Unix-specific?
-      (if (string-match "/$" (car files))
+      (if (file-directory-p (car files))
          (setq out (nconc (directory-files
                            (car files) t
                            (concat (gnus-score-file-regexp) "$"))))
@@ -2870,16 +2914,17 @@ If ADAPT, return the home adaptive file instead."
             ((stringp elem)
              elem)
             ;; Function.
-            ((gnus-functionp elem)
+            ((functionp elem)
              (funcall elem group))
             ;; Regexp-file cons.
             ((consp elem)
              (when (string-match (gnus-globalify-regexp (car elem)) group)
                (replace-match (cadr elem) t nil group))))))
     (when found
+      (setq found (nnheader-translate-file-chars found))
       (if (file-name-absolute-p found)
-          found
-        (nnheader-concat gnus-kill-files-directory found)))))
+         found
+       (nnheader-concat gnus-kill-files-directory found)))))
 
 (defun gnus-hierarchial-home-score-file (group)
   "Return the score file of the top-level hierarchy of GROUP."
@@ -2946,7 +2991,7 @@ In the `new' case, the string is a safe replacement for REGEXP.
 In the `bad' case, the string is a unsafe subexpression of REGEXP,
 and we do not have a simple replacement to suggest.
 
-See `(Gnus)Scoring Tips' for examples of good regular expressions."
+See Info node `(gnus)Scoring Tips' for examples of good regular expressions."
   (let (case-fold-search)
     (and
      ;; First, try a relatively fast necessary condition.