branch: externals/bbdb commit 0c43518419c2ea47ac35e4d626272e1063c01f00 Author: Roland Winkler <wink...@gnu.org> Commit: Roland Winkler <wink...@gnu.org>
Unify the handling of new records, new fields and modified fields --- bbdb-com.el | 130 ++++++++++++++++++++-------------------- bbdb.el | 194 ++++++++++++++++++++++++++++++++++-------------------------- 2 files changed, 176 insertions(+), 148 deletions(-) diff --git a/bbdb-com.el b/bbdb-com.el index 405005f..94378f1 100644 --- a/bbdb-com.el +++ b/bbdb-com.el @@ -803,7 +803,7 @@ but does ensure that there will not be name collisions." (let (name) (bbdb-error-retry (setq name (bbdb-read-name first-and-last)) - (bbdb-check-name (car name) (cdr name))) + (bbdb-check-name name)) (bbdb-record-set-firstname record (car name)) (bbdb-record-set-lastname record (cdr name))) @@ -811,36 +811,31 @@ but does ensure that there will not be name collisions." (bbdb-record-set-organization record (bbdb-read-organization)) ;; mail - (bbdb-record-set-mail - record (bbdb-split 'mail (bbdb-read-string "E-Mail Addresses: "))) + (let (mail) + (bbdb-error-retry + (setq mail (bbdb-split 'mail (bbdb-read-string "E-Mail Addresses: "))) + (bbdb-check-mail mail)) + (bbdb-record-set-mail record mail)) + ;; address - (let (addresses label address) + (let (addresses label) (while (not (string= "" (setq label (bbdb-read-string "Snail Mail Address Label [RET when done]: " - nil - bbdb-address-label-list)))) - (setq address (make-vector bbdb-address-length nil)) - (bbdb-record-edit-address address label t) - (push address addresses)) + nil bbdb-address-label-list)))) + ;; Here we could also already update the completion lists. Bother? + (push (bbdb-record-edit-address nil label) addresses)) (bbdb-record-set-address record (nreverse addresses))) ;; phones - (let (phones phone-list label) + (let (phones label) (while (not (string= "" (setq label (bbdb-read-string - "Phone Label [RET when done]: " nil - bbdb-phone-label-list)))) - (setq phone-list - (bbdb-error-retry - (bbdb-parse-phone - (read-string "Phone: " - (and (integerp bbdb-default-area-code) - (format "(%03d) " - bbdb-default-area-code)))))) - (push (apply 'vector label phone-list) phones)) + "Phone Label [RET when done]: " + nil bbdb-phone-label-list)))) + (push (bbdb-record-edit-phone nil label) phones)) (bbdb-record-set-phone record (nreverse phones))) ;; `bbdb-default-xfield' @@ -912,10 +907,12 @@ The following keywords are supported in SPEC: and `bbdb-allow-duplicates' is nil. :affix VAL List of strings. :aka VAL List of strings. + An error is thrown if an aka in VAL is already in use + and `bbdb-allow-duplicates' is nil. :organization VAL List of strings. :mail VAL String with comma-separated mail address or a list of strings. - An error is thrown if a mail address in MAIL is already + An error is thrown if a mail address in VAL is already in use and `bbdb-allow-duplicates' is nil. :phone VAL List of phone-number objects. A phone-number is a vector [\"label\" area-code prefix suffix extension-or-nil] @@ -962,11 +959,9 @@ The following keywords are supported in SPEC: (check (bbdb-check-type name '(or (const nil) (cons string string)) t))) - (let ((firstname (car name)) - (lastname (cdr name))) - (bbdb-check-name firstname lastname) ; check for duplicates - (bbdb-record-set-firstname record firstname) - (bbdb-record-set-lastname record lastname)))) + (bbdb-check-name name) ; check for duplicates + (bbdb-record-set-firstname record (car name)) + (bbdb-record-set-lastname record (cdr name)))) (`:affix (let ((affix (bbdb-split-maybe 'affix (pop spec)))) @@ -982,15 +977,13 @@ The following keywords are supported in SPEC: (`:aka (let ((aka (bbdb-split-maybe 'aka (pop spec)))) (if check (bbdb-check-type aka (bbdb-record-aka record-type) t)) + (bbdb-check-name aka) (bbdb-record-set-aka record aka))) (`:mail (let ((mail (bbdb-split-maybe 'mail (pop spec)))) (if check (bbdb-check-type mail (bbdb-record-mail record-type) t)) - (unless bbdb-allow-duplicates - (dolist (elt mail) - (if (bbdb-gethash elt '(mail)) - (error "%s is already in the database" elt)))) + (bbdb-check-mail mail) (bbdb-record-set-mail record mail))) (`:phone @@ -1128,19 +1121,10 @@ A non-nil prefix arg is passed on to `bbdb-read-field' as FLAG (see there)." (let ((bbdb-phone-style (if flag (if (eq bbdb-phone-style 'nanp) nil 'nanp) bbdb-phone-style))) - (apply 'vector - (bbdb-read-string "Label: " nil bbdb-phone-label-list) - (bbdb-error-retry - (bbdb-parse-phone - (read-string "Phone: " - (and (integerp bbdb-default-area-code) - (format "(%03d) " - bbdb-default-area-code)))))))) + (bbdb-record-edit-phone))) ;; Address ((eq field 'address) - (let ((address (make-vector bbdb-address-length nil))) - (bbdb-record-edit-address address nil t) - address)) + (bbdb-record-edit-address)) ;; xfield ((or (memq field bbdb-xfield-label-list) ;; New xfield @@ -1197,10 +1181,17 @@ a phone number or address with VALUE being nil. ((eq field 'phone) (unless value (error "No phone specified")) - (bbdb-record-edit-phone (bbdb-record-phone record) value)) + (bbdb-record-set-field + record field + ;; Splice new phone value into list of phones. + (let ((phones (bbdb-record-phone record))) + (setcar (memq value phones) + (bbdb-record-edit-phone value)) + phones))) ((eq field 'address) (unless value (error "No address specified")) - (bbdb-record-edit-address value nil flag)) + (bbdb-record-edit-address value nil flag) + (bbdb-record-set-field record field (bbdb-record-address record))) ((eq field 'organization) (bbdb-record-set-field record field @@ -1218,8 +1209,9 @@ a phone number or address with VALUE being nil. (bbdb-record-set-field record 'uuid (bbdb-read-string "uuid (edit at your own risk): " (bbdb-record-uuid record)))) ((eq field 'creation-date) - (bbdb-record-set-creation-date - record (bbdb-read-string "creation-date: " (bbdb-record-creation-date record)))) + (bbdb-record-set-field + record 'creation-date + (bbdb-read-string "creation-date: " (bbdb-record-creation-date record)))) ;; The timestamp is set automatically whenever we save a modified record. ;; So any editing gets overwritten. ((eq field 'timestamp)) ; do nothing @@ -1338,12 +1330,15 @@ This calls bbdb-read-xfield-FIELD if it exists." nil nil init)) (bbdb-split 'organization (bbdb-read-string "Organizations: " init)))) -(defun bbdb-record-edit-address (address &optional label ignore-country) - "Edit ADDRESS. +;; The name `bbdb-read-address' might fit better. +(defun bbdb-record-edit-address (&optional address label ignore-country) + "Edit and return ADDRESS. If LABEL is nil, edit the label sub-field of the address as well. If the country field of ADDRESS is nonempty and IGNORE-COUNTRY is nil, use the rule from `bbdb-address-format-list' matching this country. Otherwise, use the default rule according to `bbdb-address-format-list'." + (unless address + (setq address (make-vector bbdb-address-length nil))) (unless label (setq label (bbdb-read-string "Label: " (bbdb-address-label address) @@ -1403,7 +1398,8 @@ Otherwise, use the default rule according to `bbdb-address-format-list'." ;; The following is a temporary fix. Ideally, we would simply discard ;; the entire address, but that requires bigger hacking. (bbdb-address-set-country address "Emacs") - (bbdb-address-set-country address (elt new-addr 4))))) + (bbdb-address-set-country address (elt new-addr 4))) + address)) (defun bbdb-edit-address-street (streets) "Edit list STREETS." @@ -1416,10 +1412,13 @@ Otherwise, use the default rule according to `bbdb-address-format-list'." (setq n (1+ n))) (reverse list))) -;; This function can provide some guidance for writing -;; your own address editing function +;; This function can provide some guidance for writing your own +;; address editing function for `bbdb-address-format-list'. +;; Such a function should return a list or vector with five elements, +;; a list of streets, city, state, postcode, country. +;; These elements should be strings or nil. (defun bbdb-edit-address-default (address) - "Function to use for address editing. + "Function for editing ADDRESS to be used by `bbdb-address-format-list'. The sub-fields and the prompts used are: Street, line n: (nth n street) City: city @@ -1438,21 +1437,26 @@ Country: country" bbdb-default-country) bbdb-country-list))) -(defun bbdb-record-edit-phone (phones phone) - "For list PHONES edit PHONE number." +;; The name `bbdb-read-phone' might fit better. +(defun bbdb-record-edit-phone (&optional phone label) + "Edit and return PHONE. +If LABEL is nil, edit the label sub-field of PHONE as well." ;; Phone numbers are special. They are vectors with either ;; two or four elements. We do not know whether after editing PHONE ;; we still have a number requiring the same format as PHONE. - ;; So we take all numbers PHONES of the record so that we can - ;; replace the element PHONE in PHONES. - (setcar (memq phone phones) - (apply 'vector - (bbdb-read-string "Label: " - (bbdb-phone-label phone) - bbdb-phone-label-list) - (bbdb-error-retry - (bbdb-parse-phone - (read-string "Phone: " (bbdb-phone-string phone))))))) + ;; So we throw away the argument PHONE and return a new vector. + (apply 'vector + (or label + (bbdb-read-string "Label: " + (and phone (bbdb-phone-label phone)) + bbdb-phone-label-list)) + (bbdb-error-retry + (bbdb-parse-phone + (read-string "Phone: " + (or (and phone (bbdb-phone-string phone)) + (and (integerp bbdb-default-area-code) + (format "(%03d) " + bbdb-default-area-code)))))))) ;; (bbdb-list-transpose '(a b c d) 1 3) (defun bbdb-list-transpose (list i j) diff --git a/bbdb.el b/bbdb.el index 4f53361..740667e 100644 --- a/bbdb.el +++ b/bbdb.el @@ -2405,8 +2405,8 @@ It is the caller's responsibility to make the new record known to BBDB." ;; `bbdb-hashtable' associates with each KEY a list of matching records. ;; KEY includes fl-name, lf-name, organizations, AKAs and email addresses. ;; When loading the database the hash table is initialized by calling -;; `bbdb-hash-record' for each record. This function is also called -;; when new records are added to the database. +;; `bbdb-register-record' for each record. This function is also called +;; when adding new records to the database. ;; `bbdb-delete-record-internal' with arg REMHASH non-nil removes a record ;; from the hash table (besides deleting the record from the database). ;; When an existing record is modified, the code that modifies the record @@ -2481,19 +2481,6 @@ KEY must be a string or nil. Empty strings and nil are ignored." (puthash key records bbdb-hashtable) (remhash key bbdb-hashtable)))))) -(defun bbdb-hash-record (record) - "Insert RECORD in `bbdb-hashtable'. -This performs all initializations required for a new record. -Do not call this for existing records that require updating." - (bbdb-puthash (bbdb-record-name record) record) - (bbdb-puthash (bbdb-record-name-lf record) record) - (dolist (organization (bbdb-record-organization record)) - (bbdb-puthash organization record)) - (dolist (aka (bbdb-record-aka record)) - (bbdb-puthash aka record)) - (bbdb-puthash-mail record) - (puthash (bbdb-record-uuid record) record bbdb-uuid-table)) - (defun bbdb-puthash-mail (record) "For RECORD put mail into `bbdb-hashtable'." (let (mail-aka mail-canon address) @@ -2517,18 +2504,54 @@ Both OLD and NEW are lists of values." (dolist (elt new) (bbdb-puthash elt record))) -(defun bbdb-check-name (first last &optional record) - "Check whether the name FIRST LAST is a valid name. -This throws an error if the name is already used by another record -and `bbdb-allow-duplicates' is nil. If RECORD is non-nil, FIRST and LAST -may correspond to RECORD without raising an error." - ;; Are there more useful checks for names beyond checking for duplicates? +(defun bbdb-check-name (name &optional record warn) + "Check whether NAME is a valid name. +This throws an error if NAME is already used by another record +and `bbdb-allow-duplicates' is nil. +NAME may be a string, a cons (FIRST . LAST) or a list of name strings. +If RECORD is non-nil, NAME may correspond to RECORD without raising an error. +If WARN is non-nil, issue a warning instead of raising an error." + ;; Are there other useful checks for names beyond checking for duplicates? (unless bbdb-allow-duplicates - (let* ((name (bbdb-concat 'name-first-last first last)) - (records (bbdb-gethash name '(fl-name lf-name aka)))) - (if (or (and (not record) records) - (remq record records)) - (error "%s is already in BBDB" name))))) + (cl-flet ((fun (name) + (let* ((tmp (bbdb-gethash name '(fl-name lf-name aka))) + (records (if record (remq record tmp) tmp))) + (if records + ;; Be verbose as the duplicates may be AKAs. + (let ((msg (format "Name `%s' is already in BBDB: %s" + name (mapconcat 'bbdb-record-name + records ", ")))) + (if (not warn) + (error msg) + (message msg) + (sit-for 1))))))) + (cond ((stringp name) + (fun name)) + ((and (consp name) (stringp (cdr name))) + (fun (bbdb-concat 'name-first-last (car name) (cdr name)))) + (t (mapc #'fun name)))))) + +(defun bbdb-check-mail (mail &optional record warn) + "Check whether MAIL is a valid mail address. +This throws an error if MAIL is already used by another record +and `bbdb-allow-duplicates' is nil. +MAIL may be a mail string or a list of mail strings. +If RECORD is non-nil, MAIL may appear in RECORD without raising an error. +If WARN is non-nil, issue a warning instead of raising an error." + ;; Are there other useful checks for mail addresses beyond checking + ;; for duplicates? + (unless bbdb-allow-duplicates + (dolist (m (if (listp mail) mail (list mail))) + (let* ((tmp (bbdb-gethash (nth 1 (bbdb-decompose-bbdb-address m)) + '(mail))) + (records (if record (remq record tmp) tmp))) + (if records + (let ((msg (format "Mail `%s' is already in BBDB: %s" m + (mapconcat 'bbdb-record-name records ", ")))) + (if (not warn) + (error msg) + (message msg) + (sit-for 1)))))))) (defun bbdb-record-name (record) "Record cache function: Return the full name FIRST_LAST of RECORD. @@ -2829,14 +2852,14 @@ See also `bbdb-record-field'." (cond ((eq field 'firstname) ; First name (if merge (error "Does not merge names")) (if check (bbdb-check-type value (bbdb-record-firstname record-type) t)) - (bbdb-check-name value (bbdb-record-lastname record) record) + (bbdb-check-name (cons value (bbdb-record-lastname record)) record) (bbdb-record-set-name record value t)) ;; Last name ((eq field 'lastname) (if merge (error "Does not merge names")) (if check (bbdb-check-type value (bbdb-record-lastname record-type) t)) - (bbdb-check-name (bbdb-record-firstname record) value record) + (bbdb-check-name (cons (bbdb-record-firstname record) value) record) (bbdb-record-set-name record t value)) ;; Name @@ -2845,9 +2868,8 @@ See also `bbdb-record-field'." (if (stringp value) (setq value (bbdb-divide-name value)) (if check (bbdb-check-type value '(cons string string) t))) - (let ((fn (car value)) (ln (cdr value))) - (bbdb-check-name fn ln record) - (bbdb-record-set-name record fn ln))) + (bbdb-check-name value record) + (bbdb-record-set-name record (car value) (cdr value))) ;; Affix ((eq field 'affix) @@ -2874,11 +2896,7 @@ See also `bbdb-record-field'." value 'bbdb-string=))) (if check (bbdb-check-type value (bbdb-record-aka record-type) t)) (setq value (bbdb-list-strings value)) - (unless bbdb-allow-duplicates - (dolist (aka value) - (let ((old (remq record (bbdb-gethash aka '(fl-name lf-name aka))))) - (if old (error "Alternate name address \"%s\" is used by \"%s\"" - aka (mapconcat 'bbdb-record-name old ", ")))))) + (bbdb-check-name value record) (bbdb-hash-update record (bbdb-record-aka record) value) (bbdb-record-set-aka record value)) @@ -2888,11 +2906,7 @@ See also `bbdb-record-field'." value 'bbdb-string=))) (if check (bbdb-check-type value (bbdb-record-mail record-type) t)) (setq value (bbdb-list-strings value)) - (unless bbdb-allow-duplicates - (dolist (mail value) - (let ((old (remq record (bbdb-gethash mail '(mail))))) - (if old (error "Mail address \"%s\" is used by \"%s\"" - mail (mapconcat 'bbdb-record-name old ", ")))))) + (bbdb-check-mail value record) (dolist (aka (bbdb-record-mail-aka record)) (bbdb-remhash aka record)) (dolist (mail (bbdb-record-mail-canon record)) @@ -3383,46 +3397,10 @@ If `bbdb-file' uses an outdated format, migrate to `bbdb-file-format'." ;; We are just loading BBDB, so we are not yet ready ;; for sophisticated solutions. (error "Duplicate UUID %s" (bbdb-record-uuid record))) - - ;; Set the completion lists - (dolist (phone (bbdb-record-phone record)) - (bbdb-pushnew (bbdb-phone-label phone) bbdb-phone-label-list)) - (dolist (address (bbdb-record-address record)) - (bbdb-pushnew (bbdb-address-label address) bbdb-address-label-list) - (mapc (lambda (street) (bbdb-pushnewt street bbdb-street-list)) - (bbdb-address-streets address)) - (bbdb-pushnewt (bbdb-address-city address) bbdb-city-list) - (bbdb-pushnewt (bbdb-address-state address) bbdb-state-list) - (bbdb-pushnewt (bbdb-address-postcode address) bbdb-postcode-list) - (bbdb-pushnewt (bbdb-address-country address) bbdb-country-list)) - (dolist (xfield (bbdb-record-xfields record)) - (bbdb-pushnewq (car xfield) bbdb-xfield-label-list)) - (dolist (organization (bbdb-record-organization record)) - (bbdb-pushnew organization bbdb-organization-list)) - - (let ((name (bbdb-concat 'name-first-last - (bbdb-record-firstname record) - (bbdb-record-lastname record)))) - (when (and (not bbdb-allow-duplicates) - (bbdb-gethash name '(fl-name aka))) - ;; This does not check for duplicate mail fields. - ;; Yet under normal circumstances, this should really - ;; not be necessary each time BBDB is loaded as BBDB checks - ;; whether creating a new record or modifying an existing one - ;; results in duplicates. - ;; Alternatively, you can use `bbdb-search-duplicates'. - (message "Duplicate BBDB record encountered: %s" name) - (sit-for 1))) - - ;; If `bbdb-allow-duplicates' is non-nil, we allow that two records - ;; (with different uuids) refer to the same person (same name etc.). - ;; Such duplicate records are always hashed. - ;; Otherwise, an unhashed record would not be available for things - ;; like completion (and we would not know which record to keeep - ;; and which one to hide). We trust the user she knows what - ;; she wants if she keeps duplicate records in the database though - ;; `bbdb-allow-duplicates' is nil. - (bbdb-hash-record record)) + ;; With `bbdb-allow-duplicates' nil, BBDB would become unusable + ;; if duplicates threw an error upon loading BBDB. Thus we only + ;; issue a message. + (bbdb-register-record record t)) ;; Note that `bbdb-xfield-label-list' serves two purposes: ;; - check whether an xfield is new to BBDB @@ -3451,6 +3429,52 @@ If `bbdb-file' uses an outdated format, migrate to `bbdb-file-format'." (unless bbdb-silent (message "Parsing BBDB file `%s'...done" file)) bbdb-records))))) +(defun bbdb-register-record (record &optional warn) + "Register RECORD with BBDB. +This performs the registration (including hash tables and cache) required both +for records that are loaded from the database and for new records added to BBDB. +If `bbdb-allow-duplicates' is nil, this throws an error if the name, +an aka or mail address of RECORD is already in BBDB. If WARN is non-nil, +issue a warning instead. +Do not call this function directly. Call instead `bbdb-change-record'." + (bbdb-check-name (cons (bbdb-record-firstname record) + (bbdb-record-lastname record)) + record warn) + (bbdb-check-mail (bbdb-record-mail record) record warn) + + ;; If `bbdb-allow-duplicates' is non-nil, we allow that two records + ;; (with different uuids) refer to the same person (same name etc.). + ;; Such duplicate records are always hashed. + ;; Otherwise, an unhashed record would not be available for things + ;; like completion (and we would not know which record to keeep + ;; and which one to hide). We trust the user she knows what + ;; she wants if she keeps duplicate records in the database though + ;; `bbdb-allow-duplicates' is nil. + (bbdb-puthash (bbdb-record-name record) record) + (bbdb-puthash (bbdb-record-name-lf record) record) + (dolist (organization (bbdb-record-organization record)) + (bbdb-puthash organization record)) + (dolist (aka (bbdb-record-aka record)) + (bbdb-puthash aka record)) + (bbdb-puthash-mail record) + (puthash (bbdb-record-uuid record) record bbdb-uuid-table) + + ;; Update the completion lists + (dolist (phone (bbdb-record-phone record)) + (bbdb-pushnew (bbdb-phone-label phone) bbdb-phone-label-list)) + (dolist (address (bbdb-record-address record)) + (bbdb-pushnew (bbdb-address-label address) bbdb-address-label-list) + (mapc (lambda (street) (bbdb-pushnewt street bbdb-street-list)) + (bbdb-address-streets address)) + (bbdb-pushnewt (bbdb-address-city address) bbdb-city-list) + (bbdb-pushnewt (bbdb-address-state address) bbdb-state-list) + (bbdb-pushnewt (bbdb-address-postcode address) bbdb-postcode-list) + (bbdb-pushnewt (bbdb-address-country address) bbdb-country-list)) + (dolist (xfield (bbdb-record-xfields record)) + (bbdb-pushnewq (car xfield) bbdb-xfield-label-list)) + (dolist (organization (bbdb-record-organization record)) + (bbdb-pushnew organization bbdb-organization-list))) + (defun bbdb-before-save () "Run before saving `bbdb-file' as buffer-local part of `before-save-hook'." (when (and bbdb-file-remote @@ -3473,9 +3497,9 @@ If `bbdb-file' uses an outdated format, migrate to `bbdb-file-format'." "Update the database after a change of RECORD. Return RECORD if RECORD got changed compared with the database, return nil otherwise. -Hash RECORD if it is new. If RECORD is not new, it is the the caller's -responsibility to update the hashtables for RECORD. (Up-to-date hashtables are -ensured if the fields are modified by calling `bbdb-record-set-field'.) +Register RECORD if it is new. If RECORD is not new, it is the caller's +responsibility to update this information for RECORD. (This is ensured +if the fields of RECORD are modified by calling `bbdb-record-set-field'.) Redisplay RECORD if it is not new. Args IGNORED are ignored and their use is discouraged. @@ -3551,8 +3575,8 @@ They are present only for backward compatibility." (bbdb-record-set-timestamp record (format-time-string bbdb-time-stamp-format nil t)) (run-hook-with-args 'bbdb-change-hook record) + (bbdb-register-record record) ; Call this earlier? (bbdb-insert-record-internal record) - (bbdb-hash-record record) (bbdb-pushnewq record bbdb-changed-records) (run-hook-with-args 'bbdb-after-change-hook record) record)))))