Merge remote-tracking branch 'origin/master' into feature/pgtk

This commit is contained in:
Po Lu 2021-12-16 17:57:56 +08:00
commit 32b9b22f66
18 changed files with 976 additions and 105 deletions

View file

@ -526,6 +526,7 @@ Variables
* Variables with Restricted Values:: Non-constant variables whose value can
@emph{not} be an arbitrary Lisp object.
* Generalized Variables:: Extending the concept of variables.
* Multisession Variables:: Variables that survive restarting Emacs.
Scoping Rules for Variable Bindings
@ -547,6 +548,10 @@ Generalized Variables
* Setting Generalized Variables:: The @code{setf} macro.
* Adding Generalized Variables:: Defining new @code{setf} forms.
Multisession Variables
* Multisession Variables:: Variables that survive restarting Emacs.
Functions
* What Is a Function:: Lisp functions vs. primitives; terminology.

View file

@ -44,6 +44,7 @@ representing the variable.
* Variables with Restricted Values:: Non-constant variables whose value can
@emph{not} be an arbitrary Lisp object.
* Generalized Variables:: Extending the concept of variables.
* Multisession Variables:: Variables that survive restarting Emacs.
@end menu
@node Global Variables
@ -2752,3 +2753,157 @@ form that has not already had an appropriate expansion defined. In
Common Lisp, this is not an error since the function @code{(setf
@var{func})} might be defined later.
@end quotation
@node Multisession Variables
@section Multisession Variables
@cindex multisession variable
When you set a variable to a value and then close Emacs and restart
it, that value won't be automatically restored. Users usually set
normal variables in their startup files, or use Customize
(@pxref{Customization}) to set user options permanently, and various
packages have various files wher they store the data (e.g., Gnus
stores this in @file{.newsrc.eld} and the URL library stores cookies
in @file{~/.emacs.d/url/cookies}).
For things in between these two extremes (i.e., configuration which
goes in the startup file, and massive application state that goes into
separate files), Emacs provides a facility to replicate data between
sessions called @dfn{multisession variables}. (This facility may not
be available on all systems.) To give you an idea of how these are
meant to be used, here's a small example:
@lisp
@group
(define-multisession-variable foo-var 0)
(defun my-adder (num)
(interactive "nAdd number: ")
(setf (multisession-value foo)
(+ (multisession-value foo) num))
(message "The new number is: %s" (multisession-value foo)))
@end group
@end lisp
@noindent
This defines the variable @code{foo-var} and binds it to a special
multisession object which is initialized with the value @samp{0} (if
the variable doesn't already exist from a previous session). The
@code{my-adder} command queries the user for a number, adds this to
the old (possibly saved value), and then saves the new value.
This facility isn't meant to be used for huge data structures, but
should be performant for most values.
@defmac define-multisession-variable name initial-value &optional doc &rest args
This macro defines @var{name} as a multisession variable, and gives it
the @var{initial-value} if this variable hasn't been assigned a value
earlier. @var{doc} is the doc string, and several keyword arguments can
be used in @var{args}:
@table @code
@item :package @var{package-symbol}
This keyword says that a multisession variable belongs to the package
specified by @var{package-symbol}. The combination of
@var{package-symbol} and @var{name} has to be unique. If
@var{package-symbol} isn't given, this will default to the first
``segment'' of the @var{name} symbol's name, which is the part of its
name up to and excluding the first @samp{-}. For instance, if
@var{name} is @code{foo-var} and @var{package-symbol} isn't given,
@var{package-symbol} will default to @code{foo}.
@cindex synchronized multisession variables
@item :synchronized @var{bool}
Multisession variables can be @dfn{synchronized} if @var{bool} is
non-@code{nil}. This means that if there're two concurrent Emacs
instances running, and the other Emacs changes the multisession
variable @code{foo-var}, the current Emacs instance will retrieve that
modified data when accessing the value. If @var{synchronized} is
@code{nil} or missing, this won't happen, and the values in all
Emacs sessions using the variable will be independent of each other.
@item :storage @var{storage}
Use the specified @var{storage} method. This can be either
@code{sqlite} (in Emacs compiled with SQLite support) or @code{files}.
If not given, this defaults to the value of the
@code{multisession-storage} variable, described below.
@end table
@end defmac
@defun multisession-value variable
This function returns the current value of @var{variable}. If this
variable hasn't been accessed before in this Emacs session, or if it's
changed externally, it will be read in from external storage. If not,
the current value in this session is returned as is. It is an error
to call this function for a @var{variable} that is not a multisession
variable.
Values retrieved via @code{multisession-value} may or may not be
@code{eq} to each other, but they will always be @code{equal}.
This is a generalized variable (@pxref{Generalized Variables}), so the
way to update such a variable is to say, for instance:
@lisp
(setf (multisession-value foo-bar) 'zot)
@end lisp
Only Emacs Lisp values that have a readable print syntax
(@pxref{Printed Representation}) can be saved this way.
If the multisession variable is synchronized, setting it may update
the value first. For instance:
@lisp
(cl-incf (multisession-value foo-bar))
@end lisp
This first checks whether the value has changed in a different
Emacs instance, retrieves that value, and then adds 1 to that value and
stores it. But note that this is done without locking, so if many
instances are updating the value at the same time, it's unpredictable
which instance ``wins''.
@end defun
@defun multisession-delete object
This function deletes @var{object} and its value from its persistent
storage.
@end defun
@c FIXME: this lacks the documentation of the form of the arguments.
@defun make-multisession
You can also make persistent values that aren't tied to a specific
variable, but are tied to an explicit package and key.
@example
(setq foo (make-multisession :package "mail"
:key "friends"))
(setf (multisession-value foo) 'everybody)
@end example
This supports the same keywords as
@code{define-multisession-variable}, but also supports a
@code{:initial-value} keyword, which specifies the default value.
@end defun
@defopt multisession-storage
This variable controls how the multisession variables are stored. It
value defaults to @code{files}, which means that the values are stored
in a one-file-per-variable structure inside the directory specified by
@code{multisession-directory}. If this value is @code{sqlite}
instead, the values are stored in an SQLite database; this is only
available if Emacs was built with SQLite support.
@end defopt
@defopt multisession-directory
The multisession variables are stored under this directory, which
defaults to @file{multisession/} subdirectory of the
@code{user-emacs-directory}, which is typically
@file{~/.emacs.d/multisession/}.
@end defopt
@findex multisession-edit-mode
@deffn Command list-multisession-values
This command pops up a buffer listing all the multisession variables,
and enters a special mode @code{multisession-edit-mode} which allows
you to delete them and edit their values.
@end deffn

View file

@ -839,6 +839,12 @@ This change is now applied in 'dired-insert-directory'.
* Lisp Changes in Emacs 29.1
+++
** New facility for handling session state: 'multisession-value'.
This can be used as a convenient way to store (simple) application
state, and 'M-x list-multisession-values' allows users to list
(and edit) this data.
+++
** New function 'get-display-property'.
This is like 'get-text-property', but works on the 'display' text

View file

@ -1529,26 +1529,29 @@ the tests)."
(defun ert-write-junit-test-report (stats)
"Write a JUnit test report, generated from STATS."
;; https://www.ibm.com/docs/de/developer-for-zos/14.1.0?topic=formats-junit-xml-format
;; https://www.ibm.com/docs/en/developer-for-zos/14.1.0?topic=formats-junit-xml-format
;; https://llg.cubic.org/docs/junit/
(unless (zerop (length (ert--stats-tests stats)))
(when-let ((test-file
(symbol-file
(ert-test-name (aref (ert--stats-tests stats) 0)) 'ert--test)))
(with-temp-file (file-name-with-extension test-file "xml")
(ert-test-name (aref (ert--stats-tests stats) 0)) 'ert--test))
(test-report (file-name-with-extension test-file "xml")))
(with-temp-file test-report
(insert "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
(insert (format "<testsuites name=\"%s\" tests=\"%s\" failures=\"%s\" skipped=\"%s\" time=\"%s\">\n"
(file-name-nondirectory test-file)
(insert (format "<testsuites name=\"%s\" tests=\"%s\" errors=\"%s\" failures=\"%s\" skipped=\"%s\" time=\"%s\">\n"
(file-name-nondirectory test-report)
(ert-stats-total stats)
(if (ert--stats-aborted-p stats) 1 0)
(ert-stats-completed-unexpected stats)
(ert-stats-skipped stats)
(float-time
(time-subtract
(ert--stats-end-time stats)
(ert--stats-start-time stats)))))
(insert (format " <testsuite id=\"0\" name=\"%s\" tests=\"%s\" failures=\"%s\" skipped=\"%s\" time=\"%s\" timestamp=\"%s\">\n"
(file-name-nondirectory test-file)
(insert (format " <testsuite id=\"0\" name=\"%s\" tests=\"%s\" errors=\"%s\" failures=\"%s\" skipped=\"%s\" time=\"%s\" timestamp=\"%s\">\n"
(file-name-nondirectory test-report)
(ert-stats-total stats)
(if (ert--stats-aborted-p stats) 1 0)
(ert-stats-completed-unexpected stats)
(ert-stats-skipped stats)
(float-time
@ -1570,40 +1573,52 @@ the tests)."
(ert-test-result-expected-p test result))
(ert-test-result-duration result)))
(if (and (ert-test-result-expected-p test result)
(not (ert-test-aborted-with-non-local-exit-p result))
(not (ert-test-skipped-p result))
(zerop (length (ert-test-result-messages result))))
(insert "/>\n")
(insert ">\n")
(if (ert-test-skipped-p result)
(insert (format " <skipped message=\"%s\" type=\"%s\">\n"
(xml-escape-string
(string-trim
(ert-reason-for-test-result result)))
(ert-string-for-test-result
result
(ert-test-result-expected-p
test result)))
(xml-escape-string
(string-trim
(ert-reason-for-test-result result)))
"\n"
" </skipped>\n")
(unless
(ert-test-result-type-p
result (ert-test-expected-result-type test))
(insert (format " <failure message=\"%s\" type=\"%s\">\n"
(xml-escape-string
(string-trim
(ert-reason-for-test-result result)))
(ert-string-for-test-result
result
(ert-test-result-expected-p
test result)))
(xml-escape-string
(string-trim
(ert-reason-for-test-result result)))
"\n"
" </failure>\n")))
(cond
((ert-test-skipped-p result)
(insert (format " <skipped message=\"%s\" type=\"%s\">\n"
(xml-escape-string
(string-trim
(ert-reason-for-test-result result)))
(ert-string-for-test-result
result
(ert-test-result-expected-p
test result)))
(xml-escape-string
(string-trim
(ert-reason-for-test-result result)))
"\n"
" </skipped>\n"))
((ert-test-aborted-with-non-local-exit-p result)
(insert (format " <error message=\"%s\" type=\"%s\">\n"
(file-name-nondirectory test-report)
(ert-string-for-test-result
result
(ert-test-result-expected-p
test result)))
(format "Test %s aborted with non-local exit\n"
(xml-escape-string
(symbol-name (ert-test-name test))))
" </error>\n"))
((not (ert-test-result-type-p
result (ert-test-expected-result-type test)))
(insert (format " <failure message=\"%s\" type=\"%s\">\n"
(xml-escape-string
(string-trim
(ert-reason-for-test-result result)))
(ert-string-for-test-result
result
(ert-test-result-expected-p
test result)))
(xml-escape-string
(string-trim
(ert-reason-for-test-result result)))
"\n"
" </failure>\n")))
(unless (zerop (length (ert-test-result-messages result)))
(insert " <system-out>\n"
(xml-escape-string
@ -1617,21 +1632,41 @@ the tests)."
"Write a JUnit summary test report, generated from LOGFILES."
(let ((report (file-name-with-extension
(getenv "EMACS_TEST_JUNIT_REPORT") "xml"))
(tests 0) (failures 0) (skipped 0) (time 0) (id 0))
(tests 0) (errors 0) (failures 0) (skipped 0) (time 0) (id 0))
(with-temp-file report
(dolist (logfile logfiles)
(let ((test-file (file-name-with-extension logfile "xml")))
(when (file-readable-p test-file)
(insert-file-contents-literally test-file)
(let ((test-report (file-name-with-extension logfile "xml")))
(if (not (file-readable-p test-report))
(let ((logfile (file-name-with-extension logfile "log")))
(insert (format " <testsuite id=\"%s\" name=\"%s\" tests=\"1\" errors=\"1\" failures=\"0\" skipped=\"0\" time=\"0\" timestamp=\"%s\">\n"
id test-report
(ert--format-time-iso8601 (current-time))))
(insert (format " <testcase name=\"Test report missing %s\" status=\"error\" time=\"0\">\n"
(file-name-nondirectory test-report)))
(insert (format " <error message=\"Test report missing %s\" type=\"error\">\n"
(file-name-nondirectory test-report)))
(when (file-readable-p logfile)
(insert (xml-escape-string
(with-temp-buffer
(insert-file-contents-literally logfile)
(buffer-string)))))
(insert " </error>\n"
" </testcase>\n"
" </testsuite>\n")
(cl-incf errors 1)
(cl-incf id 1))
(insert-file-contents-literally test-report)
(when (looking-at-p
(regexp-quote "<?xml version=\"1.0\" encoding=\"utf-8\"?>"))
(delete-region (point) (line-beginning-position 2)))
(when (looking-at
"<testsuites name=\".+\" tests=\"\\(.+\\)\" failures=\"\\(.+\\)\" skipped=\"\\(.+\\)\" time=\"\\(.+\\)\">")
"<testsuites name=\".+\" tests=\"\\(.+\\)\" errors=\"\\(.+\\)\" failures=\"\\(.+\\)\" skipped=\"\\(.+\\)\" time=\"\\(.+\\)\">")
(cl-incf tests (string-to-number (match-string 1)))
(cl-incf failures (string-to-number (match-string 2)))
(cl-incf skipped (string-to-number (match-string 3)))
(cl-incf time (string-to-number (match-string 4)))
(cl-incf errors (string-to-number (match-string 2)))
(cl-incf failures (string-to-number (match-string 3)))
(cl-incf skipped (string-to-number (match-string 4)))
(cl-incf time (string-to-number (match-string 5)))
(delete-region (point) (line-beginning-position 2)))
(when (looking-at " <testsuite id=\"\\(0\\)\"")
(replace-match (number-to-string id) nil nil nil 1)
@ -1639,16 +1674,17 @@ the tests)."
(goto-char (point-max))
(beginning-of-line 0)
(when (looking-at-p "</testsuites>")
(delete-region (point) (line-beginning-position 2)))
(narrow-to-region (point-max) (point-max)))))
(delete-region (point) (line-beginning-position 2))))
(narrow-to-region (point-max) (point-max))))
(insert "</testsuites>\n")
(widen)
(goto-char (point-min))
(insert "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
(insert (format "<testsuites name=\"%s\" tests=\"%s\" failures=\"%s\" skipped=\"%s\" time=\"%s\">\n"
(insert (format "<testsuites name=\"%s\" tests=\"%s\" errors=\"%s\" failures=\"%s\" skipped=\"%s\" time=\"%s\">\n"
(file-name-nondirectory report)
tests failures skipped time)))))
tests errors failures skipped time)))))
(defun ert-summarize-tests-batch-and-exit (&optional high)
"Summarize the results of testing.

View file

@ -0,0 +1,439 @@
;;; multisession.el --- Multisession storage for variables -*- lexical-binding: t; -*-
;; Copyright (C) 2021 Free Software Foundation, Inc.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require 'cl-lib)
(require 'eieio)
(require 'sqlite)
(require 'url)
(require 'tabulated-list)
(defcustom multisession-storage 'files
"Storage method for multisession variables.
Valid methods are `sqlite' and `files'."
:type '(choice (const :tag "SQLite" sqlite)
(const :tag "Files" files))
:version "29.1"
:group 'files)
(defcustom multisession-directory (expand-file-name "multisession/"
user-emacs-directory)
"Directory to store multisession variables."
:type 'file
:version "29.1"
:group 'files)
;;;###autoload
(defmacro define-multisession-variable (name initial-value &optional doc
&rest args)
"Make NAME into a multisession variable initialized from INITIAL-VALUE.
DOC should be a doc string, and ARGS are keywords as applicable to
`make-multisession'."
(declare (indent defun))
(unless (plist-get args :package)
(setq args (nconc (list :package
(replace-regexp-in-string "-.*" ""
(symbol-name name)))
args)))
`(defvar ,name
(make-multisession :key ,(symbol-name name)
:initial-value ,initial-value
,@args)
,@(list doc)))
(defconst multisession--unbound (make-symbol "unbound"))
(cl-defstruct (multisession
(:constructor nil)
(:constructor multisession--create)
(:conc-name multisession--))
"A persistent variable that will live across Emacs invocations."
key
(initial-value nil)
package
(storage multisession-storage)
(synchronized nil)
(cached-value multisession--unbound)
(cached-sequence 0))
(cl-defun make-multisession (&key key initial-value package synchronized
storage)
"Create a multisession object."
(unless package
(error "No package for the multisession object"))
(unless key
(error "No key for the multisession object"))
(unless (stringp package)
(error "The package has to be a string"))
(unless (stringp key)
(error "The key has to be a string"))
(multisession--create
:key key
:synchronized synchronized
:initial-value initial-value
:package package
:storage (or storage multisession-storage)))
(defun multisession-value (object)
"Return the value of the multisession OBJECT."
(if (null user-init-file)
;; If we don't have storage, then just return the value from the
;; object.
(if (eq (multisession--cached-value object) multisession--unbound)
(multisession--initial-value object)
(multisession--cached-value object))
;; We have storage, so we update from storage.
(multisession-backend-value (multisession--storage object) object)))
(defun multisession--set-value (object value)
"Set the stored value of OBJECT to VALUE."
(if (null user-init-file)
;; We have no backend, so just store the value.
(setf (multisession--cached-value object) value)
;; We have a backend.
(multisession--backend-set-value (multisession--storage object)
object value)))
(defun multisession-delete (object)
"Delete OBJECT from the backend storage."
(multisession--backend-delete (multisession--storage object) object))
(gv-define-simple-setter multisession-value multisession--set-value)
;; SQLite Backend
(declare-function sqlite-execute "sqlite.c")
(declare-function sqlite-select "sqlite.c")
(declare-function sqlite-open "sqlite.c")
(declare-function sqlite-pragma "sqlite.c")
(declare-function sqlite-transaction "sqlite.c")
(declare-function sqlite-commit "sqlite.c")
(defvar multisession--db nil)
(defun multisession--ensure-db ()
(unless multisession--db
(let* ((file (expand-file-name "sqlite/multisession.sqlite"
multisession-directory))
(dir (file-name-directory file)))
(unless (file-exists-p dir)
(make-directory dir t))
(setq multisession--db (sqlite-open file)))
(with-sqlite-transaction multisession--db
;; Use a write-ahead-log (available since 2010), which makes
;; writes a lot faster.
(sqlite-pragma multisession--db "journal_mode = WAL")
(sqlite-pragma multisession--db "synchronous = NORMAL")
(unless (sqlite-select
multisession--db
"select name from sqlite_master where type = 'table' and name = 'multisession'")
;; Tidy up the database automatically.
(sqlite-pragma multisession--db "auto_vacuum = FULL")
;; Create the table.
(sqlite-execute
multisession--db
"create table multisession (package text not null, key text not null, sequence number not null default 1, value text not null)")
(sqlite-execute
multisession--db
"create unique index multisession_idx on multisession (package, key)")))))
(cl-defmethod multisession-backend-value ((_type (eql sqlite)) object)
(multisession--ensure-db)
(let ((id (list (multisession--package object)
(multisession--key object))))
(cond
;; We have no value yet; check the database.
((eq (multisession--cached-value object) multisession--unbound)
(let ((stored
(car
(sqlite-select
multisession--db
"select value, sequence from multisession where package = ? and key = ?"
id))))
(if stored
(let ((value (car (read-from-string (car stored)))))
(setf (multisession--cached-value object) value
(multisession--cached-sequence object) (cadr stored))
value)
;; Nothing; return the initial value.
(multisession--initial-value object))))
;; We have a value, but we want to update in case some other
;; Emacs instance has updated.
((multisession--synchronized object)
(let ((stored
(car
(sqlite-select
multisession--db
"select value, sequence from multisession where sequence > ? and package = ? and key = ?"
(cons (multisession--cached-sequence object) id)))))
(if stored
(let ((value (car (read-from-string (car stored)))))
(setf (multisession--cached-value object) value
(multisession--cached-sequence object) (cadr stored))
value)
;; Nothing, return the cached value.
(multisession--cached-value object))))
;; Just return the cached value.
(t
(multisession--cached-value object)))))
(cl-defmethod multisession--backend-set-value ((_type (eql sqlite))
object value)
(catch 'done
(let ((i 0))
(while (< i 10)
(condition-case nil
(throw 'done (multisession--set-value-sqlite object value))
(sqlite-locked-error
(setq i (1+ i))
(sleep-for (+ 0.1 (/ (float (random 10)) 10))))))
(signal 'sqlite-locked-error "Database is locked"))))
(defun multisession--set-value-sqlite (object value)
(multisession--ensure-db)
(with-sqlite-transaction multisession--db
(let ((id (list (multisession--package object)
(multisession--key object)))
(pvalue
(let ((print-length nil)
(print-circle t)
(print-level nil))
(prin1-to-string value))))
(condition-case nil
(ignore (read-from-string pvalue))
(error (error "Unable to store unreadable value: %s" pvalue)))
(sqlite-execute
multisession--db
"insert into multisession(package, key, sequence, value) values(?, ?, 1, ?) on conflict(package, key) do update set sequence = sequence + 1, value = ?"
(append id (list pvalue pvalue)))
(setf (multisession--cached-sequence object)
(caar (sqlite-select
multisession--db
"select sequence from multisession where package = ? and key = ?"
id)))
(setf (multisession--cached-value object) value))))
(cl-defmethod multisession--backend-values ((_type (eql sqlite)))
(multisession--ensure-db)
(sqlite-select
multisession--db
"select package, key, value from multisession order by package, key"))
(cl-defmethod multisession--backend-delete ((_type (eql sqlite)) object)
(sqlite-execute multisession--db
"delete from multisession where package = ? and key = ?"
(list (multisession--package object)
(multisession--key object))))
;; Files Backend
(defun multisession--encode-file-name (name)
(url-hexify-string name))
(defun multisession--update-file-value (file object)
(condition-case nil
(with-temp-buffer
(let* ((time (file-attribute-modification-time
(file-attributes file)))
(coding-system-for-read 'utf-8))
(insert-file-contents file)
(let ((stored (read (current-buffer))))
(setf (multisession--cached-value object) stored
(multisession--cached-sequence object) time)
stored)))
;; If the file is contended (could happen with file locking in
;; Windws) or unreadable, just return the current value.
(error
(if (eq (multisession--cached-value object) multisession--unbound)
(multisession--initial-value object)
(multisession--cached-value object)))))
(defun multisession--object-file-name (object)
(expand-file-name
(concat "files/"
(multisession--encode-file-name (multisession--package object))
"/"
(multisession--encode-file-name (multisession--key object))
".value")
multisession-directory))
(cl-defmethod multisession-backend-value ((_type (eql files)) object)
(let ((file (multisession--object-file-name object)))
(cond
;; We have no value yet; see whether it's stored.
((eq (multisession--cached-value object) multisession--unbound)
(if (file-exists-p file)
(multisession--update-file-value file object)
;; Nope; return the initial value.
(multisession--initial-value object)))
;; We have a value, but we want to update in case some other
;; Emacs instance has updated.
((multisession--synchronized object)
(if (and (file-exists-p file)
(time-less-p (multisession--cached-sequence object)
(file-attribute-modification-time
(file-attributes file))))
(multisession--update-file-value file object)
;; Nothing, return the cached value.
(multisession--cached-value object)))
;; Just return the cached value.
(t
(multisession--cached-value object)))))
(cl-defmethod multisession--backend-set-value ((_type (eql files))
object value)
(let ((file (multisession--object-file-name object))
(time (current-time)))
;; Ensure that the directory exists.
(let ((dir (file-name-directory file)))
(unless (file-exists-p dir)
(make-directory dir t)))
(with-temp-buffer
(let ((print-length nil)
(print-circle t)
(print-level nil))
(prin1 value (current-buffer)))
(goto-char (point-min))
(condition-case nil
(read (current-buffer))
(error (error "Unable to store unreadable value: %s" (buffer-string))))
;; Write to a temp file in the same directory and rename to the
;; file for somewhat better atomicity.
(let ((coding-system-for-write 'utf-8)
(create-lockfiles nil)
(temp (make-temp-name file)))
(write-region (point-min) (point-max) temp nil 'silent)
(set-file-times temp time)
(rename-file temp file t)))
(setf (multisession--cached-sequence object) time
(multisession--cached-value object) value)))
(cl-defmethod multisession--backend-values ((_type (eql files)))
(mapcar (lambda (file)
(let ((bits (file-name-split file)))
(list (url-unhex-string (car (last bits 2)))
(url-unhex-string
(file-name-sans-extension (car (last bits))))
(with-temp-buffer
(let ((coding-system-for-read 'utf-8))
(insert-file-contents file)
(read (current-buffer)))))))
(directory-files-recursively
(expand-file-name "files" multisession-directory)
"\\.value\\'")))
(cl-defmethod multisession--backend-delete ((_type (eql files)) object)
(let ((file (multisession--object-file-name object)))
(when (file-exists-p file)
(delete-file file))))
;; Mode for editing.
(defvar-keymap multisession-edit-mode-map
:parent tabulated-list-mode-map
"d" #'multisession-delete-value
"e" #'multisession-edit-value)
(define-derived-mode multisession-edit-mode special-mode "Multisession"
"This mode lists all elements in the \"multisession\" database."
:interactive nil
(buffer-disable-undo)
(setq-local buffer-read-only t
truncate-lines t)
(setq tabulated-list-format
[("Package" 10)
("Key" 30)
("Value" 30)])
(setq-local revert-buffer-function #'multisession-edit-mode--revert))
;;;###autoload
(defun list-multisession-values (&optional choose-storage)
"List all values in the \"multisession\" database.
If CHOOSE-STORAGE (interactively, the prefix), query for the
storage method to list."
(interactive "P")
(let ((storage
(if choose-storage
(intern (completing-read "Storage method: " '(sqlite files) nil t))
multisession-storage)))
(pop-to-buffer (get-buffer-create (format "*Multisession %s*" storage)))
(multisession-edit-mode)
(setq-local multisession-storage storage)
(multisession-edit-mode--revert)
(goto-char (point-min))))
(defun multisession-edit-mode--revert (&rest _)
(let ((inhibit-read-only t)
(id (get-text-property (point) 'tabulated-list-id)))
(erase-buffer)
(tabulated-list-init-header)
(setq tabulated-list-entries
(mapcar (lambda (elem)
(list
(cons (car elem) (cadr elem))
(vector (car elem) (cadr elem)
(string-replace "\n" "\\n"
(format "%s" (caddr elem))))))
(multisession--backend-values multisession-storage)))
(tabulated-list-print t)
(goto-char (point-min))
(when id
(when-let ((match
(text-property-search-forward 'tabulated-list-id id t)))
(goto-char (prop-match-beginning match))))))
(defun multisession-delete-value (id)
"Delete the value at point."
(interactive (list (get-text-property (point) 'tabulated-list-id))
multisession-edit-mode)
(unless id
(error "No value on the current line"))
(unless (yes-or-no-p "Really delete this item? ")
(user-error "Not deleting"))
(multisession--backend-delete multisession-storage
(make-multisession :package (car id)
:key (cdr id)))
(let ((inhibit-read-only t))
(beginning-of-line)
(delete-region (point) (progn (forward-line 1) (point)))))
(defun multisession-edit-value (id)
"Edit the value at point."
(interactive (list (get-text-property (point) 'tabulated-list-id))
multisession-edit-mode)
(unless id
(error "No value on the current line"))
(let* ((object (make-multisession
:package (car id)
:key (cdr id)
:storage multisession-storage))
(value (multisession-value object)))
(setf (multisession-value object)
(car (read-from-string
(read-string "New value: " (prin1-to-string value))))))
(multisession-edit-mode--revert))
(provide 'multisession)
;;; multisession.el ends here

View file

@ -30,6 +30,7 @@
(require 'cl-lib)
(require 'cl-extra)
(require 'transient)
(require 'multisession)
(defgroup emoji nil
"Inserting Emojis."
@ -59,7 +60,7 @@
(defvar emoji--derived nil)
(defvar emoji--names (make-hash-table :test #'equal))
(defvar emoji--done-derived nil)
(defvar emoji--recent (list "😀" "😖"))
(define-multisession-variable emoji--recent (list "😀" "😖"))
(defvar emoji--insert-buffer)
;;;###autoload
@ -83,7 +84,7 @@ of a visual interface."
(unless (fboundp 'emoji--command-Emoji)
(emoji--define-transient))
(funcall (emoji--define-transient
(cons "Recent" emoji--recent) t)))
(cons "Recent" (multisession-value emoji--recent)) t)))
;;;###autoload
(defun emoji-search ()
@ -529,15 +530,18 @@ character) under point is."
(lambda ()
(interactive)
(funcall (emoji--define-transient
(cons "Recent" emoji--recent) t end-function))))
(cons "Recent" (multisession-value emoji--recent))
t end-function))))
(defun emoji--add-recent (glyph)
"Add GLYPH to the set of recently used emojis."
(setq emoji--recent (delete glyph emoji--recent))
(push glyph emoji--recent)
;; Shorten the list.
(when-let ((tail (nthcdr 30 emoji--recent)))
(setcdr tail nil)))
(let ((recent (multisession-value emoji--recent)))
(setq recent (delete glyph recent))
(push glyph recent)
;; Shorten the list.
(when-let ((tail (nthcdr 30 recent)))
(setcdr tail nil))
(setf (multisession-value emoji--recent) recent)))
(defun emoji--columnize (list columns)
"Split LIST into COLUMN columns."

View file

@ -283,6 +283,7 @@ buffers (yet) -- that will be amended in a future version.
The `outline-minor-mode-buttons' variable specifies how the
buttons should look."
:type 'boolean
:safe #'booleanp
:version "29.1")
(defcustom outline-minor-mode-buttons
@ -376,8 +377,8 @@ When point is on a heading line, then typing `TAB' cycles between `hide all',
a heading line cycles the whole buffer (`outline-cycle-buffer').
Typing these keys anywhere outside heading lines uses their default bindings."
:type 'boolean
:safe #'booleanp
:version "28.1")
;;;###autoload(put 'outline-minor-mode-cycle 'safe-local-variable 'booleanp)
(defcustom outline-minor-mode-highlight nil
"Highlight headings in `outline-minor-mode' using font-lock keywords.
@ -391,8 +392,8 @@ faces to major mode's faces."
(const :tag "Overwrite major mode faces" override)
(const :tag "Append outline faces to major mode faces" append)
(const :tag "Highlight separately from major mode faces" t))
:safe #'symbolp
:version "28.1")
;;;###autoload(put 'outline-minor-mode-highlight 'safe-local-variable 'symbolp)
(defun outline-minor-mode-highlight-buffer ()
;; Fallback to overlays when font-lock is unsupported.

View file

@ -181,30 +181,24 @@ Effective only if `hide-ifdef-expand-reinclusion-guard' is t."
:type 'regexp
:version "25.1")
(defvar hide-ifdef-mode-submap
(defvar-keymap hide-ifdef-mode-submap
:doc "Keymap used by `hide-ifdef-mode' under `hide-ifdef-mode-prefix-key'."
;; Set up the submap that goes after the prefix key.
(let ((map (make-sparse-keymap)))
(define-key map "d" 'hide-ifdef-define)
(define-key map "u" 'hide-ifdef-undef)
(define-key map "D" 'hide-ifdef-set-define-alist)
(define-key map "U" 'hide-ifdef-use-define-alist)
(define-key map "h" 'hide-ifdefs)
(define-key map "s" 'show-ifdefs)
(define-key map "\C-d" 'hide-ifdef-block)
(define-key map "\C-s" 'show-ifdef-block)
(define-key map "e" 'hif-evaluate-macro)
(define-key map "C" 'hif-clear-all-ifdef-defined)
(define-key map "\C-q" 'hide-ifdef-toggle-read-only)
(define-key map "\C-w" 'hide-ifdef-toggle-shadowing)
(substitute-key-definition
'read-only-mode 'hide-ifdef-toggle-outside-read-only map)
;; `toggle-read-only' is obsoleted by `read-only-mode'.
(substitute-key-definition
'toggle-read-only 'hide-ifdef-toggle-outside-read-only map)
map)
"Keymap used by `hide-ifdef-mode' under `hide-ifdef-mode-prefix-key'.")
"d" #'hide-ifdef-define
"u" #'hide-ifdef-undef
"D" #'hide-ifdef-set-define-alist
"U" #'hide-ifdef-use-define-alist
"h" #'hide-ifdefs
"s" #'show-ifdefs
"C-d" #'hide-ifdef-block
"C-s" #'show-ifdef-block
"e" #'hif-evaluate-macro
"C" #'hif-clear-all-ifdef-defined
"C-q" #'hide-ifdef-toggle-read-only
"C-w" #'hide-ifdef-toggle-shadowing
"<remap> <read-only-mode>" #'hide-ifdef-toggle-outside-read-only
;; `toggle-read-only' is obsoleted by `read-only-mode'.
"<remap> <toggle-read-only>" #'hide-ifdef-toggle-outside-read-only)
(defcustom hide-ifdef-mode-prefix-key "\C-c@"
"Prefix key for all Hide-Ifdef mode commands."

View file

@ -227,12 +227,6 @@ Used for listing local printers or renamed cells.")
"w" ses-set-column-width
"x" ses-export-keymap
"\M-p" ses-read-column-printer))
(repl '(;;We'll replace these wherever they appear in the keymap
clipboard-kill-region ses-kill-override
end-of-line ses-end-of-line
kill-line ses-delete-row
kill-region ses-kill-override
open-line ses-insert-row))
(numeric "0123456789.-")
(newmap (make-keymap)))
;;Get rid of printables
@ -240,13 +234,11 @@ Used for listing local printers or renamed cells.")
;;These keys insert themselves as the beginning of a numeric value
(dotimes (x (length numeric))
(define-key newmap (substring numeric x (1+ x)) 'ses-read-cell))
;;Override these global functions wherever they're bound
(while repl
(substitute-key-definition (car repl) (cadr repl) newmap
(current-global-map))
(setq repl (cddr repl)))
;;Apparently substitute-key-definition doesn't catch this?
(define-key newmap [(menu-bar) edit cut] 'ses-kill-override)
(define-key newmap [remap clipboard-kill-region] #'ses-kill-override)
(define-key newmap [remap end-of-line] #'ses-end-of-line)
(define-key newmap [remap kill-line] #'ses-delete-row)
(define-key newmap [remap kill-region] #'ses-kill-override)
(define-key newmap [remap open-line] #'ses-insert-row)
;;Define our other local keys
(while keys
(define-key newmap (car keys) (cadr keys))

View file

@ -70,12 +70,16 @@
("tabwindow" ?f nil nil 1)))
(rotating "Sidewaysfigure and table"
(("sidewaysfigure" ?f nil nil caption)
("sidewaystable" ?t nil nil caption)))
(("sidewaysfigure" ?f nil nil caption)
("sidewaysfigure*" ?f nil nil caption)
("sidewaystable" ?t nil nil caption)
("sidewaystable*" ?t nil nil caption)))
(sidecap "CSfigure and SCtable"
(("SCfigure" ?f nil nil caption)
("SCtable" ?t nil nil caption)))
(sidecap "SCfigure and SCtable"
(("SCfigure" ?f nil nil caption)
("SCfigure*" ?f nil nil caption)
("SCtable" ?t nil nil caption)
("SCtable*" ?t nil nil caption)))
(subfigure "Subfigure environments/macro"
(("subfigure" ?f nil nil caption)

View file

@ -521,7 +521,25 @@ to invocation.")
(erase-buffer)
(ediff-set-help-message)
(insert ediff-help-message)
(shrink-window-if-larger-than-buffer)
;; With the fix for Bug#49277 and an 'ediff-setup-windows-plain'
;; layout, the window of the control buffer we want to adjust here
;; is no longer the lower of two windows on their frame both showing
;; that control buffer but rather the bottom-most window in the
;; established ediff layout for that frame. As a consequence,
;; 'shrink-window-if-larger-than-buffer' will fail to show the whole
;; buffer with 'ediff-toggle-help' because that window's maximum
;; height is not half the height of its frame but the height of the
;; control buffer's window in the established layout (Bug#52504).
;;
;; The form below is an attempt to emulate the behavior of Emacs 27
;; as faithfully as possible in this regard (the use of 'ceiling'
;; mimics the behavior of 'split-window' giving the lower window the
;; residue line when the window to split has an uneven number of
;; lines).
(when (and (window-combined-p)
(pos-visible-in-window-p (point-min)))
(fit-window-to-buffer
nil (ceiling (/ (window-total-height (frame-root-window)) 2.0))))
(or (ediff-multiframe-setup-p)
(ediff-indent-help-message))
(ediff-set-help-overlays)

View file

@ -5278,16 +5278,16 @@ LATE_LOAD has to be non-nil when loading for deferred compilation. */)
Fmake_temp_file_internal (filename, Qnil, build_string (".eln.tmp"),
Qnil);
if (NILP (Ffile_writable_p (tmp_filename)))
comp_u->handle = dynlib_open (SSDATA (encoded_filename));
comp_u->handle = dynlib_open_for_eln (SSDATA (encoded_filename));
else
{
Frename_file (filename, tmp_filename, Qt);
comp_u->handle = dynlib_open (SSDATA (ENCODE_FILE (tmp_filename)));
comp_u->handle = dynlib_open_for_eln (SSDATA (ENCODE_FILE (tmp_filename)));
Frename_file (tmp_filename, filename, Qnil);
}
}
else
comp_u->handle = dynlib_open (SSDATA (encoded_filename));
comp_u->handle = dynlib_open_for_eln (SSDATA (encoded_filename));
if (!comp_u->handle)
xsignal2 (Qnative_lisp_load_failed, filename,

View file

@ -104,6 +104,12 @@ dynlib_open (const char *dll_fname)
return (dynlib_handle_ptr) hdll;
}
dynlib_handle_ptr
dynlib_open_for_eln (const char *dll_fname)
{
return dynlib_open (dll_fname);
}
void *
dynlib_sym (dynlib_handle_ptr h, const char *sym)
{
@ -269,6 +275,12 @@ dynlib_close (dynlib_handle_ptr h)
dynlib_handle_ptr
dynlib_open (const char *path)
{
return dlopen (path, RTLD_LAZY | RTLD_GLOBAL);
}
dynlib_handle_ptr
dynlib_open_for_eln (const char *path)
{
return dlopen (path, RTLD_LAZY);
}

View file

@ -24,6 +24,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
typedef void *dynlib_handle_ptr;
dynlib_handle_ptr dynlib_open (const char *path);
dynlib_handle_ptr dynlib_open_for_eln (const char *path);
int dynlib_close (dynlib_handle_ptr h);
const char *dynlib_error (void);

View file

@ -5353,7 +5353,7 @@ dump_do_dump_relocation (const uintptr_t dump_base,
their file names through expand-file-name and
decode-coding-string. */
comp_u->file = eln_fname;
comp_u->handle = dynlib_open (SSDATA (eln_fname));
comp_u->handle = dynlib_open_for_eln (SSDATA (eln_fname));
if (!comp_u->handle)
{
fprintf (stderr, "Error using execdir %s:\n",

View file

@ -149,6 +149,8 @@ fails. */)
if (!EQ (type, Qwebkit))
error ("Bad xwidget type");
Frequire (Qxwidget, Qnil, Qnil);
struct xwidget *xw = allocate_xwidget ();
Lisp_Object val;
xw->type = type;

View file

@ -0,0 +1,201 @@
;;; multisession-tests.el --- Tests for multisession.el -*- lexical-binding: t; -*-
;; Copyright (C) 2021 Free Software Foundation, Inc.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(require 'multisession)
(require 'ert)
(require 'ert-x)
(require 'cl-lib)
(ert-deftest multi-test-sqlite-simple ()
(skip-unless (sqlite-available-p))
(ert-with-temp-file dir
:directory t
(let ((user-init-file "/tmp/foo.el")
(multisession-storage 'sqlite)
(multisession-directory dir))
(unwind-protect
(progn
(define-multisession-variable foo 0
""
:synchronized t)
(should (= (multisession-value foo) 0))
(cl-incf (multisession-value foo))
(should (= (multisession-value foo) 1))
(call-process
(concat invocation-directory invocation-name)
nil t nil
"-Q" "-batch"
"--eval" (prin1-to-string
`(progn
(require 'multisession)
(let ((multisession-directory ,dir)
(multisession-storage 'sqlite)
(user-init-file "/tmp/foo.el"))
(define-multisession-variable foo 0
""
:synchronized t)
(cl-incf (multisession-value foo))))))
(should (= (multisession-value foo) 2)))
(sqlite-close multisession--db)
(setq multisession--db nil)))))
(ert-deftest multi-test-sqlite-busy ()
(skip-unless (and t (sqlite-available-p)))
(ert-with-temp-file dir
:directory t
(let ((user-init-file "/tmp/foo.el")
(multisession-directory dir)
(multisession-storage 'sqlite)
proc)
(unwind-protect
(progn
(define-multisession-variable bar 0
""
:synchronized t)
(should (= (multisession-value bar) 0))
(cl-incf (multisession-value bar))
(should (= (multisession-value bar) 1))
(setq proc
(start-process
"other-emacs"
nil
(concat invocation-directory invocation-name)
"-Q" "-batch"
"--eval" (prin1-to-string
`(progn
(require 'multisession)
(let ((multisession-directory ,dir)
(multisession-storage 'sqlite)
(user-init-file "/tmp/bar.el"))
(define-multisession-variable bar 0
"" :synchronized t)
(dotimes (i 100)
(cl-incf (multisession-value bar))))))))
(while (process-live-p proc)
(ignore-error 'sqlite-locked-error
(message "bar %s" (multisession-value bar))
;;(cl-incf (multisession-value bar))
)
(sleep-for 0.1))
(message "bar ends up as %s" (multisession-value bar))
(should (< (multisession-value bar) 1003)))
(sqlite-close multisession--db)
(setq multisession--db nil)))))
(ert-deftest multi-test-files-simple ()
(ert-with-temp-file dir
:directory t
(let ((user-init-file "/tmp/sfoo.el")
(multisession-storage 'files)
(multisession-directory dir))
(define-multisession-variable sfoo 0
""
:synchronized t)
(should (= (multisession-value sfoo) 0))
(cl-incf (multisession-value sfoo))
(should (= (multisession-value sfoo) 1))
(call-process
(concat invocation-directory invocation-name)
nil t nil
"-Q" "-batch"
"--eval" (prin1-to-string
`(progn
(require 'multisession)
(let ((multisession-directory ,dir)
(multisession-storage 'files)
(user-init-file "/tmp/sfoo.el"))
(define-multisession-variable sfoo 0
""
:synchronized t)
(cl-incf (multisession-value sfoo))))))
(should (= (multisession-value sfoo) 2)))))
(ert-deftest multi-test-files-busy ()
(skip-unless (and t (sqlite-available-p)))
(ert-with-temp-file dir
:directory t
(let ((user-init-file "/tmp/foo.el")
(multisession-storage 'files)
(multisession-directory dir)
proc)
(define-multisession-variable sbar 0
""
:synchronized t)
(should (= (multisession-value sbar) 0))
(cl-incf (multisession-value sbar))
(should (= (multisession-value sbar) 1))
(setq proc
(start-process
"other-emacs"
nil
(concat invocation-directory invocation-name)
"-Q" "-batch"
"--eval" (prin1-to-string
`(progn
(require 'multisession)
(let ((multisession-directory ,dir)
(multisession-storage 'files)
(user-init-file "/tmp/sbar.el"))
(define-multisession-variable sbar 0
"" :synchronized t)
(dotimes (i 1000)
(cl-incf (multisession-value sbar))))))))
(while (process-live-p proc)
(message "sbar %s" (multisession-value sbar))
;;(cl-incf (multisession-value sbar))
(sleep-for 0.1))
(message "sbar ends up as %s" (multisession-value sbar))
(should (< (multisession-value sbar) 2000)))))
(ert-deftest multi-test-files-some-values ()
(ert-with-temp-file dir
:directory t
(let ((user-init-file "/tmp/sfoo.el")
(multisession-storage 'files)
(multisession-directory dir))
(define-multisession-variable foo1 nil)
(should (eq (multisession-value foo1) nil))
(setf (multisession-value foo1) nil)
(should (eq (multisession-value foo1) nil))
(setf (multisession-value foo1) t)
(should (eq (multisession-value foo1) t))
(define-multisession-variable foo2 t)
(setf (multisession-value foo2) nil)
(should (eq (multisession-value foo2) nil))
(setf (multisession-value foo2) t)
(should (eq (multisession-value foo2) t))
(define-multisession-variable foo3 t)
(should-error (setf (multisession-value foo3) (make-marker)))
(let ((string (with-temp-buffer
(set-buffer-multibyte nil)
(insert 0 1 2)
(buffer-string))))
(should-not (multibyte-string-p string))
(define-multisession-variable foo4 nil)
(setf (multisession-value foo4) string)
(should (equal (multisession-value foo4) string))))))
;;; multisession-tests.el ends here

View file

@ -184,6 +184,7 @@
(ert-deftest sqlite-load-extension ()
(skip-unless (sqlite-available-p))
(skip-unless (fboundp 'sqlite-load-extension))
(let (db)
(setq db (sqlite-open))
(should-error