YRY3LNWJB2A5JZU3PB6PAMP765F6SSVHLUL6U7SLC74QEXHHQI5QC
# * test.yml --- Test Emacs packages using makem.sh on GitHub Actions
# URL: https://github.com/alphapapa/makem.sh
# Version: 0.4.2
# * Commentary:
# Based on Steve Purcell's examples at
# <https://github.com/purcell/setup-emacs/blob/master/.github/workflows/test.yml>,
# <https://github.com/purcell/package-lint/blob/master/.github/workflows/test.yml>.
# * License:
# This program 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.
# This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
# * Code:
name: "CI"
on:
pull_request:
push:
# Comment out this section to enable testing of all branches.
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
emacs_version:
- 26.3
- 27.1
- 28.2
- snapshot
steps:
- uses: purcell/setup-emacs@master
with:
version: ${{ matrix.emacs_version }}
- uses: actions/checkout@v2
- name: Initialize sandbox
run: |
SANDBOX_DIR=$(mktemp -d) || exit 1
echo "SANDBOX_DIR=$SANDBOX_DIR" >> $GITHUB_ENV
./makem.sh -vv --sandbox=$SANDBOX_DIR --install-deps --install-linters
# The "all" rule is not used, because it treats compilation warnings
# as failures, so linting and testing are run as separate steps.
- name: Lint
# NOTE: Uncomment this line to treat lint failures as passing
# so the job doesn't show failure.
# continue-on-error: true
run: ./makem.sh -vv --sandbox=$SANDBOX_DIR lint
- name: Test
if: always() # Run test even if linting fails.
run: ./makem.sh -vv --sandbox=$SANDBOX_DIR test
# Local Variables:
# eval: (outline-minor-mode)
# End:
━━━━━━━━━━
EMENT.EL
━━━━━━━━━━
[https://elpa.gnu.org/packages/ement.svg]
Ement.el is a Matrix client for Emacs. It aims to be simple, fast,
featureful, and reliable.
Feel free to join us in the chat room:
[https://img.shields.io/matrix/ement.el:matrix.org.svg?label=%23ement.el:matrix.org]
[https://elpa.gnu.org/packages/ement.svg]
<https://elpa.gnu.org/packages/ement.html>
[https://img.shields.io/matrix/ement.el:matrix.org.svg?label=%23ement.el:matrix.org]
<https://matrix.to/#/#ement.el:matrix.org>
1 Installation
══════════════
1.1 GNU ELPA
────────────
Ement.el is published in [GNU ELPA], so it may be installed in Emacs
with the command `M-x package-install RET ement RET'. This is the
recommended way to install Ement.el, as it will install the current
stable release.
[GNU ELPA] <http://elpa.gnu.org/>
1.2 GNU Guix
────────────
Ement.el is also available in [GNU Guix] as `emacs-ement'.
[GNU Guix] <https://guix.gnu.org/>
1.3 Debian
──────────
Ement.el is also available in Debian as [elpa-ement].
[elpa-ement] <https://packages.debian.org/elpa-ement>
1.4 Git master
──────────────
The `master' branch of the Git repository is intended to be usable at
all times; only minor bugs are expected to be found in it before a new
stable release is made. To install from this, it is recommended to
use [quelpa-use-package], like this:
┌────
│ ;; Install and load `quelpa-use-package'.
│ (package-install 'quelpa-use-package)
│ (require 'quelpa-use-package)
│
│ ;; Install Ement.
│ (use-package ement
│ :quelpa (ement :fetcher github :repo "alphapapa/ement.el"))
└────
One might also use systems like [Straight] (which is also used by
[DOOM]) to install from Git, but the author cannot offer support for
them.
[quelpa-use-package] <https://github.com/quelpa/quelpa-use-package>
[Straight] <https://github.com/radian-software/straight.el>
[DOOM] <https://github.com/doomemacs/doomemacs>
1.5 Manual
──────────
Ement.el is intended to be installed with Emacs's package system,
which will ensure that the required autoloads are generated, etc. If
you choose to install it manually, you're on your own.
2 Usage
═══════
•
•
•
1. Call command `ement-connect' to connect. Multiple sessions are
supported, so you may call the command again to connect to another
account.
2. Wait for initial sync to complete (which can take a few
moments–initial sync JSON requests can be large).
3. Use these commands (room-related commands may be called with
universal prefix to prompt for the room):
• `ement-list-rooms' to view the list of joined rooms.
• `ement-view-room' to view a room's buffer, selected with
completion.
• `ement-create-room' to create a new room.
• `ement-create-space' to create a space.
• `ement-invite-user' to invite a user to a room.
• `ement-join-room' to join a room.
• `ement-leave-room' to leave a room.
• `ement-forget-room' to forget a room.
• `ement-tag-room' to toggle a tag on a room (including
favorite/low-priority status).
• `ement-list-members' to list members in a room.
• `ement-send-direct-message' to send a direct message to a user
(in an existing direct room, or creating a new one
automatically).
• `ement-room-edit-message' to edit a message at point.
• `ement-room-send-file' to send a file.
• `ement-room-send-image' to send an image.
• `ement-room-set-topic' to set a room's topic.
• `ement-room-occur' to search in a room's known events.
• `ement-room-override-name' to override a room's display name.
• `ement-ignore-user' to ignore a user (or with interactive prefix,
un-ignore).
• `ement-room-set-message-format' to set a room's message format
buffer-locally.
• `ement-room-toggle-space' to toggle a room's membership in a
space (a way to group rooms in Matrix).
• `ement-directory' to view a room directory.
• `ement-directory-search' to search a room directory.
4. Use these special buffers to see events from multiple rooms (you
can also reply to messages from these buffers!):
• See all new events that mention you in the `*Ement Mentions*'
buffer.
• See all new events in rooms that have open buffers in the `*Ement
Notifications*' buffer.
2.1 Bindings
────────────
These bindings are common to all of the following buffer types:
⁃ Switch to a room buffer: `M-g M-r'
⁃ Switch to the room list buffer: `M-g M-l'
⁃ Switch to the mentions buffer: `M-g M-m'
⁃ Switch to the notifications buffer: `M-g M-n'
2.1.1 Room buffers
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
⁃ Show command menu: `?'
*Movement*
⁃ Next event: `TAB'
⁃ Previous event: `<backtab>'
⁃ Scroll up and mark read: `SPC'
⁃ Scroll down: `S-SPC'
⁃ Jump to fully-read marker: `M-SPC'
⁃ Load older messages: at top of buffer, scroll contents up
(i.e. `S-SPC', `M-v' or `mwheel-scroll')
*Switching*
⁃ List rooms: `M-g M-l'
⁃ Switch to other room: `M-g M-r'
⁃ Switch to mentions buffer: `M-g M-m'
⁃ Switch to notifications buffer: `M-g M-n'
⁃ Quit window: `q'
*Messages*
⁃ Write message: `RET'
⁃ Write reply to event at point (when region is active, only quote
marked text) : `S-RET'
⁃ Compose message in buffer: `M-RET' (while writing in minibuffer:
`C-c ')' (Use command `ement-room-compose-org' to activate Org mode
in the compose buffer.)
⁃ Edit message: `<insert>'
⁃ Delete message: `C-k'
⁃ Send reaction to event at point, or send same reaction at point: `s
r'
⁃ Send emote: `s e'
⁃ Send file: `s f'
⁃ Send image: `s i'
⁃ View event source: `v'
⁃ Complete members and rooms at point: `C-M-i' (standard
`completion-at-point' command). (Type an `@' prefix for a member
mention, a `#' prefix for a room alias, or a `!' prefix for a room
ID.)
*Images*
⁃ Toggle scale of image (between fit-to-window and thumbnail):
`mouse-1'
⁃ Show image in new buffer at full size: `double-mouse-1'
*Users*
⁃ Send direct message: `u RET'
⁃ Invite user: `u i'
⁃ Ignore user: `u I'
*Room*
⁃ Occur search in room: `M-s o'
⁃ List members: `r m'
⁃ Set topic: `r t'
⁃ Set message format: `r f'
⁃ Set notification rules: `r n'
⁃ Override display name: `r N'
⁃ Tag/untag room: `r T'
*Room membership*
⁃ Create room: `R c'
⁃ Join room: `R j'
⁃ Leave room: `R l'
⁃ Forget room: `R F'
⁃ Toggle room's spaces: `R s'
*Other*
⁃ Sync new messages (not necessary if auto sync is enabled; with
prefix to force new sync): `g'
2.1.2 Room list buffer
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
⁃ Show buffer of room at point: `RET'
⁃ Show buffer of next unread room: `SPC'
⁃ Move between room names: `TAB' / `<backtab>'
⁃ Kill room's buffer: `k'
⁃ Toggle room's membership in a space: `s'
2.1.3 Directory buffers
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
⁃ View/join a room: `RET' / `mouse-1'
⁃ Load next batch of rooms: `+'
2.1.4 Mentions/notifications buffers
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
⁃ Move between events: `TAB' / `<backtab>'
⁃ Go to event at point in its room buffer: `RET'
⁃ Write reply to event at point (shows the event in its room while
writing) : `S-RET'
2.2 Tips
────────
⁃ Desktop notifications are enabled by default for events that mention
the local user. They can also be shown for all events in rooms with
open buffers.
⁃ Send messages in Org mode format by customizing the option
`ement-room-send-message-filter' (which enables Org format by
default), or by calling `ement-room-compose-org' in a compose buffer
(which enables it for a single message). Then Org-formatted
messages are automatically converted and sent as HTML-formatted
messages (with the Org syntax as the plain-text fallback). You can
send syntax such as:
• Bold, italic, underline, strikethrough
• Links
• Tables
• Source blocks (including results with `:exports both')
• Footnotes (okay, that might be pushing it, but you can!)
• And, generally, anything that Org can export to HTML
⁃ Starting in the room list buffer, by pressing `SPC' repeatedly, you
can cycle through and read all rooms with unread buffers. (If a
room doesn't have a buffer, it will not be included.)
⁃ Room buffers and the room-list buffer can be bookmarked in Emacs,
i.e. using `C-x r m'. This is especially useful with [Burly]: you
can arrange an Emacs frame with several room buffers displayed at
once, use `burly-bookmark-windows' to bookmark the layout, and then
you can restore that layout and all of the room buffers by opening
the bookmark, rather than having to manually arrange them every time
you start Emacs or change the window configuration.
⁃ Images and other files can be uploaded to rooms using drag-and-drop.
⁃ Mention members by typing a `@' followed by their displayname or
Matrix ID. (Members' names and rooms' aliases/IDs may be completed
with `completion-at-point' commands.)
⁃ You can customize settings in the `ement' group.
• *Note:* `setq' should not be used for certain options, because it
will not call the associated setter function. Users who have an
aversion to the customization system may experience problems.
[Burly] <https://github.com/alphapapa/burly.el>
2.2.1 Displaying symbols and emojis
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
Emacs may not display certain symbols and emojis well by default.
Based on [this question and answer], you may find that the simplest
way to fix this is to install an appropriate font, like [Noto Emoji],
and then use this Elisp code:
┌────
│ (setf use-default-font-for-symbols nil)
│ (set-fontset-font t 'unicode "Noto Emoji" nil 'append)
└────
[this question and answer]
<https://emacs.stackexchange.com/questions/62049/override-the-default-font-for-emoji-characters>
[Noto Emoji] <https://www.google.com/get/noto/#emoji-zsye>
2.3 Encrypted room support through Pantalaimon
──────────────────────────────────────────────
Ement.el doesn't support encrypted rooms natively, but it can be used
transparently with the E2EE-aware reverse proxy daemon [Pantalaimon].
After configuring it according to its documentation, call
`ement-connect' with the appropriate hostname and port, like:
┌────
│ (ement-connect :uri-prefix "http://localhost:8009")
└────
[Pantalaimon] <https://github.com/matrix-org/pantalaimon/>
3 Rationale
═══════════
Why write a new Emacs Matrix client when there is already
[matrix-client.el], by the same author, no less? A few reasons:
• `matrix-client' uses an older version of the Matrix spec, r0.3.0,
with a few elements of r0.4.0 grafted in. Bringing it up to date
with the current version of the spec, r0.6.1, would be more work
than to begin with the current version. Ement.el targets r0.6.1
from the beginning.
• `matrix-client' does not use Matrix's lazy-loading feature (which
was added to the specification later), so initial sync requests can
take a long time for the server to process and can be large
(sometimes tens of megabytes of JSON for the client to process!).
Ement.el uses lazy-loading, which significantly improves
performance.
• `matrix-client' automatically makes buffers for every room a user
has joined, even if the user doesn't currently want to watch a room.
Ement.el opens room buffers on-demand, improving performance by not
having to insert events into buffers for rooms the user isn't
watching.
• `matrix-client' was developed without the intention of publishing it
to, e.g. MELPA or ELPA. It has several dependencies, and its code
does not always install or compile cleanly due to macro-expansion
issues (apparently depending on the user's Emacs config). Ement.el
is designed to have minimal dependencies outside of Emacs (currently
only one, `plz', which could be imported into the project), and
every file is linted and compiles cleanly using [makem.sh].
• `matrix-client' uses EIEIO, probably unnecessarily, since few, if
any, of the benefits of EIEIO are realized in it. Ement.el uses
structs instead.
• `matrix-client' uses bespoke code for inserting messages into
buffers, which works pretty well, but has a few minor bugs which are
difficult to track down. Ement.el uses Emacs's built-in (and
perhaps little-known) `ewoc' library, which makes it much simpler
and more reliable to insert and update messages in buffers, and
enables the development of advanced UI features more easily.
• `matrix-client' was, to a certain extent, designed to imitate other
messaging apps. The result is, at least when used with the
`matrix-client-frame' command, fairly pleasing to use, but isn't
especially "Emacsy." Ement.el is intended to better fit into
Emacs's paradigms.
• `matrix-client''s long name makes for long symbol names, which makes
for tedious, verbose code. `ement' is easy to type and makes for
concise, readable code.
• The author has learned much since writing `matrix-client' and hopes
to write simpler, more readable, more maintainable code in Ement.el.
It's hoped that this will enable others to contribute more easily.
Note that, while `matrix-client' remains usable, and probably will for
some time to come, Ement.el has now surpassed it in every way. The
only reason to choose `matrix-client' instead is if one is using an
older version of Emacs that isn't supported by Ement.el.
[matrix-client.el] <https://github.com/alphapapa/matrix-client.el>
[makem.sh] <https://github.com/alphapapa/makem.sh>
4 Changelog
═══════════
4.1 0.9.2
─────────
*Fixes*
⁃ Restore position in room list when refreshing.
⁃ Completion in minibuffer.
4.2 0.9.1
─────────
*Fixes*
⁃ Error in `ement-room-list' command upon initial sync.
4.3 0.9
───────
*Additions*
⁃ Option `ement-room-timestamp-header-align' controls how timestamp
headers are aligned in room buffers.
⁃ Option `ement-room-view-hook' runs functions when `ement-room-view'
is called. (By default, it refreshes the room list buffer.)
⁃ In the room list, middle-clicking a room which has a buffer closes
its buffer.
⁃ Basic support for video events. (Thanks to [Arto Jantunen].)
*Changes*
⁃ Using new option `ement-room-timestamp-header-align', timestamp
headers default to right-aligned. (With default settings, this
keeps them near message timestamps and makes for a cleaner
appearance.)
*Fixes*
⁃ Recognition of certain MXID or displayname forms in outgoing
messages when linkifying (aka "pilling") them.
⁃ Unreadable room avatar images no longer cause errors. (Fixes
[#147]. Thanks to [@jgarte] for reporting.)
⁃ Don't error in `ement-room-list' when no rooms are joined. (Fixes
[#123]. Thanks to [@Kabouik] and [Omar Antolín Camarena] for
reporting.)
⁃ Enable member/room completion in compose buffers. (Fixes [#115].
Thanks to Thanks to [Justus Piater] and [Caleb Chase] for
reporting.)
[Arto Jantunen] <https://github.com/viiru->
[#147] <https://github.com/alphapapa/ement.el/issues/147>
[@jgarte] <https://github.com/jgarte>
[#123] <https://github.com/alphapapa/ement.el/issues/123>
[@Kabouik] <https://github.com/Kabouik>
[Omar Antolín Camarena] <https://github.com/oantolin>
[#115] <https://github.com/alphapapa/ement.el/issues/115>
[Justus Piater] <https://github.com/piater>
[Caleb Chase] <https://github.com/chasecaleb>
4.4 0.8.3
─────────
*Fixes*
⁃ Avoid use of `pcase''s `(map :KEYWORD)' form. (This can cause a
broken installation on older versions of Emacs that have an older
version of the `map' library loaded, such as Emacs 27.2 included in
Debian 11. Since there's no way to force Emacs to actually load the
version of `map' required by this package before installing it
(which would naturally happen upon restarting Emacs), we can only
avoid using such forms while these versions of Emacs are widely
used.)
4.5 0.8.2
─────────
*Fixes*
⁃ Deduplicate grouped membership events.
4.6 0.8.1
─────────
Added missing changelog entry (of course).
4.7 0.8
───────
*Additions*
⁃ Command `ement-create-space' creates a new space.
⁃ Command `ement-room-toggle-space' toggles a room's membership in a
space (a way to group rooms in Matrix).
⁃ Visibility of sections in the room list is saved across sessions.
⁃ Command `ement-room-list-kill-buffer' kills a room's buffer from the
room list.
⁃ Set `device_id' and `initial_device_display_name' upon login
(e.g. `Ement.el: username@hostname'). ([#134]. Thanks to [Arto
Jantunen] for reporting.)
*Changes*
⁃ Room-related commands may be called interactively with a universal
prefix to prompt for the room/session (allowing to send events or
change settings in rooms other than the current one).
⁃ Command `ement-room-list' reuses an existing window showing the room
list when possible. ([#131]. Thanks to [Jeff Bowman] for
suggesting.)
⁃ Command `ement-tag-room' toggles tags (rather than adding by default
and removing when called with a prefix).
⁃ Default room grouping now groups "spaced" rooms separately.
*Fixes*
⁃ Message format filter works properly when writing replies.
⁃ Improve insertion of sender name headers when using the "Elemental"
message format.
⁃ Prompts in commands `ement-leave-room' and `ement-forget-room'.
[#134] <https://github.com/alphapapa/ement.el/issues/134>
[Arto Jantunen] <https://github.com/viiru->
[#131] <https://github.com/alphapapa/ement.el/issues/131>
[Jeff Bowman] <https://github.com/jeffbowman>
4.8 0.7
───────
*Additions*
⁃ Command `ement-room-override-name' sets a local override for a
room's display name. (Especially helpful for 1:1 rooms and bridged
rooms. See [MSC3015].)
*Changes*
⁃ Improve display of room tombstones (displayed at top and bottom of
buffer, and new room ID is linked to join).
⁃ Use descriptive prompts in `ement-leave-room' and
`ement-forget-room' commands.
*Fixes*
⁃ Command `ement-view-space' when called from a room buffer. (Thanks
to [Richard Brežák] for reporting.)
⁃ Don't call `display-buffer' when reverting room list buffer. (Fixes
[#121]. Thanks to [mekeor] for reporting.)
⁃ Retry sync for network timeouts. (Accidentally broken in v0.6.)
*Internal*
⁃ Function `ement-put-account-data' accepts `:room' argument to put on
a room's account data.
[MSC3015]
<https://github.com/matrix-org/matrix-spec-proposals/pull/3015#issuecomment-1451017296>
[Richard Brežák] <https://github.com/MagicRB>
[#121] <https://github.com/alphapapa/ement.el/issues/121>
[mekeor] <https://github.com/mekeor>
4.9 0.6
───────
*Additions*
⁃ Command `ement-view-space' to view a space's rooms in a directory
buffer.
*Changes*
⁃ Improve `ement-describe-room' command (formatting, bindings).
*Fixes*
⁃ Retry sync for HTTP 502 "Bad Gateway" errors.
⁃ Formatting of unban events.
⁃ Update password authentication according to newer Matrix spec.
(Fixes compatibility with Conduit servers. [#66]. Thanks to
[Travis Peacock], [Arto Jantunen], and [Stephen D].)
⁃ Image scaling issues. (Thanks to [Visuwesh].)
[#66] <https://github.com/alphapapa/ement.el/issues/66>
[Travis Peacock] <https://github.com/tpeacock19>
[Arto Jantunen] <https://github.com/viiru->
[Stephen D] <https://github.com/scd31>
[Visuwesh] <https://github.com/vizs>
4.10 0.5.2
──────────
*Fixes*
⁃ Apply `ement-initial-sync-timeout' properly (important for when the
homeserver is slow to respond).
4.11 0.5.1
──────────
*Fixes*
⁃ Autoload `ement-directory' commands.
⁃ Faces in `ement-directory' listings.
4.12 0.5
────────
*Additions*
⁃ Present "joined-and-left" and "rejoined-and-left" membership event
pairs as such.
⁃ Process and show rooms' canonical alias events.
*Changes*
⁃ The [taxy.el]-based room list, with programmable, smart grouping, is
now the default `ement-room-list'. (The old,
`tabulated-list-mode'-based room list is available as
`ement-tabulated-room-list'.)
⁃ When selecting a room to view with completion, don't offer spaces.
⁃ When selecting a room with completion, empty aliases and topics are
omitted instead of being displayed as nil.
*Fixes*
⁃ Use of send-message filter when replying.
⁃ Replies may be written in compose buffers.
[taxy.el] <https://github.com/alphapapa/taxy.el>
4.13 0.4.1
──────────
*Fixes*
⁃ Don't show "curl process interrupted" message when updating a read
marker's position again.
4.14 0.4
────────
*Additions*
⁃ Option `ement-room-unread-only-counts-notifications', now enabled by
default, causes rooms' unread status to be determined only by their
notification counts (which are set by the server and depend on
rooms' notification settings).
⁃ Command `ement-room-set-notification-state' sets a room's
notification state (imitating Element's user-friendly presets).
⁃ Room buffers' Transient menus show the room's notification state
(imitating Element's user-friendly presets).
⁃ Command `ement-set-display-name' sets the user's global displayname.
⁃ Command `ement-room-set-display-name' sets the user's displayname in
a room (which is also now displayed in the room's Transient menu).
⁃ Column `Notifications' in the `ement-taxy-room-list' buffer shows
rooms' notification state.
⁃ Option `ement-interrupted-sync-hook' allows customization of how
sync interruptions are handled. (Now, by default, a warning is
displayed instead of merely a message.)
*Changes*
⁃ When a room's read receipt is updated, the room's buffer is also
marked as unmodified. (In concert with the new option, this makes
rooms' unread status more intuitive.)
*Fixes*
⁃ Binding of command `ement-forget-room' in room buffers.
⁃ Highlighting of `@room' mentions.
4.15 0.3.1
──────────
*Fixes*
⁃ Room unread status (when the last event in a room is sent by the
local user, the room is considered read).
4.16 0.3
────────
*Additions*
⁃ Command `ement-directory' shows a server's room directory.
⁃ Command `ement-directory-search' searches a server's room directory.
⁃ Command `ement-directory-next' fetches the next batch of rooms in a
directory.
⁃ Command `ement-leave-room' accepts a `FORCE-P' argument
(interactively, with prefix) to leave a room without prompting.
⁃ Command `ement-forget-room' accepts a `FORCE-P' argument
(interactively, with prefix) to also leave the room, and to forget
it without prompting.
⁃ Option `ement-notify-mark-frame-urgent-predicates' marks the frame
as urgent when (by default) a message mentions the local user or
"@room" and the message's room has an open buffer.
*Changes*
⁃ Minor improvements to date/time headers.
*Fixes*
⁃ Command `ement-describe-room' for rooms without topics.
⁃ Improve insertion of old messages around existing timestamp headers.
⁃ Reduce D-Bus notification system check timeout to 2 seconds (from
the default of 25).
⁃ Compatibility with Emacs 27.
4.17 0.2.1
──────────
*Fixes*
⁃ Info manual export filename.
4.18 0.2
────────
*Changes*
⁃ Read receipts are re-enabled. (They're now implemented with a
global idle timer rather than `window-scroll-functions', which
sometimes caused a strange race condition that could cause Emacs to
become unresponsive or crash.)
⁃ When determining whether a room is considered unread, non-message
events like membership changes, reactions, etc. are ignored. This
fixes a bug that caused certain rooms that had no message events
(like some bridged rooms) to appear as unread when they shouldn't
have. But it's unclear whether this is always preferable (e.g. one
might want a member leaving a room to cause it to be marked unread),
so this is classified as a change rather than simply a fix, and more
improvements may be made to this in the future. (Fixes [#97].
Thanks to [Julien Roy] for reporting and testing.)
⁃ The `ement-taxy-room-list' view no longer automatically refreshes
the list if the region is active in the buffer. (This allows the
user to operate on multiple rooms without the contents of the buffer
changing before completing the process.)
*Fixes*
⁃ Links to only rooms (as opposed to links to events in rooms) may be
activated to join them.
⁃ Read receipts mark the last completely visible event (rather than
one that's only partially displayed).
⁃ Prevent error when a room avatar image fails to load.
[#97] <https://github.com/alphapapa/ement.el/issues/97>
[Julien Roy] <https://github.com/MrRoy>
4.19 0.1.4
──────────
*Fixed*
⁃ Info manual directory headers.
4.20 0.1.3
──────────
*Fixed*
⁃ Temporarily disable sending of read receipts due to an unusual bug
that could cause Emacs to become unresponsive. (The feature will be
re-enabled in a future release.)
4.21 0.1.2
──────────
*Fixed*
⁃ Function `ement-room-sync' correctly updates room-list buffers.
(Thanks to [Visuwesh].)
⁃ Only send D-Bus notifications when supported. (Fixes [#83]. Thanks
to [Tassilo Horn].)
[Visuwesh] <https://github.com/vizs>
[#83] <https://github.com/alphapapa/ement.el/issues/83>
[Tassilo Horn] <https://github.com/tsdh>
4.22 0.1.1
──────────
*Fixed*
⁃ Function `ement-room-scroll-up-mark-read' selects the correct room
window.
⁃ Option `ement-room-list-avatars' defaults to what function
`display-images-p' returns.
4.23 0.1
────────
After almost two years of development, the first tagged release.
Submitted to GNU ELPA.
5 Development
═════════════
Bug reports, feature requests, suggestions — /oh my/!
5.1 Copyright Assignment
────────────────────────
Ement.el is published in GNU ELPA and is considered part of GNU Emacs.
Therefore, cumulative contributions of more than 15 lines of code
require that the author assign copyright of such contributions to the
FSF. Authors who are interested in doing so may contact
[assign@gnu.org] to request the appropriate form.
[assign@gnu.org] <mailto:assign@gnu.org>
5.2 Matrix spec in Org format
─────────────────────────────
An Org-formatted version of the Matrix spec is available in the
[meta/spec] branch.
[meta/spec] <https://github.com/alphapapa/ement.el/tree/meta/spec>
6 License
═════════
GPLv3
#+TITLE: Ement.el
#+PROPERTY: LOGGING nil
# Export options.
#+OPTIONS: broken-links:t *:t
# Info export options.
#+EXPORT_FILE_NAME: ement.texi
#+TEXINFO_DIR_CATEGORY: Emacs
#+TEXINFO_DIR_TITLE: Ement: (ement)
#+TEXINFO_DIR_DESC: Matrix client for Emacs
# Note: This readme works with the org-make-toc <https://github.com/alphapapa/org-make-toc> package, which automatically updates the table of contents.
#+HTML: <img src="images/logo-128px.png" align="right">
# ELPA badge image.
[[https://elpa.gnu.org/packages/ement.html][https://elpa.gnu.org/packages/ement.svg]]
Ement.el is a Matrix client for Emacs. It aims to be simple, fast, featureful, and reliable.
Feel free to join us in the chat room: [[https://matrix.to/#/#ement.el:matrix.org][https://img.shields.io/matrix/ement.el:matrix.org.svg?label=%23ement.el:matrix.org]]
* Contents :noexport:
:PROPERTIES:
:TOC: :include siblings
:END:
:CONTENTS:
- [[#installation][Installation]]
- [[#usage][Usage]]
- [[#bindings][Bindings]]
- [[#tips][Tips]]
- [[#encrypted-room-support-through-pantalaimon][Encrypted room support through Pantalaimon]]
- [[#rationale][Rationale]]
- [[#changelog][Changelog]]
- [[#development][Development]]
:END:
* Screenshots :noexport:
:PROPERTIES:
:ID: d818f690-5f22-4eb0-83e1-4d8ce16c9e5b
:END:
The default formatting style resembles IRC clients, with each message being prefixed by the username (which enables powerful Emacs features, like using Occur to show all messages from or mentioning a user). Alternative, built-in styles include an Element-like one with usernames above groups of messages, as well as a classic, no-margins IRC style. Messages may be optionally displayed with unique colors for each user (with customizeable contrast), making it easier to follow conversations. Timestamp headers are optionally displayed where a certain amount of time passes between events, as well as where the date changes.
[[images/ement-for-twim.png]]
/Two rooms shown in side-by-side buffers, showing inline images, reactions, date/time headings, room avatars, and messages colored by user (using the modus-vivendi Emacs theme)./
[[images/emacs-with-fully-read-line.png]]
/#emacs:libera.chat showing colored text from IRC users, replies with quoted parts, messages colored by user, addressed usernames colored by their user color, highlighted mentions, and the fully-read marker line (using the modus-vivendi Emacs theme)./
[[images/screenshot5.png]]
/Four rooms shown at once, with messages colored by user, in the default Emacs theme./
[[images/screenshot2.png]]
/A room at the top in the "Elemental" display style, with sender names displayed over groups of messages, and only self-messages in an alternate color. The lower window shows an earlier version of the rooms list./
[[images/reactions.png]]
/Reactions displayed as color emojis (may need [[#displaying-symbols-and-emojis][proper Emacs configuration]])./
* Installation
:PROPERTIES:
:TOC: :depth 0
:END:
** GNU ELPA
Ement.el is published in [[http://elpa.gnu.org/][GNU ELPA]], so it may be installed in Emacs with the command ~M-x package-install RET ement RET~. This is the recommended way to install Ement.el, as it will install the current stable release.
** GNU Guix
Ement.el is also available in [[https://guix.gnu.org/][GNU Guix]] as ~emacs-ement~.
** Debian
Ement.el is also available in Debian as [[https://packages.debian.org/elpa-ement][elpa-ement]].
** Git master
The ~master~ branch of the Git repository is intended to be usable at all times; only minor bugs are expected to be found in it before a new stable release is made. To install from this, it is recommended to use [[https://github.com/quelpa/quelpa-use-package][quelpa-use-package]], like this:
#+BEGIN_SRC elisp
;; Install and load `quelpa-use-package'.
(package-install 'quelpa-use-package)
(require 'quelpa-use-package)
;; Install Ement.
(use-package ement
:quelpa (ement :fetcher github :repo "alphapapa/ement.el"))
#+END_SRC
One might also use systems like [[https://github.com/radian-software/straight.el][Straight]] (which is also used by [[https://github.com/doomemacs/doomemacs][DOOM]]) to install from Git, but the author cannot offer support for them.
** Manual
Ement.el is intended to be installed with Emacs's package system, which will ensure that the required autoloads are generated, etc. If you choose to install it manually, you're on your own.
* Usage
:PROPERTIES:
:TOC: :include descendants :depth 1
:END:
:CONTENTS:
- [[#bindings][Bindings]]
- [[#tips][Tips]]
- [[#encrypted-room-support-through-pantalaimon][Encrypted room support through Pantalaimon]]
:END:
1. Call command ~ement-connect~ to connect. Multiple sessions are supported, so you may call the command again to connect to another account.
2. Wait for initial sync to complete (which can take a few moments--initial sync JSON requests can be large).
3. Use these commands (room-related commands may be called with universal prefix to prompt for the room):
- ~ement-list-rooms~ to view the list of joined rooms.
- ~ement-view-room~ to view a room's buffer, selected with completion.
- ~ement-create-room~ to create a new room.
- ~ement-create-space~ to create a space.
- ~ement-invite-user~ to invite a user to a room.
- ~ement-join-room~ to join a room.
- ~ement-leave-room~ to leave a room.
- ~ement-forget-room~ to forget a room.
- ~ement-tag-room~ to toggle a tag on a room (including favorite/low-priority status).
- ~ement-list-members~ to list members in a room.
- ~ement-send-direct-message~ to send a direct message to a user (in an existing direct room, or creating a new one automatically).
- ~ement-room-edit-message~ to edit a message at point.
- ~ement-room-send-file~ to send a file.
- ~ement-room-send-image~ to send an image.
- ~ement-room-set-topic~ to set a room's topic.
- ~ement-room-occur~ to search in a room's known events.
- ~ement-room-override-name~ to override a room's display name.
- ~ement-ignore-user~ to ignore a user (or with interactive prefix, un-ignore).
- ~ement-room-set-message-format~ to set a room's message format buffer-locally.
- ~ement-room-toggle-space~ to toggle a room's membership in a space (a way to group rooms in Matrix).
- ~ement-directory~ to view a room directory.
- ~ement-directory-search~ to search a room directory.
4. Use these special buffers to see events from multiple rooms (you can also reply to messages from these buffers!):
- See all new events that mention you in the =*Ement Mentions*= buffer.
- See all new events in rooms that have open buffers in the =*Ement Notifications*= buffer.
** Bindings
These bindings are common to all of the following buffer types:
+ Switch to a room buffer: ~M-g M-r~
+ Switch to the room list buffer: ~M-g M-l~
+ Switch to the mentions buffer: ~M-g M-m~
+ Switch to the notifications buffer: ~M-g M-n~
*** Room buffers
+ Show command menu: ~?~
[[images/transient.png]]
*Movement*
+ Next event: ~TAB~
+ Previous event: ~<backtab>~
+ Scroll up and mark read: ~SPC~
+ Scroll down: ~S-SPC~
+ Jump to fully-read marker: ~M-SPC~
+ Load older messages: at top of buffer, scroll contents up (i.e. ~S-SPC~, ~M-v~ or ~mwheel-scroll~)
*Switching*
+ List rooms: ~M-g M-l~
+ Switch to other room: ~M-g M-r~
+ Switch to mentions buffer: ~M-g M-m~
+ Switch to notifications buffer: ~M-g M-n~
+ Quit window: ~q~
*Messages*
+ Write message: ~RET~
+ Write reply to event at point (when region is active, only quote marked text) : ~S-RET~
+ Compose message in buffer: ~M-RET~ (while writing in minibuffer: ~C-c ')~ (Use command ~ement-room-compose-org~ to activate Org mode in the compose buffer.)
+ Edit message: ~<insert>~
+ Delete message: ~C-k~
+ Send reaction to event at point, or send same reaction at point: ~s r~
+ Send emote: ~s e~
+ Send file: ~s f~
+ Send image: ~s i~
+ View event source: ~v~
+ Complete members and rooms at point: ~C-M-i~ (standard ~completion-at-point~ command). (Type an ~@~ prefix for a member mention, a ~#~ prefix for a room alias, or a ~!~ prefix for a room ID.)
*Images*
+ Toggle scale of image (between fit-to-window and thumbnail): ~mouse-1~
+ Show image in new buffer at full size: ~double-mouse-1~
*Users*
+ Send direct message: ~u RET~
+ Invite user: ~u i~
+ Ignore user: ~u I~
*Room*
+ Occur search in room: ~M-s o~
+ List members: ~r m~
+ Set topic: ~r t~
+ Set message format: ~r f~
+ Set notification rules: ~r n~
+ Override display name: ~r N~
+ Tag/untag room: ~r T~
*Room membership*
+ Create room: ~R c~
+ Join room: ~R j~
+ Leave room: ~R l~
+ Forget room: ~R F~
+ Toggle room's spaces: ~R s~
*Other*
+ Sync new messages (not necessary if auto sync is enabled; with prefix to force new sync): ~g~
*** Room list buffer
+ Show buffer of room at point: ~RET~
+ Show buffer of next unread room: ~SPC~
+ Move between room names: ~TAB~ / ~<backtab>~
+ Kill room's buffer: ~k~
+ Toggle room's membership in a space: ~s~
*** Directory buffers
+ View/join a room: ~RET~ / ~mouse-1~
+ Load next batch of rooms: ~+~
*** Mentions/notifications buffers
+ Move between events: ~TAB~ / ~<backtab>~
+ Go to event at point in its room buffer: ~RET~
+ Write reply to event at point (shows the event in its room while writing) : ~S-RET~
** Tips
# TODO: Show sending messages in Org format.
+ Desktop notifications are enabled by default for events that mention the local user. They can also be shown for all events in rooms with open buffers.
+ Send messages in Org mode format by customizing the option ~ement-room-send-message-filter~ (which enables Org format by default), or by calling ~ement-room-compose-org~ in a compose buffer (which enables it for a single message). Then Org-formatted messages are automatically converted and sent as HTML-formatted messages (with the Org syntax as the plain-text fallback). You can send syntax such as:
- Bold, italic, underline, strikethrough
- Links
- Tables
- Source blocks (including results with ~:exports both~)
- Footnotes (okay, that might be pushing it, but you can!)
- And, generally, anything that Org can export to HTML
+ Starting in the room list buffer, by pressing ~SPC~ repeatedly, you can cycle through and read all rooms with unread buffers. (If a room doesn't have a buffer, it will not be included.)
+ Room buffers and the room-list buffer can be bookmarked in Emacs, i.e. using =C-x r m=. This is especially useful with [[https://github.com/alphapapa/burly.el][Burly]]: you can arrange an Emacs frame with several room buffers displayed at once, use =burly-bookmark-windows= to bookmark the layout, and then you can restore that layout and all of the room buffers by opening the bookmark, rather than having to manually arrange them every time you start Emacs or change the window configuration.
+ Images and other files can be uploaded to rooms using drag-and-drop.
+ Mention members by typing a ~@~ followed by their displayname or Matrix ID. (Members' names and rooms' aliases/IDs may be completed with ~completion-at-point~ commands.)
+ You can customize settings in the ~ement~ group.
- *Note:* ~setq~ should not be used for certain options, because it will not call the associated setter function. Users who have an aversion to the customization system may experience problems.
*** Displaying symbols and emojis
Emacs may not display certain symbols and emojis well by default. Based on [[https://emacs.stackexchange.com/questions/62049/override-the-default-font-for-emoji-characters][this question and answer]], you may find that the simplest way to fix this is to install an appropriate font, like [[https://www.google.com/get/noto/#emoji-zsye][Noto Emoji]], and then use this Elisp code:
#+BEGIN_SRC elisp
(setf use-default-font-for-symbols nil)
(set-fontset-font t 'unicode "Noto Emoji" nil 'append)
#+END_SRC
** Encrypted room support through Pantalaimon
Ement.el doesn't support encrypted rooms natively, but it can be used transparently with the E2EE-aware reverse proxy daemon [[https://github.com/matrix-org/pantalaimon/][Pantalaimon]]. After configuring it according to its documentation, call ~ement-connect~ with the appropriate hostname and port, like:
#+BEGIN_SRC elisp
(ement-connect :uri-prefix "http://localhost:8009")
#+END_SRC
* Rationale
Why write a new Emacs Matrix client when there is already [[https://github.com/alphapapa/matrix-client.el][matrix-client.el]], by the same author, no less? A few reasons:
- ~matrix-client~ uses an older version of the Matrix spec, r0.3.0, with a few elements of r0.4.0 grafted in. Bringing it up to date with the current version of the spec, r0.6.1, would be more work than to begin with the current version. Ement.el targets r0.6.1 from the beginning.
- ~matrix-client~ does not use Matrix's lazy-loading feature (which was added to the specification later), so initial sync requests can take a long time for the server to process and can be large (sometimes tens of megabytes of JSON for the client to process!). Ement.el uses lazy-loading, which significantly improves performance.
- ~matrix-client~ automatically makes buffers for every room a user has joined, even if the user doesn't currently want to watch a room. Ement.el opens room buffers on-demand, improving performance by not having to insert events into buffers for rooms the user isn't watching.
- ~matrix-client~ was developed without the intention of publishing it to, e.g. MELPA or ELPA. It has several dependencies, and its code does not always install or compile cleanly due to macro-expansion issues (apparently depending on the user's Emacs config). Ement.el is designed to have minimal dependencies outside of Emacs (currently only one, ~plz~, which could be imported into the project), and every file is linted and compiles cleanly using [[https://github.com/alphapapa/makem.sh][makem.sh]].
- ~matrix-client~ uses EIEIO, probably unnecessarily, since few, if any, of the benefits of EIEIO are realized in it. Ement.el uses structs instead.
- ~matrix-client~ uses bespoke code for inserting messages into buffers, which works pretty well, but has a few minor bugs which are difficult to track down. Ement.el uses Emacs's built-in (and perhaps little-known) ~ewoc~ library, which makes it much simpler and more reliable to insert and update messages in buffers, and enables the development of advanced UI features more easily.
- ~matrix-client~ was, to a certain extent, designed to imitate other messaging apps. The result is, at least when used with the ~matrix-client-frame~ command, fairly pleasing to use, but isn't especially "Emacsy." Ement.el is intended to better fit into Emacs's paradigms.
- ~matrix-client~'s long name makes for long symbol names, which makes for tedious, verbose code. ~ement~ is easy to type and makes for concise, readable code.
- The author has learned much since writing ~matrix-client~ and hopes to write simpler, more readable, more maintainable code in Ement.el. It's hoped that this will enable others to contribute more easily.
Note that, while ~matrix-client~ remains usable, and probably will for some time to come, Ement.el has now surpassed it in every way. The only reason to choose ~matrix-client~ instead is if one is using an older version of Emacs that isn't supported by Ement.el.
* Changelog
:PROPERTIES:
:TOC: :depth 0
:END:
** 0.9.2
*Fixes*
+ Restore position in room list when refreshing.
+ Completion in minibuffer.
** 0.9.1
*Fixes*
+ Error in ~ement-room-list~ command upon initial sync.
** 0.9
*Additions*
+ Option ~ement-room-timestamp-header-align~ controls how timestamp headers are aligned in room buffers.
+ Option ~ement-room-view-hook~ runs functions when ~ement-room-view~ is called. (By default, it refreshes the room list buffer.)
+ In the room list, middle-clicking a room which has a buffer closes its buffer.
+ Basic support for video events. (Thanks to [[https://github.com/viiru-][Arto Jantunen]].)
*Changes*
+ Using new option ~ement-room-timestamp-header-align~, timestamp headers default to right-aligned. (With default settings, this keeps them near message timestamps and makes for a cleaner appearance.)
*Fixes*
+ Recognition of certain MXID or displayname forms in outgoing messages when linkifying (aka "pilling") them.
+ Unreadable room avatar images no longer cause errors. (Fixes [[https://github.com/alphapapa/ement.el/issues/147][#147]]. Thanks to [[https://github.com/jgarte][@jgarte]] for reporting.)
+ Don't error in ~ement-room-list~ when no rooms are joined. (Fixes [[https://github.com/alphapapa/ement.el/issues/123][#123]]. Thanks to [[https://github.com/Kabouik][@Kabouik]] and [[https://github.com/oantolin][Omar Antolín Camarena]] for reporting.)
+ Enable member/room completion in compose buffers. (Fixes [[https://github.com/alphapapa/ement.el/issues/115][#115]]. Thanks to Thanks to [[https://github.com/piater][Justus Piater]] and [[https://github.com/chasecaleb][Caleb Chase]] for reporting.)
** 0.8.3
*Fixes*
+ Avoid use of ~pcase~'s ~(map :KEYWORD)~ form. (This can cause a broken installation on older versions of Emacs that have an older version of the ~map~ library loaded, such as Emacs 27.2 included in Debian 11. Since there's no way to force Emacs to actually load the version of ~map~ required by this package before installing it (which would naturally happen upon restarting Emacs), we can only avoid using such forms while these versions of Emacs are widely used.)
** 0.8.2
*Fixes*
+ Deduplicate grouped membership events.
** 0.8.1
Added missing changelog entry (of course).
** 0.8
*Additions*
+ Command ~ement-create-space~ creates a new space.
+ Command ~ement-room-toggle-space~ toggles a room's membership in a space (a way to group rooms in Matrix).
+ Visibility of sections in the room list is saved across sessions.
+ Command ~ement-room-list-kill-buffer~ kills a room's buffer from the room list.
+ Set ~device_id~ and ~initial_device_display_name~ upon login (e.g. =Ement.el: username@hostname=). ([[https://github.com/alphapapa/ement.el/issues/134][#134]]. Thanks to [[https://github.com/viiru-][Arto Jantunen]] for reporting.)
*Changes*
+ Room-related commands may be called interactively with a universal prefix to prompt for the room/session (allowing to send events or change settings in rooms other than the current one).
+ Command ~ement-room-list~ reuses an existing window showing the room list when possible. ([[https://github.com/alphapapa/ement.el/issues/131][#131]]. Thanks to [[https://github.com/jeffbowman][Jeff Bowman]] for suggesting.)
+ Command ~ement-tag-room~ toggles tags (rather than adding by default and removing when called with a prefix).
+ Default room grouping now groups "spaced" rooms separately.
*Fixes*
+ Message format filter works properly when writing replies.
+ Improve insertion of sender name headers when using the "Elemental" message format.
+ Prompts in commands ~ement-leave-room~ and ~ement-forget-room~.
** 0.7
*Additions*
+ Command ~ement-room-override-name~ sets a local override for a room's display name. (Especially helpful for 1:1 rooms and bridged rooms. See [[https://github.com/matrix-org/matrix-spec-proposals/pull/3015#issuecomment-1451017296][MSC3015]].)
*Changes*
+ Improve display of room tombstones (displayed at top and bottom of buffer, and new room ID is linked to join).
+ Use descriptive prompts in ~ement-leave-room~ and ~ement-forget-room~ commands.
*Fixes*
+ Command ~ement-view-space~ when called from a room buffer. (Thanks to [[https://github.com/MagicRB][Richard Brežák]] for reporting.)
+ Don't call ~display-buffer~ when reverting room list buffer. (Fixes [[https://github.com/alphapapa/ement.el/issues/121][#121]]. Thanks to [[https://github.com/mekeor][mekeor]] for reporting.)
+ Retry sync for network timeouts. (Accidentally broken in v0.6.)
*Internal*
+ Function ~ement-put-account-data~ accepts ~:room~ argument to put on a room's account data.
** 0.6
*Additions*
+ Command ~ement-view-space~ to view a space's rooms in a directory buffer.
*Changes*
+ Improve ~ement-describe-room~ command (formatting, bindings).
*Fixes*
+ Retry sync for HTTP 502 "Bad Gateway" errors.
+ Formatting of unban events.
+ Update password authentication according to newer Matrix spec. (Fixes compatibility with Conduit servers. [[https://github.com/alphapapa/ement.el/issues/66][#66]]. Thanks to [[https://github.com/tpeacock19][Travis Peacock]], [[https://github.com/viiru-][Arto Jantunen]], and [[https://github.com/scd31][Stephen D]].)
+ Image scaling issues. (Thanks to [[https://github.com/vizs][Visuwesh]].)
** 0.5.2
*Fixes*
+ Apply ~ement-initial-sync-timeout~ properly (important for when the homeserver is slow to respond).
** 0.5.1
*Fixes*
+ Autoload ~ement-directory~ commands.
+ Faces in ~ement-directory~ listings.
** 0.5
*Additions*
+ Present "joined-and-left" and "rejoined-and-left" membership event pairs as such.
+ Process and show rooms' canonical alias events.
*Changes*
+ The [[https://github.com/alphapapa/taxy.el][taxy.el]]-based room list, with programmable, smart grouping, is now the default ~ement-room-list~. (The old, ~tabulated-list-mode~-based room list is available as ~ement-tabulated-room-list~.)
+ When selecting a room to view with completion, don't offer spaces.
+ When selecting a room with completion, empty aliases and topics are omitted instead of being displayed as nil.
*Fixes*
+ Use of send-message filter when replying.
+ Replies may be written in compose buffers.
** 0.4.1
*Fixes*
+ Don't show "curl process interrupted" message when updating a read marker's position again.
** 0.4
*Additions*
+ Option ~ement-room-unread-only-counts-notifications~, now enabled by default, causes rooms' unread status to be determined only by their notification counts (which are set by the server and depend on rooms' notification settings).
+ Command ~ement-room-set-notification-state~ sets a room's notification state (imitating Element's user-friendly presets).
+ Room buffers' Transient menus show the room's notification state (imitating Element's user-friendly presets).
+ Command ~ement-set-display-name~ sets the user's global displayname.
+ Command ~ement-room-set-display-name~ sets the user's displayname in a room (which is also now displayed in the room's Transient menu).
+ Column ~Notifications~ in the ~ement-taxy-room-list~ buffer shows rooms' notification state.
+ Option ~ement-interrupted-sync-hook~ allows customization of how sync interruptions are handled. (Now, by default, a warning is displayed instead of merely a message.)
*Changes*
+ When a room's read receipt is updated, the room's buffer is also marked as unmodified. (In concert with the new option, this makes rooms' unread status more intuitive.)
*Fixes*
+ Binding of command ~ement-forget-room~ in room buffers.
+ Highlighting of ~@room~ mentions.
** 0.3.1
*Fixes*
+ Room unread status (when the last event in a room is sent by the local user, the room is considered read).
** 0.3
*Additions*
+ Command ~ement-directory~ shows a server's room directory.
+ Command ~ement-directory-search~ searches a server's room directory.
+ Command ~ement-directory-next~ fetches the next batch of rooms in a directory.
+ Command ~ement-leave-room~ accepts a ~FORCE-P~ argument (interactively, with prefix) to leave a room without prompting.
+ Command ~ement-forget-room~ accepts a ~FORCE-P~ argument (interactively, with prefix) to also leave the room, and to forget it without prompting.
+ Option ~ement-notify-mark-frame-urgent-predicates~ marks the frame as urgent when (by default) a message mentions the local user or "@room" and the message's room has an open buffer.
*Changes*
+ Minor improvements to date/time headers.
*Fixes*
+ Command ~ement-describe-room~ for rooms without topics.
+ Improve insertion of old messages around existing timestamp headers.
+ Reduce D-Bus notification system check timeout to 2 seconds (from the default of 25).
+ Compatibility with Emacs 27.
** 0.2.1
*Fixes*
+ Info manual export filename.
** 0.2
*Changes*
+ Read receipts are re-enabled. (They're now implemented with a global idle timer rather than ~window-scroll-functions~, which sometimes caused a strange race condition that could cause Emacs to become unresponsive or crash.)
+ When determining whether a room is considered unread, non-message events like membership changes, reactions, etc. are ignored. This fixes a bug that caused certain rooms that had no message events (like some bridged rooms) to appear as unread when they shouldn't have. But it's unclear whether this is always preferable (e.g. one might want a member leaving a room to cause it to be marked unread), so this is classified as a change rather than simply a fix, and more improvements may be made to this in the future. (Fixes [[https://github.com/alphapapa/ement.el/issues/97][#97]]. Thanks to [[https://github.com/MrRoy][Julien Roy]] for reporting and testing.)
+ The ~ement-taxy-room-list~ view no longer automatically refreshes the list if the region is active in the buffer. (This allows the user to operate on multiple rooms without the contents of the buffer changing before completing the process.)
*Fixes*
+ Links to only rooms (as opposed to links to events in rooms) may be activated to join them.
+ Read receipts mark the last completely visible event (rather than one that's only partially displayed).
+ Prevent error when a room avatar image fails to load.
** 0.1.4
*Fixed*
+ Info manual directory headers.
** 0.1.3
*Fixed*
# + Read receipt-sending function was called too many times when scrolling.
# + Send read receipts even when the last receipt is outside the range of retrieved events.
+ Temporarily disable sending of read receipts due to an unusual bug that could cause Emacs to become unresponsive. (The feature will be re-enabled in a future release.)
** 0.1.2
*Fixed*
+ Function ~ement-room-sync~ correctly updates room-list buffers. (Thanks to [[https://github.com/vizs][Visuwesh]].)
+ Only send D-Bus notifications when supported. (Fixes [[https://github.com/alphapapa/ement.el/issues/83][#83]]. Thanks to [[https://github.com/tsdh][Tassilo Horn]].)
** 0.1.1
*Fixed*
+ Function ~ement-room-scroll-up-mark-read~ selects the correct room window.
+ Option ~ement-room-list-avatars~ defaults to what function ~display-images-p~ returns.
** 0.1
After almost two years of development, the first tagged release. Submitted to GNU ELPA.
* Development
:PROPERTIES:
:TOC: :include this :ignore descendants
:END:
Bug reports, feature requests, suggestions — /oh my/!
** Copyright Assignment
:PROPERTIES:
:TOC: :ignore (this)
:END:
Ement.el is published in GNU ELPA and is considered part of GNU Emacs. Therefore, cumulative contributions of more than 15 lines of code require that the author assign copyright of such contributions to the FSF. Authors who are interested in doing so may contact [[mailto:assign@gnu.org][assign@gnu.org]] to request the appropriate form.
** Matrix spec in Org format
:PROPERTIES:
:TOC: :ignore (this)
:END:
An Org-formatted version of the Matrix spec is available in the [[https://github.com/alphapapa/ement.el/tree/meta/spec][meta/spec]] branch.
* License
:PROPERTIES:
:TOC: :ignore (this)
:END:
GPLv3
* COMMENT Config :noexport:
:PROPERTIES:
:TOC: :ignore (this descendants)
:END:
# NOTE: The #+OPTIONS: and other keywords did not take effect when in this section (perhaps due to file size or to changes in Org), so they were moved to the top of the file.
** File-local variables
# Local Variables:
# eval: (require 'org-make-toc)
# before-save-hook: org-make-toc
# org-export-with-properties: ()
# org-export-with-title: t
# End:
This is the file .../info/dir, which contains the
topmost node of the Info hierarchy, called (dir)Top.
The first time you invoke Info you start off looking at this node.
File: dir, Node: Top This is the top of the INFO tree
This (the Directory node) gives a menu of major topics.
Typing "q" exits, "H" lists all Info commands, "d" returns here,
"h" gives a primer for first-timers,
"mEmacs<Return>" visits the Emacs manual, etc.
In Emacs, you can click mouse button 2 on a menu item or cross reference
to select it.
* Menu:
Emacs
* Ement: (ement). Matrix client for Emacs.
;;; ement-api.el --- Matrix API library -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
;;;; Debugging
;; NOTE: Uncomment this form and `emacs-lisp-byte-compile-and-load' the file to enable
;; `ement-debug' messages. This is commented out by default because, even though the
;; messages are only displayed when `warning-minimum-log-level' is `:debug' at runtime, if
;; that is so at expansion time, the expanded macro calls format the message and check the
;; log level at runtime, which is not zero-cost.
;; (eval-and-compile
;; (setq-local warning-minimum-log-level nil)
;; (setq-local warning-minimum-log-level :debug))
;;;; Requirements
(require 'json)
(require 'url-parse)
(require 'url-util)
(require 'plz)
(require 'ement-macros)
(require 'ement-structs)
;;;; Variables
;;;; Customization
;;;; Commands
;;;; Functions
(cl-defun ement-api (session endpoint
&key then data params queue
(content-type "application/json")
(data-type 'text)
(else #'ement-api-error) (method 'get)
;; FIXME: What's the right term for the URL part after "/_matrix/"?
(endpoint-category "client")
(json-read-fn #'json-read)
;; NOTE: Hard to say what the default timeouts
;; should be. Sometimes the matrix.org homeserver
;; can get slow and respond a minute or two later.
(connect-timeout 10) (timeout 60)
(version "r0"))
"Make API request on SESSION to ENDPOINT.
The request automatically uses SESSION's server, URI prefix, and
access token.
These keyword arguments are passed to `plz', which see: THEN,
DATA (passed as BODY), QUEUE (passed to `plz-queue', which see),
DATA-TYPE (passed as BODY-TYPE), ELSE, METHOD,
JSON-READ-FN (passed as AS), CONNECT-TIMEOUT, TIMEOUT.
Other arguments include PARAMS (used as the URL's query
parameters), ENDPOINT-CATEGORY (added to the endpoint URL), and
VERSION (added to the endpoint URL).
Note that most Matrix requests expect JSON-encoded data, so
usually the DATA argument should be passed through
`json-encode'."
(declare (indent defun))
(pcase-let* (((cl-struct ement-session server token) session)
((cl-struct ement-server uri-prefix) server)
((cl-struct url type host portspec) (url-generic-parse-url uri-prefix))
(path (format "/_matrix/%s/%s/%s" endpoint-category version endpoint))
(query (url-build-query-string params))
(filename (concat path "?" query))
(url (url-recreate-url
(url-parse-make-urlobj type nil nil host portspec filename nil data t)))
(headers (ement-alist "Content-Type" content-type))
(plz-args))
(when token
;; Almost every request will require a token (only a few, like checking login flows, don't),
;; so we simplify the API by using the token automatically when the session has one.
(push (cons "Authorization" (concat "Bearer " token)) headers))
(setf plz-args (list method url :headers headers :body data :body-type data-type
:as json-read-fn :then then :else else
:connect-timeout connect-timeout :timeout timeout :noquery t))
;; Omit `then' from debugging because if it's a partially applied
;; function on the session object, which may be very large, it
;; will take a very long time to print into the warnings buffer.
;; (ement-debug (current-time) method url headers)
(if queue
(plz-run
(apply #'plz-queue queue plz-args))
(apply #'plz plz-args))))
(define-error 'ement-api-error "Ement API error" 'error)
(defun ement-api-error (plz-error)
"Signal an Ement API error for PLZ-ERROR."
;; This feels a little messy, but it seems to be reasonable.
(pcase-let* (((cl-struct plz-error response
(message plz-message) (curl-error `(,curl-exit-code . ,curl-message)))
plz-error)
(status (when (plz-response-p response)
(plz-response-status response)))
(body (when (plz-response-p response)
(plz-response-body response)))
(json-object (when body
(ignore-errors
(json-read-from-string body))))
(error-message (format "%S: %s"
(or curl-exit-code status)
(or (when json-object
(alist-get 'error json-object))
curl-message
plz-message))))
(signal 'ement-api-error (list error-message))))
;;;; Footer
(provide 'ement-api)
;;; ement-api.el ends here
;;; ement-autoloads.el --- automatically extracted autoloads -*- lexical-binding: t -*-
;;
;;; Code:
(add-to-list 'load-path (directory-file-name
(or (file-name-directory #$) (car load-path))))
;;;### (autoloads nil "ement" "ement.el" (0 0 0 0))
;;; Generated autoloads from ement.el
(autoload 'ement-connect "ement" "\
Connect to Matrix with USER-ID and PASSWORD, or using SESSION.
Interactively, with prefix, ignore a saved session and log in
again; otherwise, use a saved session if `ement-save-sessions' is
enabled and a saved session is available, or prompt to log in if
not enabled or available.
If USERID or PASSWORD are not specified, the user will be
prompted for them.
If URI-PREFIX is specified, it should be the prefix of the
server's API URI, including protocol, hostname, and optionally
the port, e.g.
\"https://matrix-client.matrix.org\"
\"http://localhost:8080\"
\(fn &key USER-ID PASSWORD URI-PREFIX SESSION)" t nil)
(register-definition-prefixes "ement" '("ement-"))
;;;***
;;;### (autoloads nil "ement-api" "ement-api.el" (0 0 0 0))
;;; Generated autoloads from ement-api.el
(register-definition-prefixes "ement-api" '("ement-api-error"))
;;;***
;;;### (autoloads nil "ement-directory" "ement-directory.el" (0 0
;;;;;; 0 0))
;;; Generated autoloads from ement-directory.el
(autoload 'ement-directory "ement-directory" "\
View the public room directory on SERVER with SESSION.
Show up to LIMIT rooms. Interactively, with prefix, prompt for
server and LIMIT.
SINCE may be a next-batch token.
\(fn &key SERVER SESSION SINCE (LIMIT 100))" t nil)
(autoload 'ement-directory-search "ement-directory" "\
View public rooms on SERVER matching QUERY.
QUERY is a string used to filter results.
\(fn QUERY &key SERVER SESSION SINCE (LIMIT 1000))" t nil)
(autoload 'ement-view-space "ement-directory" "\
View child rooms in SPACE on SESSION.
SPACE may be a room ID or an `ement-room' struct.
\(fn SPACE SESSION)" t nil)
(register-definition-prefixes "ement-directory" '("ement-directory-"))
;;;***
;;;### (autoloads nil "ement-lib" "ement-lib.el" (0 0 0 0))
;;; Generated autoloads from ement-lib.el
(register-definition-prefixes "ement-lib" '("ement-"))
;;;***
;;;### (autoloads nil "ement-macros" "ement-macros.el" (0 0 0 0))
;;; Generated autoloads from ement-macros.el
(register-definition-prefixes "ement-macros" '("ement-"))
;;;***
;;;### (autoloads nil "ement-notify" "ement-notify.el" (0 0 0 0))
;;; Generated autoloads from ement-notify.el
(register-definition-prefixes "ement-notify" '("ement-notify"))
;;;***
;;;### (autoloads nil "ement-room" "ement-room.el" (0 0 0 0))
;;; Generated autoloads from ement-room.el
(register-definition-prefixes "ement-room" '("ement-"))
;;;***
;;;### (autoloads nil "ement-room-list" "ement-room-list.el" (0 0
;;;;;; 0 0))
;;; Generated autoloads from ement-room-list.el
(autoload 'ement-room-list--after-initial-sync "ement-room-list" "\
Call `ement-room-list', ignoring arguments.
To be called from `ement-after-initial-sync-hook'.
\(fn &rest IGNORE)" nil nil)
(defalias 'ement-list-rooms 'ement-room-list)
(autoload 'ement-room-list "ement-room-list" "\
Show a buffer listing Ement rooms, grouped with Taxy KEYS.
After showing it, its window is selected. The buffer is named
BUFFER-NAME and is shown with DISPLAY-BUFFER-ACTION; or if
DISPLAY-BUFFER-ACTION is nil, the buffer is not displayed.
\(fn &key (BUFFER-NAME \"*Ement Room List*\") (KEYS ement-room-list-default-keys) (DISPLAY-BUFFER-ACTION \\='((display-buffer-reuse-window display-buffer-same-window))))" t nil)
(autoload 'ement-room-list-auto-update "ement-room-list" "\
Automatically update the Taxy room list buffer.
+Does so when variable `ement-room-list-auto-update' is non-nil.
+To be called in `ement-sync-callback-hook'.
\(fn SESSION)" nil nil)
(register-definition-prefixes "ement-room-list" '("ement-room-list-"))
;;;***
;;;### (autoloads nil "ement-tabulated-room-list" "ement-tabulated-room-list.el"
;;;;;; (0 0 0 0))
;;; Generated autoloads from ement-tabulated-room-list.el
(autoload 'ement-tabulated-room-list "ement-tabulated-room-list" "\
Show buffer listing joined rooms.
Calls `pop-to-buffer-same-window'. Interactively, with prefix,
call `pop-to-buffer'.
\(fn &rest IGNORE)" t nil)
(autoload 'ement-tabulated-room-list-auto-update "ement-tabulated-room-list" "\
Automatically update the room list buffer.
Does so when variable `ement-tabulated-room-list-auto-update' is non-nil.
To be called in `ement-sync-callback-hook'.
\(fn SESSION)" nil nil)
(register-definition-prefixes "ement-tabulated-room-list" '("ement-tabulated-room-list-"))
;;;***
;;;### (autoloads nil nil ("ement-pkg.el" "ement-structs.el") (0
;;;;;; 0 0 0))
;;;***
;; Local Variables:
;; version-control: never
;; no-byte-compile: t
;; no-update-autoloads: t
;; coding: utf-8
;; End:
;;; ement-autoloads.el ends here
;;; ement-directory.el --- Public room directory support -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library provides support for viewing and searching public room directories on
;; Matrix homeservers.
;; To make rendering the list flexible and useful, we'll use `taxy-magit-section'.
;;; Code:
;;;; Requirements
(require 'ement)
(require 'ement-room-list)
(require 'taxy)
(require 'taxy-magit-section)
;;;; Variables
(defvar ement-directory-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") #'ement-directory-RET)
(define-key map [mouse-1] #'ement-directory-mouse-1)
(define-key map (kbd "+") #'ement-directory-next)
map))
(defgroup ement-directory nil
"Options for room directories."
:group 'ement)
;;;; Mode
(define-derived-mode ement-directory-mode magit-section-mode "Ement-Directory"
:global nil)
(defvar-local ement-directory-etc nil
"Alist storing information in `ement-directory' buffers.")
;;;;; Keys
(eval-and-compile
(taxy-define-key-definer ement-directory-define-key
ement-directory-keys "ement-directory-key" "FIXME: Docstring."))
;; TODO: Other keys like guest_can_join, world_readable, etc. (Last-updated time would be
;; nice, but the server doesn't include that in the results.)
(ement-directory-define-key joined-p ()
(pcase-let (((map ('room_id id)) item)
((map session) ement-directory-etc))
(when (cl-find id (ement-session-rooms session)
:key #'ement-room-id :test #'equal)
"Joined")))
(ement-directory-define-key size (&key < >)
(pcase-let (((map ('num_joined_members size)) item))
(cond ((and < (< size <))
(format "< %s members" <))
((and > (> size >))
(format "> %s members" >)))))
(ement-directory-define-key space-p ()
"Groups rooms that are themselves spaces."
(pcase-let (((map ('room_type type)) item))
(when (equal "m.space" type)
"Spaces")))
(defcustom ement-directory-default-keys
'((joined-p)
(space-p)
((size :> 10000))
((size :> 1000))
((size :> 100))
((size :> 10))
((size :< 11)))
"Default keys."
:type 'sexp)
;;;; Columns
(defvar-local ement-directory-room-avatar-cache (make-hash-table)
;; Use a buffer-local variable so that the cache is cleared when the buffer is closed.
"Hash table caching room avatars for the `ement-directory' room list.")
(eval-and-compile
(taxy-magit-section-define-column-definer "ement-directory"))
;; TODO: Fetch avatars (with queueing and async updating/insertion?).
(ement-directory-define-column #("✓" 0 1 (help-echo "Joined")) ()
(pcase-let (((map ('room_id id)) item)
((map session) ement-directory-etc))
(if (cl-find id (ement-session-rooms session)
:key #'ement-room-id :test #'equal)
"✓"
" ")))
(ement-directory-define-column "Name" (:max-width 25)
(pcase-let* (((map name ('room_type type)) item)
(face (pcase type
("m.space" 'ement-room-list-space)
(_ 'ement-room-list-name))))
(propertize (or name "[unnamed]")
'face face)))
(ement-directory-define-column "Alias" (:max-width 25)
(pcase-let (((map ('canonical_alias alias)) item))
(or alias "")))
(ement-directory-define-column "Size" ()
(pcase-let (((map ('num_joined_members size)) item))
(number-to-string size)))
(ement-directory-define-column "Topic" (:max-width 50)
(pcase-let (((map topic) item))
(if topic
(replace-regexp-in-string "\n" " | " topic nil t)
"")))
(ement-directory-define-column "ID" ()
(pcase-let (((map ('room_id id)) item))
id))
(unless ement-directory-columns
;; TODO: Automate this or document it
(setq-default ement-directory-columns
'("Name" "Alias" "Size" "Topic" "ID")))
;;;; Commands
;; TODO: Pagination of results.
;;;###autoload
(cl-defun ement-directory (&key server session since (limit 100))
"View the public room directory on SERVER with SESSION.
Show up to LIMIT rooms. Interactively, with prefix, prompt for
server and LIMIT.
SINCE may be a next-batch token."
(interactive (let* ((session (ement-complete-session :prompt "Search on session: "))
(server (if current-prefix-arg
(read-string "Search on server: " nil nil
(ement-server-name (ement-session-server session)))
(ement-server-name (ement-session-server session))))
(args (list :server server :session session)))
(when current-prefix-arg
(cl-callf plist-put args
:limit (read-number "Limit number of rooms: " 100)))
args))
(pcase-let ((revert-function (lambda (&rest _ignore)
(interactive)
(ement-directory :server server :session session :limit limit)))
(endpoint "publicRooms")
(params (list (list "limit" limit))))
(when since
(cl-callf append params (list (list "since" since))))
(ement-api session endpoint :params params
:then (lambda (results)
(pcase-let (((map ('chunk rooms) ('next_batch next-batch)
('total_room_count_estimate remaining))
results))
(ement-directory--view rooms :append-p since
:buffer-name (format "*Ement Directory: %s*" server)
:root-section-name (format "Ement Directory: %s" server)
:init-fn (lambda ()
(setf (alist-get 'server ement-directory-etc) server
(alist-get 'session ement-directory-etc) session
(alist-get 'next-batch ement-directory-etc) next-batch
(alist-get 'limit ement-directory-etc) limit)
(setq-local revert-buffer-function revert-function)
(when remaining
;; FIXME: The server seems to report all of the rooms on
;; the server as remaining even when searching for a
;; specific term like "emacs".
;; TODO: Display this in a more permanent place (like a
;; header or footer).
(message
(substitute-command-keys
"%s rooms remaining (use \\[ement-directory-next] to fetch more)")
remaining)))))))
(ement-message "Listing %s rooms on %s..." limit server)))
;;;###autoload
(cl-defun ement-directory-search (query &key server session since (limit 1000))
"View public rooms on SERVER matching QUERY.
QUERY is a string used to filter results."
(interactive (let* ((session (ement-complete-session :prompt "Search on session: "))
(server (if current-prefix-arg
(read-string "Search on server: " nil nil
(ement-server-name (ement-session-server session)))
(ement-server-name (ement-session-server session))))
(query (read-string (format "Search for rooms on %s matching: " server)))
(args (list query :server server :session session)))
(when current-prefix-arg
(cl-callf plist-put (cdr args)
:limit (read-number "Limit number of rooms: " 1000)))
args))
;; TODO: Handle "include_all_networks" and "third_party_instance_id". See § 10.5.4.
(pcase-let* ((revert-function (lambda (&rest _ignore)
(interactive)
(ement-directory-search query :server server :session session)))
(endpoint "publicRooms")
(data (rassq-delete-all nil
(ement-alist "limit" limit
"filter" (ement-alist "generic_search_term" query)
"since" since))))
(ement-api session endpoint :method 'post :data (json-encode data)
:then (lambda (results)
(pcase-let (((map ('chunk rooms) ('next_batch next-batch)
('total_room_count_estimate remaining))
results))
(ement-directory--view rooms :append-p since
:buffer-name (format "*Ement Directory: \"%s\" on %s*" query server)
:root-section-name (format "Ement Directory: \"%s\" on %s" query server)
:init-fn (lambda ()
(setf (alist-get 'server ement-directory-etc) server
(alist-get 'session ement-directory-etc) session
(alist-get 'next-batch ement-directory-etc) next-batch
(alist-get 'limit ement-directory-etc) limit
(alist-get 'query ement-directory-etc) query)
(setq-local revert-buffer-function revert-function)
(when remaining
(message
(substitute-command-keys
"%s rooms remaining (use \\[ement-directory-next] to fetch more)")
remaining)))))))
(ement-message "Searching for %S on %s..." query server)))
(defun ement-directory-next ()
"Fetch next batch of results in `ement-directory' buffer."
(interactive)
(pcase-let (((map next-batch query limit server session) ement-directory-etc))
(unless next-batch
(user-error "No more results"))
(if query
(ement-directory-search query :server server :session session :limit limit :since next-batch)
(ement-directory :server server :session session :limit limit :since next-batch))))
(defun ement-directory-mouse-1 (event)
"Call `ement-directory-RET' at EVENT."
(interactive "e")
(mouse-set-point event)
(call-interactively #'ement-directory-RET))
(defun ement-directory-RET ()
"View or join room at point, or cycle section at point."
(interactive)
(cl-etypecase (oref (magit-current-section) value)
(null nil)
(list (pcase-let* (((map ('name name) ('room_id room-id)) (oref (magit-current-section) value))
((map session) ement-directory-etc)
(room (cl-find room-id (ement-session-rooms session)
:key #'ement-room-id :test #'equal)))
(if room
(ement-view-room room session)
;; Room not joined: prompt to join. (Don't use the alias in the prompt,
;; because multiple rooms might have the same alias, e.g. when one is
;; upgraded or tombstoned.)
(when (yes-or-no-p (format "Join room \"%s\" <%s>? " name room-id))
(ement-join-room room-id session)))))
(taxy-magit-section (call-interactively #'magit-section-cycle))))
;;;; Functions
(cl-defun ement-directory--view (rooms &key init-fn append-p
(buffer-name "*Ement Directory*")
(root-section-name "Ement Directory")
(keys ement-directory-default-keys)
(display-buffer-action '(display-buffer-same-window)))
"View ROOMS in an `ement-directory-mode' buffer.
ROOMS should be a list of rooms from an API request. Calls
INIT-FN immediately after activating major mode. Sets
BUFFER-NAME and ROOT-SECTION-NAME, and uses
DISPLAY-BUFFER-ACTION. KEYS are a list of `taxy' keys. If
APPEND-P, add ROOMS to buffer rather than replacing existing
contents. To be called by `ement-directory-search'."
(declare (indent defun))
(let (column-sizes window-start)
(cl-labels ((format-item
;; NOTE: We use the buffer-local variable `ement-directory-etc' rather
;; than a closure variable because the taxy-magit-section struct's format
;; table is not stored in it, and we can't reuse closures' variables.
;; (It would be good to store the format table in the taxy-magit-section
;; in the future, to make this cleaner.)
(item) (gethash item (alist-get 'format-table ement-directory-etc)))
;; NOTE: Since these functions take an "item" (which is a [room session]
;; vector), they're prefixed "item-" rather than "room-".
(size
(item) (pcase-let (((map ('num_joined_members size)) item))
size))
(t<nil (a b) (and a (not b)))
(t>nil (a b) (and (not a) b))
(make-fn (&rest args)
(apply #'make-taxy-magit-section
:make #'make-fn
:format-fn #'format-item
;; FIXME: Should we reuse `ement-room-list-level-indent' here?
:level-indent ement-room-list-level-indent
;; :visibility-fn #'visible-p
;; :heading-indent 2
:item-indent 2
;; :heading-face-fn #'heading-face
args)))
(with-current-buffer (get-buffer-create buffer-name)
(unless (eq 'ement-directory-mode major-mode)
;; Don't obliterate buffer-local variables.
(ement-directory-mode))
(when init-fn
(funcall init-fn))
(pcase-let* ((taxy (if append-p
(alist-get 'taxy ement-directory-etc)
(make-fn
:name root-section-name
:take (taxy-make-take-function keys ement-directory-keys))))
(taxy-magit-section-insert-indent-items nil)
(inhibit-read-only t)
(pos (point))
(section-ident (when (magit-current-section)
(magit-section-ident (magit-current-section))))
(format-cons))
(setf taxy (thread-last taxy
(taxy-fill (cl-coerce rooms 'list))
(taxy-sort #'> #'size)
(taxy-sort* #'string> #'taxy-name))
(alist-get 'taxy ement-directory-etc) taxy
format-cons (taxy-magit-section-format-items
ement-directory-columns ement-directory-column-formatters taxy)
(alist-get 'format-table ement-directory-etc) (car format-cons)
column-sizes (cdr format-cons)
header-line-format (taxy-magit-section-format-header
column-sizes ement-directory-column-formatters)
window-start (if (get-buffer-window buffer-name)
(window-start (get-buffer-window buffer-name))
0))
(delete-all-overlays)
(erase-buffer)
(save-excursion
(taxy-magit-section-insert taxy :items 'first
;; :blank-between-depth bufler-taxy-blank-between-depth
:initial-depth 0))
(goto-char pos)
(when (and section-ident (magit-get-section section-ident))
(goto-char (oref (magit-get-section section-ident) start)))))
(display-buffer buffer-name display-buffer-action)
(when (get-buffer-window buffer-name)
(set-window-start (get-buffer-window buffer-name) window-start))
;; NOTE: In order for `bookmark--jump-via' to work properly, the restored buffer
;; must be set as the current buffer, so we have to do this explicitly here.
(set-buffer buffer-name))))
;;;; Spaces
;; Viewing spaces and the rooms in them.
;;;###autoload
(defun ement-view-space (space session)
;; TODO: Use this for spaces instead of `ement-view-room' (or something like that).
;; TODO: Display space's topic in the header or something.
"View child rooms in SPACE on SESSION.
SPACE may be a room ID or an `ement-room' struct."
;; TODO: "from" query parameter.
(interactive (ement-complete-room :predicate #'ement--room-space-p
:prompt "Space: "))
(pcase-let* ((id (cl-typecase space
(string space)
(ement-room (ement-room-id space))))
(endpoint (format "rooms/%s/hierarchy" id))
(revert-function (lambda (&rest _ignore)
(interactive)
(ement-view-space space session))))
(ement-api session endpoint :version "v1"
:then (lambda (results)
(pcase-let (((map rooms ('next_batch next-batch))
results))
(ement-directory--view rooms ;; :append-p since
;; TODO: Use space's alias where possible.
:buffer-name (format "*Ement Directory: space \"%s\"" id)
:root-section-name (format "*Ement Directory: space \"%s\"" id)
:init-fn (lambda ()
(setf (alist-get 'session ement-directory-etc) session
(alist-get 'next-batch ement-directory-etc) next-batch
;; (alist-get 'limit ement-directory-etc) limit
(alist-get 'space ement-directory-etc) space)
(setq-local revert-buffer-function revert-function)
;; TODO: Handle next batches.
;; (when remaining
;; (message
;; (substitute-command-keys
;; "%s rooms remaining (use \\[ement-directory-next] to fetch more)")
;; remaining))
)))))))
;;;; Footer
(provide 'ement-directory)
;;; ement-directory.el ends here
;;; ement-lib.el --- Library of Ement functions -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library provides functions used in other Ement libraries. It exists so they may
;; be required where needed, without causing circular dependencies.
;;; Code:
;;;; Requirements
(eval-when-compile
(require 'eieio)
(require 'ewoc)
(require 'pcase)
(require 'subr-x)
(require 'taxy-magit-section)
(require 'ement-macros))
(require 'cl-lib)
(require 'color)
(require 'map)
(require 'xml)
(require 'ement-api)
(require 'ement-structs)
;;;; Variables
(defvar ement-sessions)
(defvar ement-users)
(defvar ement-ewoc)
(defvar ement-room)
(defvar ement-session)
(defvar ement-room-buffer-name-prefix)
(defvar ement-room-buffer-name-suffix)
(defvar ement-room-leave-kill-buffer)
(defvar ement-room-prism)
(defvar ement-room-prism-color-adjustment)
(defvar ement-room-prism-minimum-contrast)
(defvar ement-room-unread-only-counts-notifications)
;;;; Function declarations
;; Instead of using top-level `declare-function' forms (which can easily become obsolete
;; if not kept with the code that needs them), this allows the use of `(declare (function
;; ...))' forms in each function definition, so that if a function is moved or removed,
;; the `declare-function' goes with it.
;; TODO: Propose this upstream.
(eval-and-compile
(defun ement--byte-run--declare-function (_name _args &rest values)
"Return a `declare-function' form with VALUES.
Allows the use of a form like:
(declare (function FN FILE ...))
inside of a function definition, effectively keeping its
`declare-function' form inside the function definition, ensuring
that stray such forms don't remain if the function is removed."
`(declare-function ,@values))
(cl-pushnew '(function ement--byte-run--declare-function) defun-declarations-alist :test #'equal)
(cl-pushnew '(function ement--byte-run--declare-function) macro-declarations-alist :test #'equal))
;;;; Compatibility
;; These workarounds should be removed when they aren't needed.
;;;;; Emacs 28 color features.
;; Copied from Emacs 28. See <https://github.com/alphapapa/ement.el/issues/99>.
;; FIXME: Remove this workaround when possible.
(eval-and-compile
(unless (boundp 'color-luminance-dark-limit)
(defconst ement--color-luminance-dark-limit 0.325
"The relative luminance below which a color is considered 'dark'.
A 'dark' color in this sense provides better contrast with white
than with black; see `color-dark-p'.
This value was determined experimentally.")))
(defalias 'ement--color-dark-p
(if (fboundp 'color-dark-p)
'color-dark-p
(lambda (rgb)
"Whether RGB is more readable against white than black.
RGB is a 3-element list (R G B), each component in the range [0,1].
This predicate can be used both for determining a suitable (black or white)
contrast colour with RGB as background and as foreground."
(unless (<= 0 (apply #'min rgb) (apply #'max rgb) 1)
(error "RGB components %S not in [0,1]" rgb))
;; Compute the relative luminance after gamma-correcting (assuming sRGB),
;; and compare to a cut-off value determined experimentally.
;; See https://en.wikipedia.org/wiki/Relative_luminance for details.
(let* ((sr (nth 0 rgb))
(sg (nth 1 rgb))
(sb (nth 2 rgb))
;; Gamma-correct the RGB components to linear values.
;; Use the power 2.2 as an approximation to sRGB gamma;
;; it should be good enough for the purpose of this function.
(r (expt sr 2.2))
(g (expt sg 2.2))
(b (expt sb 2.2))
(y (+ (* r 0.2126) (* g 0.7152) (* b 0.0722))))
(< y ement--color-luminance-dark-limit)))))
;;;; Functions
;;;;; Commands
(cl-defun ement-create-room
(session &key name alias topic invite direct-p creation-content
(then (lambda (data)
(message "Created new room: %s" (alist-get 'room_id data))))
(visibility 'private))
"Create new room on SESSION.
Then call function THEN with response data. Optional string
arguments are NAME, ALIAS, and TOPIC. INVITE may be a list of
user IDs to invite. If DIRECT-P, set the \"is_direct\" flag in
the request. CREATION-CONTENT may be an alist of extra keys to
include with the request (see Matrix spec)."
;; TODO: Document other arguments.
;; SPEC: 10.1.1.
(declare (indent defun))
(interactive (list (ement-complete-session)
:name (read-string "New room name: ")
:alias (read-string "New room alias (e.g. \"foo\" for \"#foo:matrix.org\"): ")
:topic (read-string "New room topic: ")
:visibility (completing-read "New room visibility: " '(private public))))
(cl-labels ((given-p
(var) (and var (not (string-empty-p var)))))
(pcase-let* ((endpoint "createRoom")
(data (ement-aprog1
(ement-alist "visibility" visibility)
(when (given-p alias)
(push (cons "room_alias_name" alias) it))
(when (given-p name)
(push (cons "name" name) it))
(when (given-p topic)
(push (cons "topic" topic) it))
(when invite
(push (cons "invite" invite) it))
(when direct-p
(push (cons "is_direct" t) it))
(when creation-content
(push (cons "creation_content" creation-content) it)))))
(ement-api session endpoint :method 'post :data (json-encode data)
:then then))))
(cl-defun ement-create-space
(session &key name alias topic
(then (lambda (data)
(message "Created new space: %s" (alist-get 'room_id data))))
(visibility 'private))
"Create new space on SESSION.
Then call function THEN with response data. Optional string
arguments are NAME, ALIAS, and TOPIC."
(declare (indent defun))
(interactive (list (ement-complete-session)
:name (read-string "New space name: ")
:alias (read-string "New space alias (e.g. \"foo\" for \"#foo:matrix.org\"): ")
:topic (read-string "New space topic: ")
:visibility (completing-read "New space visibility: " '(private public))))
(ement-create-room session :name name :alias alias :topic topic :visibility visibility
:creation-content (ement-alist "type" "m.space") :then then))
(defun ement-room-leave (room session &optional force-p)
"Leave ROOM on SESSION.
If FORCE-P, leave without prompting. ROOM may be an `ement-room'
struct, or a room ID or alias string."
;; TODO: Rename `room' argument to `room-or-id'.
(interactive
(ement-with-room-and-session
:prompt-form (ement-complete-room :prompt "Leave room: ")
(list ement-room ement-session)))
(cl-etypecase room
(ement-room)
(string (setf room (ement-afirst (or (equal room (ement-room-canonical-alias it))
(equal room (ement-room-id it)))
(ement-session-rooms session)))))
(when (or force-p (yes-or-no-p (format "Leave room %s? " (ement--format-room room))))
(pcase-let* (((cl-struct ement-room id) room)
(endpoint (format "rooms/%s/leave" (url-hexify-string id))))
(ement-api session endpoint :method 'post :data ""
:then (lambda (_data)
(when ement-room-leave-kill-buffer
;; NOTE: This generates a symbol and sets its function value to a lambda
;; which removes the symbol from the hook, removing itself from the hook.
;; TODO: When requiring Emacs 27, use `letrec'.
(let* ((leave-fn-symbol (gensym (format "ement-leave-%s" room)))
(leave-fn (lambda (_session)
(remove-hook 'ement-sync-callback-hook leave-fn-symbol)
;; FIXME: Probably need to unintern the symbol.
(when-let ((buffer (map-elt (ement-room-local room) 'buffer)))
(when (buffer-live-p buffer)
(kill-buffer buffer))))))
(setf (symbol-function leave-fn-symbol) leave-fn)
(add-hook 'ement-sync-callback-hook leave-fn-symbol)))
(ement-message "Left room: %s" (ement--format-room room)))
:else (lambda (plz-error)
(pcase-let* (((cl-struct plz-error response) plz-error)
((cl-struct plz-response status body) response)
((map error) (json-read-from-string body)))
(pcase status
(429 (error "Unable to leave room %s: %s" room error))
(_ (error "Unable to leave room %s: %s %S" room status plz-error)))))))))
(defalias 'ement-leave-room #'ement-room-leave)
(defun ement-forget-room (room session &optional force-p)
"Forget ROOM on SESSION.
If FORCE-P (interactively, with prefix), prompt to leave the room
when necessary, and forget the room without prompting."
(interactive
(ement-with-room-and-session
:prompt-form (ement-complete-room :prompt "Forget room: ")
(list ement-room ement-session current-prefix-arg)))
(pcase-let* (((cl-struct ement-room id display-name status) room)
(endpoint (format "rooms/%s/forget" (url-hexify-string id))))
(pcase status
('join (if (and force-p
(yes-or-no-p (format "Leave and forget room %s? (WARNING: You will not be able to rejoin the room to access its content.) "
(ement--format-room room))))
(progn
;; TODO: Use `letrec'.
(let* ((forget-fn-symbol (gensym (format "ement-forget-%s" room)))
(forget-fn (lambda (_session)
(when (equal 'leave (ement-room-status room))
(remove-hook 'ement-sync-callback-hook forget-fn-symbol)
;; FIXME: Probably need to unintern the symbol.
(ement-forget-room room session 'force)))))
(setf (symbol-function forget-fn-symbol) forget-fn)
(add-hook 'ement-sync-callback-hook forget-fn-symbol))
(ement-leave-room room session 'force))
(user-error "Room %s is joined (must be left before forgetting)"
(ement--format-room room))))
('leave (when (or force-p (yes-or-no-p (format "Forget room \"%s\" (%s)? " display-name id)))
(ement-api session endpoint :method 'post :data ""
:then (lambda (_data)
;; NOTE: The spec does not seem to indicate that the action of forgetting
;; a room is synced to other clients, so it seems that we need to remove
;; the room from the session here.
(setf (ement-session-rooms session)
(cl-remove room (ement-session-rooms session)))
;; TODO: Indicate forgotten in footer in room buffer.
(ement-message "Forgot room: %s." (ement--format-room room)))))))))
(defun ement-ignore-user (user-id session &optional unignore-p)
"Ignore USER-ID on SESSION.
If UNIGNORE-P (interactively, with prefix), un-ignore USER."
(interactive (list (ement-complete-user-id)
(ement-complete-session)
current-prefix-arg))
(pcase-let* (((cl-struct ement-session account-data) session)
;; TODO: Store session account-data events in an alist keyed on type.
((map ('content (map ('ignored_users ignored-users))))
(cl-find "m.ignored_user_list" account-data
:key (lambda (event) (alist-get 'type event)) :test #'equal)))
(if unignore-p
;; Being map keys, the user IDs have been interned by `json-read'.
(setf ignored-users (map-delete ignored-users (intern user-id)))
;; Empty maps are used to list ignored users.
(setf (map-elt ignored-users user-id) nil))
(ement-put-account-data session "m.ignored_user_list" (ement-alist "ignored_users" ignored-users)
:then (lambda (data)
(ement-debug "PUT successful" data)
(message "Ement: User %s %s." user-id (if unignore-p "unignored" "ignored"))))))
(defun ement-invite-user (user-id room session)
"Invite USER-ID to ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room."
;; SPEC: 10.4.2.1.
(interactive
(ement-with-room-and-session
(list (ement-complete-user-id) ement-room ement-session)))
(pcase-let* ((endpoint (format "rooms/%s/invite"
(url-hexify-string (ement-room-id room))))
(data (ement-alist "user_id" user-id) ))
(ement-api session endpoint :method 'post :data (json-encode data)
;; TODO: Handle error codes.
:then (lambda (_data)
(message "User %s invited to room \"%s\" (%s)" user-id
(ement-room-display-name room)
(ement-room-id room))))))
(defun ement-list-members (room session bufferp)
"Show members of ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room. If BUFFERP (interactively, with
prefix), or if there are many members, show in a new buffer;
otherwise show in echo area."
(interactive
(ement-with-room-and-session
(list ement-room ement-session current-prefix-arg)))
(pcase-let* (((cl-struct ement-room members (local (map fetched-members-p))) room)
(list-members
(lambda (&optional _)
(cond ((or bufferp (> (hash-table-count members) 51))
;; Show in buffer.
(let* ((buffer (get-buffer-create (format "*Ement members: %s*" (ement-room-display-name room))))
(members (cl-sort (cl-loop for user being the hash-values of members
for id = (ement-user-id user)
for displayname = (ement--user-displayname-in room user)
collect (cons displayname id))
(lambda (a b) (string-collate-lessp a b nil t)) :key #'car))
(displayname-width (cl-loop for member in members
maximizing (string-width (car member))))
(format-string (format "%%-%ss <%%s>" displayname-width)))
(with-current-buffer buffer
(erase-buffer)
(save-excursion
(dolist (member members)
(insert (format format-string (car member) (cdr member)) "\n"))))
(pop-to-buffer buffer)))
(t
;; Show in echo area.
(message "Members of %s (%s): %s" (ement--room-display-name room)
(hash-table-count members)
(string-join (map-apply (lambda (_id user)
(ement--user-displayname-in room user))
members)
", ")))))))
(if fetched-members-p
(funcall list-members)
(ement--get-joined-members room session
:then list-members))
(message "Listing members of %s..." (ement--format-room room))))
(defun ement-send-direct-message (session user-id message)
"Send a direct MESSAGE to USER-ID on SESSION.
Uses the latest existing direct room with the user, or creates a
new one automatically if necessary."
;; SPEC: 13.23.2.
(interactive
(let* ((session (ement-complete-session))
(user-id (ement-complete-user-id))
(message (read-string "Message: ")))
(list session user-id message)))
(if-let* ((seen-user (gethash user-id ement-users))
(existing-direct-room (ement--direct-room-for-user seen-user session)))
(progn
(ement-send-message existing-direct-room session :body message)
(message "Message sent to %s <%s> in room %S <%s>."
(ement--user-displayname-in existing-direct-room seen-user)
user-id
(ement-room-display-name existing-direct-room) (ement-room-id existing-direct-room)))
;; No existing room for user: make new one.
(message "Creating new room for user %s..." user-id)
(ement-create-room session :direct-p t :invite (list user-id)
:then (lambda (data)
(let* ((room-id (alist-get 'room_id data))
(room (or (cl-find room-id (ement-session-rooms session)
:key #'ement-room-id)
;; New room hasn't synced yet: make a temporary struct.
(make-ement-room :id room-id)))
(direct-rooms-account-data-event-content
;; FIXME: Make account-data a map.
(alist-get 'content (cl-find-if (lambda (event)
(equal "m.direct" (alist-get 'type event)))
(ement-session-account-data session)))))
;; Mark new room as direct: add the room to the account-data event, then
;; put the new account data to the server. (See also:
;; <https://github.com/matrix-org/matrix-react-sdk/blob/919aab053e5b3bdb5a150fd90855ad406c19e4ab/src/Rooms.ts#L91>).
(setf (map-elt direct-rooms-account-data-event-content user-id) (vector room-id))
(ement-put-account-data session "m.direct" direct-rooms-account-data-event-content)
;; Send message to new room.
(ement-send-message room session :body message)
(message "Room \"%s\" created for user %s. Sending message..."
room-id user-id))))))
(defun ement-tag-room (tag room session)
"Toggle TAG for ROOM on SESSION."
(interactive
(ement-with-room-and-session
(let* ((prompt (format "Toggle tag (%s): " (ement--format-room ement-room)))
(default-tags
(ement-alist (propertize "Favourite"
'face (when (ement--room-tagged-p "m.favourite" ement-room)
'transient-value))
"m.favourite"
(propertize "Low-priority"
'face (when (ement--room-tagged-p "m.lowpriority" ement-room)
'transient-value))
"m.lowpriority"))
(input (completing-read prompt default-tags))
(tag (alist-get input default-tags (concat "u." input) nil #'string=)))
(list tag ement-room ement-session))))
(pcase-let* (((cl-struct ement-session user) session)
((cl-struct ement-user (id user-id)) user)
((cl-struct ement-room (id room-id)) room)
(endpoint (format "user/%s/rooms/%s/tags/%s"
(url-hexify-string user-id) (url-hexify-string room-id) (url-hexify-string tag)))
(method (if (ement--room-tagged-p tag room) 'delete 'put)))
;; TODO: "order".
;; FIXME: Removing a tag on a left room doesn't seem to work (e.g. to unfavorite a room after leaving it, but not forgetting it).
(ement-api session endpoint :version "v3" :method method :data (pcase method ('put "{}"))
:then (lambda (_)
(ement-message "%s tag %S on %s"
(pcase method
('delete "Removed")
('put "Added"))
tag (ement--format-room room)) ))))
(defun ement-set-display-name (display-name session)
"Set DISPLAY-NAME for user on SESSION.
Sets global displayname."
(interactive
(let* ((session (ement-complete-session))
(display-name (read-string "Set display-name to: " nil nil
(ement-user-displayname (ement-session-user session)))))
(list display-name session)))
(pcase-let* (((cl-struct ement-session user) session)
((cl-struct ement-user (id user-id)) user)
(endpoint (format "profile/%s/displayname" (url-hexify-string user-id))))
(ement-api session endpoint :method 'put :version "v3"
:data (json-encode (ement-alist "displayname" display-name))
:then (lambda (_data)
(message "Ement: Display name set to %S for <%s>" display-name
(ement-user-id (ement-session-user session)))))))
(defun ement-room-set-display-name (display-name room session)
"Set DISPLAY-NAME for user in ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room. Sets the name only in ROOM, not
globally."
(interactive
(ement-with-room-and-session
(let* ((prompt (format "Set display-name in %S to: "
(ement--format-room ement-room)))
(display-name (read-string prompt nil nil
(ement-user-displayname (ement-session-user ement-session)))))
(list display-name ement-room ement-session))))
;; NOTE: This does not seem to be documented in the spec, so we imitate the
;; "/myroomnick" command in SlashCommands.tsx from matrix-react-sdk.
(pcase-let* (((cl-struct ement-room state) room)
((cl-struct ement-session user) session)
((cl-struct ement-user id) user)
(member-event (cl-find-if (lambda (event)
(and (equal id (ement-event-state-key event))
(equal "m.room.member" (ement-event-type event))
(equal "join" (alist-get 'membership (ement-event-content event)))))
state)))
(cl-assert member-event)
(setf (alist-get 'displayname (ement-event-content member-event)) display-name)
(ement-put-state room "m.room.member" id (ement-event-content member-event) session
:then (lambda (_data)
(message "Ement: Display name set to %S for <%s> in %S" display-name
(ement-user-id (ement-session-user session))
(ement--format-room room))))))
;;;;;; Describe room
(defvar ement-describe-room-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "q") #'quit-window)
map)
"Keymap for `ement-describe-room-mode' buffers.")
(define-derived-mode ement-describe-room-mode read-only-mode
"Ement-Describe-Room" "Major mode for `ement-describe-room' buffers.")
(defun ement-describe-room (room session)
"Describe ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room."
(interactive (ement-with-room-and-session (list ement-room ement-session)))
(cl-labels ((heading (string)
(propertize (or string "") 'face 'font-lock-builtin-face))
(id (string)
(propertize (or string "") 'face 'font-lock-constant-face))
(member<
(a b) (string-collate-lessp (car a) (car b) nil t)))
(pcase-let* (((cl-struct ement-room (id room-id) avatar display-name canonical-alias members timeline status topic
(local (map fetched-members-p)))
room)
((cl-struct ement-session user) session)
((cl-struct ement-user (id user-id)) user)
(inhibit-read-only t))
(if (not fetched-members-p)
;; Members not fetched: fetch them and re-call this command.
(ement--get-joined-members room session
:then (lambda (_) (ement-room-describe room session)))
(with-current-buffer (get-buffer-create (format "*Ement room description: %s*" (or display-name canonical-alias room-id)))
(let ((inhibit-read-only t))
(erase-buffer)
;; We avoid looping twice by doing a bit more work here and
;; returning a cons which we destructure.
(pcase-let* ((`(,member-pairs . ,name-width)
(cl-loop for user being the hash-values of members
for formatted = (ement--format-user user room session)
for id = (format "<%s>" (id (ement-user-id user)))
collect (cons formatted id)
into pairs
maximizing (string-width id) into width
finally return (cons (cl-sort pairs #'member<) width)))
;; We put the MXID first, because users may use Unicode characters
;; in their displayname, which `string-width' does not always
;; return perfect results for, and putting it last prevents
;; alignment problems.
(spec (format "%%-%ss %%s" name-width)))
(save-excursion
(insert "\"" (propertize (or display-name canonical-alias room-id) 'face 'font-lock-doc-face) "\"" " is a room "
(propertize (pcase status
('invite "invited")
('join "joined")
('leave "left")
(_ (symbol-name status)))
'face 'font-lock-comment-face)
" on session <" (id user-id) ">.\n\n"
(heading "Avatar: ") (or avatar "") "\n\n"
(heading "ID: ") "<" (id room-id) ">" "\n"
(heading "Alias: ") "<" (id canonical-alias) ">" "\n\n"
(heading "Topic: ") (propertize (or topic "[none]") 'face 'font-lock-comment-face) "\n\n"
(heading "Retrieved events: ") (number-to-string (length timeline)) "\n"
(heading " spanning: ")
(format-time-string "%Y-%m-%d %H:%M:%S"
(/ (ement-event-origin-server-ts
(car (cl-sort (copy-sequence timeline) #'< :key #'ement-event-origin-server-ts)))
1000))
(heading " to ")
(format-time-string "%Y-%m-%d %H:%M:%S\n\n"
(/ (ement-event-origin-server-ts
(car (cl-sort (copy-sequence timeline) #'> :key #'ement-event-origin-server-ts)))
1000))
(heading "Members") " (" (number-to-string (hash-table-count members)) "):\n")
(pcase-dolist (`(,formatted . ,id) member-pairs)
(insert " " (format spec id formatted) "\n")))))
(unless (eq major-mode 'ement-describe-room-mode)
;; Without this check, activating the mode again causes a "Cyclic keymap
;; inheritance" error.
(ement-describe-room-mode))
(pop-to-buffer (current-buffer)))))))
(defalias 'ement-room-describe #'ement-describe-room)
;;;;;; Push rules
;; NOTE: Although v1.4 of the spec is available and describes setting the push rules using
;; the "v3" API endpoint, the Element client continues to use the "r0" endpoint, which is
;; slightly different. This implementation will follow Element's initially, because the
;; spec is not simple, and imitating Element's requests will make it easier.
(defun ement-room-notification-state (room session)
"Return notification state for ROOM on SESSION.
Returns one of nil (meaning default rules are used), `all-loud',
`all', `mentions-and-keywords', or `none'."
;; Following the implementation of getRoomNotifsState() in RoomNotifs.ts in matrix-react-sdk.
;; TODO: Guest support (in which case the state should be `all').
;; TODO: Store account data as a hash table of event types.
(let ((push-rules (cl-find-if (lambda (alist)
(equal "m.push_rules" (alist-get 'type alist)))
(ement-session-account-data session))))
(cl-labels ((override-mute-rule-for-room-p
;; Following findOverrideMuteRule() in RoomNotifs.ts.
(room) (when-let ((overrides (map-nested-elt push-rules '(content global override))))
(cl-loop for rule in overrides
when (and (alist-get 'enabled rule)
(rule-for-room-p rule room))
return rule)))
(rule-for-room-p
;; Following isRuleForRoom() in RoomNotifs.ts.
(rule room) (and (/= 1 (length (alist-get 'conditions rule)))
(pcase-let* ((condition (elt (alist-get 'conditions rule) 0))
((map kind key pattern) condition))
(and (equal "event_match" kind)
(equal "room_id" key)
(equal (ement-room-id room) pattern)))))
(mute-rule-p
(rule) (and (= 1 (length (alist-get 'actions rule)))
(equal "dont_notify" (elt (alist-get 'actions rule) 0))))
(tweak-rule-p
(type rule) (pcase-let (((map ('actions `[,action ,alist])) rule))
(and (equal "notify" action)
(equal type (alist-get 'set_tweak alist))))))
;; If none of these match, nil is returned, meaning that the default rule is used
;; for the room.
(if (override-mute-rule-for-room-p room)
'none
(when-let ((room-rule (cl-find-if (lambda (rule)
(equal (ement-room-id room) (alist-get 'rule_id rule)))
(map-nested-elt push-rules '(content global room)))))
(cond ((not (alist-get 'enabled room-rule))
;; NOTE: According to comment in getRoomNotifsState(), this assumes that
;; the default is to notify for all messages, which "will be 'wrong' for
;; one to one rooms because they will notify loudly for all messages."
'all)
((mute-rule-p room-rule)
;; According to comment, a room-level mute still allows mentions to
;; notify.
'mentions-and-keywords)
((tweak-rule-p "sound" room-rule) 'all-loud)))))))
(defun ement-room-set-notification-state (state room session)
"Set notification STATE for ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room. STATE may be nil to set the rules to
default, `all', `mentions-and-keywords', or `none'."
;; This merely attempts to reproduce the behavior of Element's simple notification
;; options. It does not attempt to offer all of the features defined in the spec. And,
;; yes, it is rather awkward, having to sometimes* make multiple requests of different
;; "kinds" to set the rules for a single room, but that is how the API works.
;;
;; * It appears that Element only makes multiple requests of different kinds when
;; strictly necessary, but coding that logic now would seem likely to be a waste of
;; time, given that Element doesn't even use the latest version of the spec yet. So
;; we'll just do the "dumb" thing and always send requests of both "override" and
;; "room" kinds, which appears to Just Work™.
;;
;; TODO: Match rules to these user-friendly notification states for presentation. See
;; <https://github.com/matrix-org/matrix-react-sdk/blob/8c67984f50f985aa481df24778078030efa39001/src/RoomNotifs.ts>.
;; TODO: Support `all-loud' ("all_messages_loud").
(interactive
(ement-with-room-and-session
(let* ((prompt (format "Set notification rules for %s: " (ement--format-room ement-room)))
(available-states (ement-alist "Default" nil
"All messages" 'all
"Mentions and keywords" 'mentions-and-keywords
"None" 'none))
(selected-rule (completing-read prompt (mapcar #'car available-states) nil t))
(state (alist-get selected-rule available-states nil nil #'equal)))
(list state ement-room ement-session))))
(cl-labels ((set-rule (kind rule queue message-fn)
(pcase-let* (((cl-struct ement-room (id room-id)) room)
(rule-id (url-hexify-string room-id))
(endpoint (format "pushrules/global/%s/%s" kind rule-id))
(method (if rule 'put 'delete))
(then (if rule
;; Setting rules requires PUTting the rules, then making a second
;; request to enable them.
(lambda (_data)
(ement-api session (concat endpoint "/enabled") :queue queue :version "r0"
:method 'put :data (json-encode (ement-alist 'enabled t))
:then message-fn))
message-fn)))
(ement-api session endpoint :queue queue :method method :version "r0"
:data (json-encode rule)
:then then
:else (lambda (plz-error)
(pcase-let* (((cl-struct plz-error response) plz-error)
((cl-struct plz-response status) response))
(pcase status
(404 (pcase rule
(`nil
;; Room already had no rules, so none being found is not an
;; error.
nil)
(_ ;; Unexpected error: re-signal.
(ement-api-error plz-error))))
(_ ;; Unexpected error: re-signal.
(ement-api-error plz-error)))))))))
(pcase-let* ((available-states
(ement-alist
nil (ement-alist
"override" nil
"room" nil)
'all (ement-alist
"override" nil
"room" (ement-alist
'actions (vector "notify" (ement-alist
'set_tweak "sound"
'value "default"))))
'mentions-and-keywords (ement-alist
"override" nil
"room" (ement-alist
'actions (vector "dont_notify")))
'none (ement-alist
"override" (ement-alist
'actions (vector "dont_notify")
'conditions (vector (ement-alist
'kind "event_match"
'key "room_id"
'pattern (ement-room-id room))))
"room" nil)))
(kinds-and-rules (alist-get state available-states nil nil #'equal)))
(cl-loop with queue = (make-plz-queue :limit 1)
with total = (1- (length kinds-and-rules))
for count from 0
for message-fn = (if (equal count total)
(lambda (_data)
(message "Set notification rules for room: %s" (ement--format-room room)))
#'ignore)
for (kind . state) in kinds-and-rules
do (set-rule kind state queue message-fn)))))
;;;;; Public functions
;; These functions could reasonably be called by code in other packages.
(cl-defun ement-put-state
(room type key data session
&key (then (lambda (response-data)
(ement-debug "State data put on room" response-data data room session))))
"Put state event of TYPE with KEY and DATA on ROOM on SESSION.
DATA should be an alist, which will become the JSON request
body."
(declare (indent defun))
(pcase-let* ((endpoint (format "rooms/%s/state/%s/%s"
(url-hexify-string (ement-room-id room))
type key)))
(ement-api session endpoint :method 'put :data (json-encode data)
;; TODO: Handle error codes.
:then then)))
(defun ement-message (format-string &rest args)
"Call `message' on FORMAT-STRING prefixed with \"Ement: \"."
;; TODO: Use this function everywhere we use `message'.
(apply #'message (concat "Ement: " format-string) args))
(cl-defun ement-upload (session &key data filename then else
(content-type "application/octet-stream"))
"Upload DATA with FILENAME to content repository on SESSION.
THEN and ELSE are passed to `ement-api', which see."
(declare (indent defun))
(ement-api session "upload" :method 'post :endpoint-category "media"
;; NOTE: Element currently uses "r0" not "v3", so so do we.
:params (when filename
(list (list "filename" filename)))
:content-type content-type :data data :data-type 'binary
:then then :else else))
(cl-defun ement-complete-session (&key (prompt "Session: "))
"Return an Ement session selected with completion."
(cl-etypecase (length ement-sessions)
((integer 1 1) (cdar ement-sessions))
((integer 2 *) (let* ((ids (mapcar #'car ement-sessions))
(selected-id (completing-read prompt ids nil t)))
(alist-get selected-id ement-sessions nil nil #'equal)))
(otherwise (user-error "No active sessions. Call `ement-connect' to log in"))))
(declare-function ewoc-locate "ewoc")
(defun ement-complete-user-id ()
"Return a user-id selected with completion.
Selects from seen users on all sessions. If point is on an
event, suggests the event's sender as initial input. Allows
unseen user IDs to be input as well."
(cl-labels ((format-user (user)
;; FIXME: Per-room displaynames are now stored in room structs
;; rather than user structs, so to be complete, this needs to
;; iterate over all known rooms, looking for the user's
;; displayname in that room.
(format "%s <%s>"
(ement-user-displayname user)
(ement-user-id user))))
(let* ((display-to-id
(cl-loop for key being the hash-keys of ement-users
using (hash-values value)
collect (cons (format-user value) key)))
(user-at-point (when (equal major-mode 'ement-room-mode)
(when-let ((node (ewoc-locate ement-ewoc)))
(when (ement-event-p (ewoc-data node))
(format-user (ement-event-sender (ewoc-data node)))))))
(selected-user (completing-read "User: " (mapcar #'car display-to-id)
nil nil user-at-point)))
(or (alist-get selected-user display-to-id nil nil #'equal)
selected-user))))
(cl-defun ement-put-account-data
(session type data &key room
(then (lambda (received-data)
;; Handle echoed-back account data event (the spec does not explain this,
;; but see <https://github.com/matrix-org/matrix-react-sdk/blob/675b4271e9c6e33be354a93fcd7807253bd27fcd/src/settings/handlers/AccountSettingsHandler.ts#L150>).
;; FIXME: Make session account-data a map instead of a list of events.
(if room
(push received-data (ement-room-account-data room))
(push received-data (ement-session-account-data session)))
;; NOTE: Commenting out this ement-debug form because a bug in Emacs
;; causes this long string to be interpreted as the function's
;; docstring and cause a too-long-docstring warning.
;; (ement-debug "Account data put and received back on session %s: PUT(json-encoded):%S RECEIVED:%S"
;; (ement-user-id (ement-session-user session)) (json-encode data) received-data)
)))
"Put account data of TYPE with DATA on SESSION.
If ROOM, put it on that room's account data. Also handle the
echoed-back event."
(declare (indent defun))
(pcase-let* (((cl-struct ement-session (user (cl-struct ement-user (id user-id)))) session)
(room-part (if room (format "/rooms/%s" (ement-room-id room)) ""))
(endpoint (format "user/%s%s/account_data/%s" (url-hexify-string user-id) room-part type)))
(ement-api session endpoint :method 'put :data (json-encode data)
:then then)))
(defun ement-redact (event room session &optional reason)
"Redact EVENT in ROOM on SESSION, optionally for REASON."
(pcase-let* (((cl-struct ement-event (id event-id)) event)
((cl-struct ement-room (id room-id)) room)
(endpoint (format "rooms/%s/redact/%s/%s"
room-id event-id (ement--update-transaction-id session)))
(content (ement-alist "reason" reason)))
(ement-api session endpoint :method 'put :data (json-encode content)
:then (lambda (_data)
(message "Event %s redacted." event-id)))))
;;;;; Inline functions
(defsubst ement--user-color (user)
"Return USER's color, setting it if necessary.
USER is an `ement-user' struct."
(or (ement-user-color user)
(setf (ement-user-color user)
(ement--prism-color (ement-user-id user)))))
;;;;; Private functions
;; These functions aren't expected to be called by code in other packages (but if that
;; were necessary, they could be renamed accordingly).
;; (defun ement--room-routing (room)
;; "Return a list of servers to route to ROOM through."
;; ;; See <https://spec.matrix.org/v1.2/appendices/#routing>.
;; ;; FIXME: Ensure highest power level user is at least level 50.
;; ;; FIXME: Ignore servers blocked due to server ACLs.
;; ;; FIXME: Ignore servers which are IP addresses.
;; (cl-labels ((most-powerful-user-in
;; (room))
;; (servers-by-population-in
;; (room))
;; (server-of (user)))
;; (let (first-server-by-power-level)
;; (delete-dups
;; (remq nil
;; (list
;; ;; 1.
;; (or (when-let ((user (most-powerful-user-in room)))
;; (setf first-server-by-power-level t)
;; (server-of user))
;; (car (servers-by-population-in room)))
;; ;; 2.
;; (if first-server-by-power-level
;; (car (servers-by-population-in room))
;; (cl-second (servers-by-population-in room)))
;; ;; 3.
;; (cl-third (servers-by-population-in room))))))))
(defun ement--room-space-p (room)
"Return non-nil if ROOM is a space."
(equal "m.space" (ement-room-type room)))
(defun ement--room-in-space-p (room space)
"Return non-nil if ROOM is in SPACE on SESSION."
;; We could use `ement---room-spaces', but since that returns rooms by looking them up
;; by ID in the session's rooms list, this is more efficient.
(pcase-let* (((cl-struct ement-room (id parent-id) (local (map children))) space)
((cl-struct ement-room (id child-id) (local (map parents))) room))
(or (member parent-id parents)
(member child-id children))))
(defun ement--room-spaces (room session)
"Return list of ROOM's parent spaces on SESSION."
;; NOTE: This only looks in the room's parents list; it doesn't look in every space's children
;; list. This should be good enough, assuming we add to the lists correctly elsewhere.
(pcase-let* (((cl-struct ement-session rooms) session)
((cl-struct ement-room (local (map parents))) room))
(cl-remove-if-not (lambda (session-room-id)
(member session-room-id parents))
rooms :key #'ement-room-id)))
(cl-defun ement--prism-color (string &key (contrast-with (face-background 'default nil 'default)))
"Return a computed color for STRING.
The color is adjusted to have sufficient contrast with the color
CONTRAST-WITH (by default, the default face's background). The
computed color is useful for user messages, generated room
avatars, etc."
;; TODO: Use this instead of `ement-room--user-color'. (Same algorithm ,just takes a
;; string as argument.)
;; TODO: Try using HSV somehow so we could avoid having so many strings return a
;; nearly-black color.
(cl-labels ((relative-luminance
;; Copy of `modus-themes-wcag-formula', an elegant
;; implementation by Protesilaos Stavrou. Also see
;; <https://en.wikipedia.org/wiki/Relative_luminance> and
;; <https://www.w3.org/TR/WCAG20/#relativeluminancedef>.
(rgb) (cl-loop for k in '(0.2126 0.7152 0.0722)
for x in rgb
sum (* k (if (<= x 0.03928)
(/ x 12.92)
(expt (/ (+ x 0.055) 1.055) 2.4)))))
(contrast-ratio
;; Copy of `modus-themes-contrast'; see above.
(a b) (let ((ct (/ (+ (relative-luminance a) 0.05)
(+ (relative-luminance b) 0.05))))
(max ct (/ ct))))
(increase-contrast
(color against target toward)
(let ((gradient (cdr (color-gradient color toward 20)))
new-color)
(cl-loop do (setf new-color (pop gradient))
while new-color
until (>= (contrast-ratio new-color against) target)
;; Avoid infinite loop in case of weirdness
;; by returning color as a fallback.
finally return (or new-color color)))))
(let* ((id string)
(id-hash (float (+ (abs (sxhash id)) ement-room-prism-color-adjustment)))
;; TODO: Wrap-around the value to get the color I want.
(ratio (/ id-hash (float most-positive-fixnum)))
(color-num (round (* (* 255 255 255) ratio)))
(color-rgb (list (/ (float (logand color-num 255)) 255)
(/ (float (lsh (logand color-num 65280) -8)) 255)
(/ (float (lsh (logand color-num 16711680) -16)) 255)))
(contrast-with-rgb (color-name-to-rgb contrast-with)))
(when (< (contrast-ratio color-rgb contrast-with-rgb) ement-room-prism-minimum-contrast)
(setf color-rgb (increase-contrast color-rgb contrast-with-rgb ement-room-prism-minimum-contrast
(color-name-to-rgb
;; Ideally we would use the foreground color,
;; but in some themes, like Solarized Dark,
;; the foreground color's contrast is too low
;; to be effective as the value to increase
;; contrast against, so we use white or black.
(pcase contrast-with
((or `nil "unspecified-bg")
;; The `contrast-with' color (i.e. the
;; default background color) is nil. This
;; probably means that we're displaying on
;; a TTY.
(if (fboundp 'frame--current-backround-mode)
;; This function can tell us whether
;; the background color is dark or
;; light, but it was added in Emacs
;; 28.1.
(pcase (frame--current-backround-mode (selected-frame))
('dark "white")
('light "black"))
;; Pre-28.1: Since faces' colors may be
;; "unspecified" on TTY frames, in which
;; case we have nothing to compare with, we
;; assume that the background color of such
;; a frame is black and increase contrast
;; toward white.
"white"))
(_
;; The `contrast-with` color is usable: test it.
(if (ement--color-dark-p (color-name-to-rgb contrast-with))
"white" "black")))))))
(apply #'color-rgb-to-hex (append color-rgb (list 2))))))
(cl-defun ement--format-user (user &optional (room ement-room) (session ement-session))
"Format `ement-user' USER for ROOM on SESSION.
ROOM defaults to the value of `ement-room'."
(let ((face (cond ((equal (ement-user-id (ement-session-user session))
(ement-user-id user))
'ement-room-self)
(ement-room-prism
`(:inherit ement-room-user :foreground ,(or (ement-user-color user)
(setf (ement-user-color user)
(ement--prism-color user)))))
(t 'ement-room-user))))
;; FIXME: If a membership state event has not yet been received, this
;; sets the display name in the room to the user ID, and that prevents
;; the display name from being used if the state event arrives later.
(propertize (ement--user-displayname-in room user)
'face face
'help-echo (ement-user-id user))))
(cl-defun ement--format-body-mentions
(body room &key (template "<a href=\"https://matrix.to/#/%s\">%s</a>"))
"Return string for BODY with mentions in ROOM linkified with TEMPLATE.
TEMPLATE is a format string in which the first \"%s\" is replaced
with the user's MXID and the second with the displayname. A
mention is qualified by an \"@\"-prefixed displayname or
MXID (optionally suffixed with a colon), or a colon-suffixed
displayname, followed by a blank, question mark, comma, or
period, anywhere in the body."
;; Examples:
;; "@foo: hi"
;; "@foo:matrix.org: hi"
;; "foo: hi"
;; "@foo and @bar:matrix.org: hi"
;; "foo: how about you and @bar ..."
(declare (indent defun))
(cl-labels ((members-having-displayname
;; Iterating over the hash table values isn't as efficient as a hash
;; lookup, but in most rooms it shouldn't be a problem.
(name members) (cl-loop for user being the hash-values of members
when (equal name (ement--user-displayname-in room user))
collect user)))
(pcase-let* (((cl-struct ement-room members) room)
(regexp (rx (or bos bow (1+ blank))
(or (seq (group
;; Group 1: full @-prefixed MXID.
"@" (group
;; Group 2: displayname. (NOTE: Does not work
;; with displaynames containing spaces.)
(1+ (seq (optional ".") alnum)))
(optional ":" (1+ (seq (optional ".") alnum))))
(or ":" eow eos (syntax punctuation)))
(seq (group
;; Group 3: MXID username or displayname.
(1+ (not blank)))
":" (1+ blank)))))
(pos 0) (replace-group) (replacement))
(while (setf pos (string-match regexp body pos))
(if (setf replacement
(or (when-let (member (gethash (match-string 1 body) members))
;; Found user ID: use it as replacement.
(setf replace-group 1)
(format template (match-string 1 body)
(ement--xml-escape-string (ement--user-displayname-in room member))))
(when-let* ((name (or (when (match-string 2 body)
(setf replace-group 1)
(match-string 2 body))
(prog1 (match-string 3 body)
(setf replace-group 3))))
(members (members-having-displayname name members))
(member (when (= 1 (length members))
;; If multiple members are found with the same
;; displayname, do nothing.
(car members))))
;; Found displayname: use it and MXID as replacement.
(format template (ement-user-id member)
(ement--xml-escape-string name)))))
(progn
;; Found member: replace and move to end of replacement.
(setf body (replace-match replacement t t body replace-group))
(let ((difference (- (length replacement) (length (match-string 0 body)))))
(setf pos (if (/= 0 difference)
;; Replacement of a different length: adjust POS accordingly.
(+ pos difference)
(match-end 0)))))
;; No replacement: move to end of match.
(setf pos (match-end 0))))))
body)
(defun ement--event-mentions-room-p (event &rest _ignore)
"Return non-nil if EVENT mentions \"@room\"."
(pcase-let (((cl-struct ement-event (content (map body))) event))
(when body
(string-match-p (rx (or space bos) "@room" eow) body))))
(cl-defun ement-complete-room (&key session (predicate #'identity)
(prompt "Room: ") (suggest t))
"Return a (room session) list selected from SESSION with completion.
If SESSION is nil, select from rooms in all of `ement-sessions'.
When SUGGEST, suggest current buffer's room (or a room at point
in a room list buffer) as initial input (i.e. it should be set to
nil when switching from one room buffer to another). PROMPT may
override the default prompt. PREDICATE may be a function to
select which rooms are offered; it is also applied to the
suggested room."
(declare (indent defun))
(pcase-let* ((sessions (if session
(list session)
(mapcar #'cdr ement-sessions)))
(name-to-room-session
(cl-loop for session in sessions
append (cl-loop for room in (ement-session-rooms session)
when (funcall predicate room)
collect (cons (ement--format-room room 'topic)
(list room session)))))
(names (mapcar #'car name-to-room-session))
(selected-name (completing-read
prompt names nil t
(when suggest
(when-let ((suggestion (ement--room-at-point)))
(when (or (not predicate)
(funcall predicate suggestion))
(ement--format-room suggestion 'topic)))))))
(alist-get selected-name name-to-room-session nil nil #'string=)))
(cl-defun ement-send-message (room session
&key body formatted-body replying-to-event filter then)
"Send message to ROOM on SESSION with BODY and FORMATTED-BODY.
THEN may be a function to call after the event is sent
successfully. It is called with keyword arguments for ROOM,
SESSION, CONTENT, and DATA.
REPLYING-TO-EVENT may be an event the message is
in reply to; the message will reference it appropriately.
FILTER may be a function through which to pass the message's
content object before sending (see,
e.g. `ement-room-send-org-filter')."
(declare (indent defun))
(cl-assert (not (string-empty-p body)))
(cl-assert (or (not formatted-body) (not (string-empty-p formatted-body))))
(pcase-let* (((cl-struct ement-room (id room-id)) room)
(endpoint (format "rooms/%s/send/m.room.message/%s" (url-hexify-string room-id)
(ement--update-transaction-id session)))
(formatted-body (when formatted-body
(ement--format-body-mentions formatted-body room)))
(content (ement-aprog1
(ement-alist "msgtype" "m.text"
"body" body)
(when formatted-body
(push (cons "formatted_body" formatted-body) it)
(push (cons "format" "org.matrix.custom.html") it))))
(then (or then #'ignore)))
(when filter
(setf content (funcall filter content room)))
(when replying-to-event
(setf content (ement--add-reply content replying-to-event room)))
(ement-api session endpoint :method 'put :data (json-encode content)
:then (apply-partially then :room room :session session
;; Data is added when calling back.
:content content :data))))
(defalias 'ement--button-buttonize
;; FIXME: This doesn't set the mouse-face to highlight, and it doesn't use the
;; default-button category. Neither does `button-buttonize', of course, but why?
(if (version< emacs-version "28.1")
(lambda (string callback &optional data)
"Make STRING into a button and return it.
When clicked, CALLBACK will be called with the DATA as the
function argument. If DATA isn't present (or is nil), the button
itself will be used instead as the function argument."
(propertize string
'face 'button
'button t
'follow-link t
'category t
'button-data data
'keymap button-map
'action callback))
#'button-buttonize))
(defun ement--add-reply (data replying-to-event room)
"Return DATA adding reply data for REPLYING-TO-EVENT in ROOM.
DATA is an unsent message event's data alist."
;; SPEC: <https://matrix.org/docs/spec/client_server/r0.6.1#id351> "13.2.2.6.1 Rich replies"
;; FIXME: Rename DATA.
(pcase-let* (((cl-struct ement-event (id replying-to-event-id)
content (sender replying-to-sender))
replying-to-event)
((cl-struct ement-user (id replying-to-sender-id)) replying-to-sender)
((map ('body replying-to-body) ('formatted_body replying-to-formatted-body)) content)
(replying-to-sender-name (ement--user-displayname-in ement-room replying-to-sender))
(quote-string (format "> <%s> %s\n\n" replying-to-sender-name replying-to-body))
(reply-body (alist-get "body" data nil nil #'string=))
(reply-formatted-body (alist-get "formatted_body" data nil nil #'string=))
(reply-body-with-quote (concat quote-string reply-body))
(reply-formatted-body-with-quote
(format "<mx-reply>
<blockquote>
<a href=\"https://matrix.to/#/%s/%s\">In reply to</a>
<a href=\"https://matrix.to/#/%s\">%s</a>
<br />
%s
</blockquote>
</mx-reply>
%s"
(ement-room-id room) replying-to-event-id replying-to-sender-id replying-to-sender-name
;; TODO: Encode HTML special characters. Not as straightforward in Emacs as one
;; might hope: there's `web-mode-html-entities' and `org-entities'. See also
;; <https://emacs.stackexchange.com/questions/8166/encode-non-html-characters-to-html-equivalent>.
(or replying-to-formatted-body replying-to-body)
(or reply-formatted-body reply-body))))
;; NOTE: map-elt doesn't work with string keys, so we use `alist-get'.
(setf (alist-get "body" data nil nil #'string=) reply-body-with-quote
(alist-get "formatted_body" data nil nil #'string=) reply-formatted-body-with-quote
data (append (ement-alist "m.relates_to"
(ement-alist "m.in_reply_to"
(ement-alist "event_id" replying-to-event-id))
"format" "org.matrix.custom.html")
data))
data))
(defun ement--direct-room-for-user (user session)
"Return last-modified direct room with USER on SESSION, if one exists."
;; Loosely modeled on the Element function findDMForUser in createRoom.ts.
(cl-labels ((membership-event-for-p
(event user) (and (equal "m.room.member" (ement-event-type event))
(equal (ement-user-id user) (ement-event-state-key event))))
(latest-membership-for
(user room)
(when-let ((latest-membership-event
(car
(cl-sort
;; I guess we need to check both state and timeline events.
(append (cl-remove-if-not (lambda (event)
(membership-event-for-p event user))
(ement-room-state room))
(cl-remove-if-not (lambda (event)
(membership-event-for-p event user))
(ement-room-timeline room)))
(lambda (a b)
;; Sort latest first so we can use the car.
(> (ement-event-origin-server-ts a)
(ement-event-origin-server-ts b)))))))
(alist-get 'membership (ement-event-content latest-membership-event))))
(latest-event-in
(room) (car
(cl-sort
(append (ement-room-state room)
(ement-room-timeline room))
(lambda (a b)
;; Sort latest first so we can use the car.
(> (ement-event-origin-server-ts a)
(ement-event-origin-server-ts b)))))))
(let* ((direct-rooms (cl-remove-if-not
(lambda (room)
(ement--room-direct-p room session))
(ement-session-rooms session)))
(direct-joined-rooms
;; Ensure that the local user is still in each room.
(cl-remove-if-not
(lambda (room)
(equal "join" (latest-membership-for (ement-session-user session) room)))
direct-rooms))
;; Since we don't currently keep a member list for each room, we look in the room's
;; join events to see if the user has joined or been invited.
(direct-rooms-with-user
(cl-remove-if-not
(lambda (room)
(member (latest-membership-for user room) '("invite" "join")))
direct-joined-rooms)))
(car (cl-sort direct-rooms-with-user
(lambda (a b)
(> (latest-event-in a) (latest-event-in b))))))))
(defun ement--event-replaces-p (a b)
"Return non-nil if event A replaces event B.
That is, if event A replaces B in their
\"m.relates_to\"/\"m.relations\" and \"m.replace\" metadata."
(pcase-let* (((cl-struct ement-event (id a-id)
(content (map ('m.relates_to
(map ('rel_type a-rel-type)
('event_id a-replaces-event-id))))))
a)
((cl-struct ement-event (id b-id)
;; Not sure why this ends up in the unsigned key, but it does.
(unsigned (map ('m.relations
(map ('m.replace
(map ('event_id b-replaced-by-event-id))))))))
b))
(or (and (equal "m.replace" a-rel-type)
(equal a-replaces-event-id b-id))
(equal a-id b-replaced-by-event-id))))
(defun ement--events-equal-p (a b)
"Return non-nil if events A and B are essentially equal.
That is, A and B are either the same event (having the same event
ID), or one event replaces the other (in their m.relates_to and
m.replace metadata)."
(or (equal (ement-event-id a) (ement-event-id b))
(ement--event-replaces-p a b)
(ement--event-replaces-p b a)))
(defun ement--format-room (room &optional topic)
"Return ROOM formatted with name, alias, ID, and optionally TOPIC.
Suitable for use in completion, etc."
(if topic
(format "%s%s(<%s>)%s"
(or (ement-room-display-name room)
(setf (ement-room-display-name room)
(ement--room-display-name room)))
(if (ement-room-canonical-alias room)
(format " <%s> " (ement-room-canonical-alias room))
" ")
(ement-room-id room)
(if (ement-room-topic room)
(format ": \"%s\"" (ement-room-topic room))
""))
(format "%s%s(<%s>)"
(or (ement-room-display-name room)
(setf (ement-room-display-name room)
(ement--room-display-name room)))
(if (ement-room-canonical-alias room)
(format " <%s> " (ement-room-canonical-alias room))
" ")
(ement-room-id room))))
(defun ement--members-alist (room)
"Return alist of member displaynames mapped to IDs seen in ROOM."
;; We map displaynames to IDs because `ement-room--format-body-mentions' needs to find
;; MXIDs from displaynames.
(pcase-let* (((cl-struct ement-room timeline) room)
(members-seen (mapcar #'ement-event-sender timeline))
(members-alist))
(dolist (member members-seen)
;; Testing with `benchmark-run-compiled', it appears that using `cl-pushnew' is
;; about 10x faster than using `delete-dups'.
(cl-pushnew (cons (ement--user-displayname-in room member)
(ement-user-id member))
members-alist))
members-alist))
(defun ement--mxc-to-url (uri session)
"Return HTTPS URL for MXC URI accessed through SESSION."
(pcase-let* (((cl-struct ement-session server) session)
((cl-struct ement-server uri-prefix) server)
(server-name) (media-id))
(string-match (rx "mxc://" (group (1+ (not (any "/"))))
"/" (group (1+ anything))) uri)
(setf server-name (match-string 1 uri)
media-id (match-string 2 uri))
(format "%s/_matrix/media/r0/download/%s/%s"
uri-prefix server-name media-id)))
(defun ement--remove-face-property (string value)
"Remove VALUE from STRING's `face' properties.
Used to remove the `button' face from buttons, because that face
can cause undesirable underlining."
(let ((pos 0))
(cl-loop for next-face-change-pos = (next-single-property-change pos 'face string)
for face-at = (get-text-property pos 'face string)
when face-at
do (put-text-property pos (or next-face-change-pos (length string))
'face (cl-typecase face-at
(atom (if (equal value face-at)
nil face-at))
(list (remove value face-at)))
string)
while next-face-change-pos
do (setf pos next-face-change-pos))))
(defun ement--resize-image (image max-width max-height)
"Return a copy of IMAGE set to MAX-WIDTH and MAX-HEIGHT.
IMAGE should be one as created by, e.g. `create-image'."
;; It would be nice if the image library had some simple functions to do this sort of thing.
(let ((new-image (cl-copy-list image)))
(when (fboundp 'imagemagick-types)
;; Only do this when ImageMagick is supported.
;; FIXME: When requiring Emacs 27+, remove this (I guess?).
(setf (image-property new-image :type) 'imagemagick))
(setf (image-property new-image :max-width) max-width
(image-property new-image :max-height) max-height)
new-image))
(defun ement--room-alias (room)
"Return latest m.room.canonical_alias event in ROOM."
;; FIXME: This function probably needs to compare timestamps to ensure that older events
;; that are inserted at the head of the events lists aren't used instead of newer ones.
(or (cl-loop for event in (ement-room-timeline room)
when (equal "m.room.canonical_alias" (ement-event-type event))
return (alist-get 'alias (ement-event-content event)))
(cl-loop for event in (ement-room-state room)
when (equal "m.room.canonical_alias" (ement-event-type event))
return (alist-get 'alias (ement-event-content event)))))
(declare-function magit-current-section "magit-section")
(declare-function eieio-oref "eieio-core")
(defun ement--room-at-point ()
"Return room at point.
Works in major-modes `ement-room-mode',
`ement-tabulated-room-list-mode', and `ement-room-list-mode'."
(pcase major-mode
('ement-room-mode ement-room)
('ement-tabulated-room-list-mode (tabulated-list-get-id))
('ement-room-list-mode
(cl-typecase (oref (magit-current-section) value)
(taxy-magit-section nil)
(t (pcase (oref (magit-current-section) value)
(`[,room ,_session] room)))))))
(defun ement--room-direct-p (room session)
"Return non-nil if ROOM on SESSION is a direct chat."
(cl-labels ((content-contains-room-id
(content room-id) (cl-loop for (_user-id . room-ids) in content
;; NOTE: room-ids is a vector.
thereis (seq-contains-p room-ids room-id))))
(pcase-let* (((cl-struct ement-session account-data) session)
((cl-struct ement-room id) room))
(or (cl-loop for event in account-data
when (equal "m.direct" (alist-get 'type event))
thereis (content-contains-room-id (alist-get 'content event) id))
(cl-loop
;; Invited rooms have no account-data yet, and their
;; directness flag is in invite-state events.
for event in (ement-room-invite-state room)
thereis (alist-get 'is_direct (ement-event-content event)))))))
(defun ement--room-display-name (room)
"Return the displayname for ROOM."
;; SPEC: <https://matrix.org/docs/spec/client_server/r0.6.1#calculating-the-display-name-for-a-room>.
;; NOTE: The spec seems incomplete, because the algorithm it recommends does not say how
;; or when to use "m.room.member" events for rooms without heroes (e.g. invited rooms).
;; TODO: Add SESSION argument and use it to remove local user from names.
(cl-labels ((latest-event (type content-field)
(or (cl-loop for event in (ement-room-timeline room)
when (and (equal type (ement-event-type event))
(not (string-empty-p (alist-get content-field (ement-event-content event)))))
return (alist-get content-field (ement-event-content event)))
(cl-loop for event in (ement-room-state room)
when (and (equal type (ement-event-type event))
(not (string-empty-p (alist-get content-field (ement-event-content event)))))
return (alist-get content-field (ement-event-content event)))))
(member-events-name
() (when-let ((member-events (cl-loop for accessor in '(ement-room-timeline ement-room-state ement-room-invite-state)
append (cl-remove-if-not (apply-partially #'equal "m.room.member")
(funcall accessor room)
:key #'ement-event-type))))
(string-join (delete-dups
(mapcar (lambda (event)
(ement--user-displayname-in room (ement-event-sender event)))
member-events))
", ")))
(heroes-name
() (pcase-let* (((cl-struct ement-room summary) room)
((map ('m.heroes hero-ids) ('m.joined_member_count joined-count)
('m.invited_member_count invited-count))
summary))
;; TODO: Disambiguate hero display names.
(when hero-ids
(cond ((<= (+ joined-count invited-count) 1)
;; Empty room.
(empty-room hero-ids joined-count))
((>= (length hero-ids) (1- (+ joined-count invited-count)))
;; Members == heroes.
(hero-names hero-ids))
((and (< (length hero-ids) (1- (+ joined-count invited-count)))
(> (+ joined-count invited-count) 1))
;; More members than heroes.
(heroes-and-others hero-ids joined-count))))))
(hero-names
(heroes) (string-join (mapcar #'hero-name heroes) ", "))
(hero-name
(id) (if-let ((user (gethash id ement-users)))
(ement--user-displayname-in room user)
id))
(heroes-and-others
(heroes joined)
(format "%s, and %s others" (hero-names heroes)
(- joined (length heroes))))
(name-override
() (when-let ((event (alist-get "org.matrix.msc3015.m.room.name.override"
(ement-room-account-data room)
nil nil #'equal)))
(map-nested-elt event '(content name))))
(empty-room
(heroes joined) (cl-etypecase (length heroes)
((satisfies zerop) "Empty room")
((number 1 5) (format "Empty room (was %s)"
(hero-names heroes)))
(t (format "Empty room (was %s)"
(heroes-and-others heroes joined))))))
(or (name-override)
(latest-event "m.room.name" 'name)
(latest-event "m.room.canonical_alias" 'alias)
(heroes-name)
(member-events-name)
(ement-room-id room))))
(defun ement--room-favourite-p (room)
"Return non-nil if ROOM is tagged as favourite."
(ement--room-tagged-p "m.favourite" room))
(defun ement--room-low-priority-p (room)
"Return non-nil if ROOM is tagged as low-priority."
(ement--room-tagged-p "m.lowpriority" room))
(defun ement--room-tagged-p (tag room)
"Return non-nil if ROOM has TAG."
;; TODO: Use `make-ement-event' on account-data events.
(pcase-let* (((cl-struct ement-room account-data) room)
(tag-event (alist-get "m.tag" account-data nil nil #'equal)))
(when tag-event
(pcase-let (((map ('content (map tags))) tag-event))
(cl-typecase tag
;; Tags are symbols internally, because `json-read' converts map keys to them.
(string (setf tag (intern tag))))
(assoc tag tags)))))
(defun ement--room-unread-p (room session)
"Return non-nil if ROOM is considered unread for SESSION.
The room is unread if it has a modified, live buffer; if it has
non-zero unread notification counts; or if its fully-read marker
is not at the latest known message event."
;; Roughly equivalent to the "red/gray/bold/idle" states listed in
;; <https://github.com/matrix-org/matrix-react-sdk/blob/b0af163002e8252d99b6d7075c83aadd91866735/docs/room-list-store.md#list-ordering-algorithm-importance>.
(pcase-let* (((cl-struct ement-room timeline account-data unread-notifications receipts
(local (map buffer)))
room)
((cl-struct ement-session user) session)
((cl-struct ement-user (id our-id)) user)
((map notification_count highlight_count) unread-notifications)
(fully-read-event-id (map-nested-elt (alist-get "m.fully_read" account-data nil nil #'equal)
'(content event_id))))
;; MAYBE: Ignore whether the buffer is modified. Since we have a better handle on how
;; Matrix does notifications/unreads/highlights, maybe that's not needed, and it would
;; be more consistent to ignore it.
(or (and buffer (buffer-modified-p buffer))
(and unread-notifications
(or (not (zerop notification_count))
(not (zerop highlight_count))))
;; NOTE: This is *WAY* too complicated, but it seems roughly equivalent to doesRoomHaveUnreadMessages() from
;; <https://github.com/matrix-org/matrix-react-sdk/blob/7fa01ffb068f014506041bce5f02df4f17305f02/src/Unread.ts#L52>.
(when (and (not ement-room-unread-only-counts-notifications)
timeline)
;; A room should rarely, if ever, have a nil timeline, but in case it does
;; (which apparently can happen, given user reports), it should not be
;; considered unread.
(cl-labels ((event-counts-toward-unread-p
;; NOTE: We only consider message events, so membership, reaction,
;; etc. events will not mark a room as unread. Ideally, I think
;; that join/leave events should, at least optionally, mark a room
;; as unread (e.g. in a 1:1 room with a friend, if the other user
;; left, one would probably want to know, and marking the room
;; unread would help the user notice), but since membership events
;; have to be processed to understand their meaning, it's not
;; straightforward to know whether one should mark a room unread.
;; FIXME: Use code from `ement-room--format-member-event' to
;; distinguish ones that should count.
(event) (equal "m.room.message" (ement-event-type event))))
(let ((our-read-receipt-event-id (car (gethash our-id receipts)))
(first-counting-event (cl-find-if #'event-counts-toward-unread-p timeline)))
(cond ((equal fully-read-event-id (ement-event-id (car timeline)))
;; The fully-read marker is at the last known event: the room is read.
nil)
((and (not our-read-receipt-event-id)
(when first-counting-event
(and (not (equal fully-read-event-id (ement-event-id first-counting-event)))
(not (equal our-id (ement-user-id (ement-event-sender first-counting-event)))))))
;; The room has no read receipt, and the latest message event is not
;; the event at which our fully-read marker is at, and it is not sent
;; by us: the room is unread. (This is a kind of failsafe to ensure
;; the user doesn't miss any messages, but it's unclear whether this
;; is really correct or best.)
t)
((equal our-id (ement-user-id (ement-event-sender (car timeline))))
;; We sent the last event: the room is read.
nil)
((and first-counting-event
(equal our-id (ement-user-id (ement-event-sender first-counting-event))))
;; We sent the last message event: the room is read.
nil)
((cl-loop for event in timeline
when (event-counts-toward-unread-p event)
return (and (not (equal our-read-receipt-event-id (ement-event-id event)))
(not (equal fully-read-event-id (ement-event-id event)))))
;; The latest message event is not the event at which our
;; read-receipt or fully-read marker are at: the room is unread.
t))))))))
(defun ement--update-transaction-id (session)
"Return SESSION's incremented transaction ID formatted for sending.
Increments ID and appends current timestamp to avoid reuse
problems."
;; TODO: Naming things is hard.
;; In the event that Emacs isn't killed cleanly and the session isn't saved to disk, the
;; transaction ID would get reused the next time the user connects. To avoid that, we
;; append the current time to the ID. (IDs are just strings, and Element does something
;; similar, so this seems reasonable.)
(format "%s-%s"
(cl-incf (ement-session-transaction-id session))
(format-time-string "%s")))
(defun ement--user-displayname-in (room user)
"Return the displayname for USER in ROOM."
;; SPEC: <https://matrix.org/docs/spec/client_server/r0.6.1#calculating-the-display-name-for-a-user>.
;; FIXME: Add step 3 of the spec. For now we skip to step 4.
;; NOTE: Both state and timeline events must be searched. (A helpful user
;; in #matrix-dev:matrix.org, Michael (t3chguy), clarified this for me).
(if-let ((cached-name (gethash user (ement-room-displaynames room))))
cached-name
;; Put timeline events before state events, because IIUC they should be more recent.
(cl-labels ((join-displayname-event-p
(event) (and (eq user (ement-event-sender event))
(equal "m.room.member" (ement-event-type event))
(equal "join" (alist-get 'membership (ement-event-content event)))
(alist-get 'displayname (ement-event-content event)))))
;; FIXME: Should probably sort the relevant events to get the latest one.
(if-let* ((displayname (or (cl-loop for event in (ement-room-timeline room)
when (join-displayname-event-p event)
return (alist-get 'displayname (ement-event-content event)))
(cl-loop for event in (ement-room-state room)
when (join-displayname-event-p event)
return (alist-get 'displayname (ement-event-content event)))))
(calculated-name displayname))
(puthash user calculated-name (ement-room-displaynames room))
;; No membership state event: use pre-calculated displayname or ID.
(or (ement-user-displayname user)
(ement-user-id user))))))
(defun ement--xml-escape-string (string)
"Return STRING having been escaped with `xml-escape-string'.
Before Emacs 28, ignores `xml-invalid-character' errors (and any
invalid characters cause STRING to remain unescaped). After
Emacs 28, uses the NOERROR argument to `xml-escape-string'."
(condition-case _
(xml-escape-string string 'noerror)
(wrong-number-of-arguments
(condition-case _
(xml-escape-string string)
(xml-invalid-character
;; We still don't want to error on this, so just return the string.
string)))))
(defun ement--mark-room-direct (room session)
"Mark ROOM on SESSION as a direct room.
This may be used to mark rooms as direct which, for whatever
reason (like a bug in your favorite client), were not marked as
such when they were created."
(pcase-let* (((cl-struct ement-room timeline (id room-id)) room)
((cl-struct ement-session (user local-user)) session)
((cl-struct ement-user (id local-user-id)) local-user)
(direct-rooms-account-data-event-content
(alist-get 'content
(cl-find-if (lambda (event)
(equal "m.direct" (alist-get 'type event)))
(ement-session-account-data session))))
(members (delete-dups (mapcar #'ement-event-sender timeline)))
(other-users (cl-remove local-user-id members
:key #'ement-user-id :test #'equal))
((cl-struct ement-user (id other-user-id)) (car other-users))
;; The alist keys are MXIDs as symbols.
(other-user-id (intern other-user-id))
(existing-direct-rooms-for-user (map-elt direct-rooms-account-data-event-content other-user-id)))
(cl-assert (= 1 (length other-users)))
(setf (map-elt direct-rooms-account-data-event-content other-user-id)
(cl-coerce (append existing-direct-rooms-for-user (list room-id))
'vector))
(ement-put-account-data session "m.direct" direct-rooms-account-data-event-content
:then (lambda (_data)
(message "Ement: Room <%s> marked as direct for <%s>." room-id other-user-id)))
(message "Ement: Marking room as direct...")))
(cl-defun ement--get-joined-members (room session &key then else)
"Get joined members in ROOM on SESSION and call THEN with response data.
Or call ELSE with error data if request fails. Also puts members
on `ement-users', updating their displayname and avatar URL
slots, and puts them on ROOM's `members' table."
(declare (indent defun))
(pcase-let* (((cl-struct ement-room id members) room)
(endpoint (format "rooms/%s/joined_members" (url-hexify-string id))))
(ement-api session endpoint
:else else
:then (lambda (data)
(clrhash members)
(mapc (lambda (member)
(pcase-let* ((`(,id-symbol
. ,(map ('avatar_url avatar-url)
('display_name display-name)))
member)
(member-id (symbol-name id-symbol))
(user (or (gethash member-id ement-users)
(puthash member-id (make-ement-user :id member-id)
ement-users))))
(setf (ement-user-displayname user) display-name
(ement-user-avatar-url user) avatar-url)
(puthash member-id user members)))
(alist-get 'joined data))
(setf (alist-get 'fetched-members-p (ement-room-local room)) t)
(when then
;; Finally, call the given callback.
(funcall then data))))
(message "Ement: Getting joined members in %s..." (ement--format-room room))))
(cl-defun ement--human-format-duration (seconds &optional abbreviate)
"Return human-formatted string describing duration SECONDS.
If SECONDS is less than 1, returns \"0 seconds\". If ABBREVIATE
is non-nil, return a shorter version, without spaces. This is a
simple calculation that does not account for leap years, leap
seconds, etc."
;; Copied from `ts-human-format-duration' (same author).
(if (< seconds 1)
(if abbreviate "0s" "0 seconds")
(cl-macrolet ((format> (place)
;; When PLACE is greater than 0, return formatted string using its symbol name.
`(when (> ,place 0)
(format "%d%s%s" ,place
(if abbreviate "" " ")
(if abbreviate
,(substring (symbol-name place) 0 1)
,(symbol-name place)))))
(join-places (&rest places)
;; Return string joining the names and values of PLACES.
`(string-join (delq nil
(list ,@(cl-loop for place in places
collect `(format> ,place))))
(if abbreviate "" ", "))))
(pcase-let ((`(,years ,days ,hours ,minutes ,seconds) (ement--human-duration seconds)))
(join-places years days hours minutes seconds)))))
(defun ement--human-duration (seconds)
"Return list describing duration SECONDS.
List includes years, days, hours, minutes, and seconds. This is
a simple calculation that does not account for leap years, leap
seconds, etc."
;; Copied from `ts-human-format-duration' (same author).
(cl-macrolet ((dividef (place divisor)
;; Divide PLACE by DIVISOR, set PLACE to the remainder, and return the quotient.
`(prog1 (/ ,place ,divisor)
(setf ,place (% ,place ,divisor)))))
(let* ((seconds (floor seconds))
(years (dividef seconds 31536000))
(days (dividef seconds 86400))
(hours (dividef seconds 3600))
(minutes (dividef seconds 60)))
(list years days hours minutes seconds))))
;;; Footer
(provide 'ement-lib)
;;; ement-lib.el ends here
;;; ement-macros.el --- Ement macros -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
;;;; Requirements
(require 'map)
;;;; Debugging
(require 'warnings)
;; NOTE: Uncomment this form and `emacs-lisp-byte-compile-and-load' the file to enable
;; `ement-debug' messages. This is commented out by default because, even though the
;; messages are only displayed when `warning-minimum-log-level' is `:debug' at runtime, if
;; that is so at expansion time, the expanded macro calls format the message and check the
;; log level at runtime, which is not zero-cost.
;; (eval-and-compile
;; (setq-local warning-minimum-log-level nil)
;; (setq-local warning-minimum-log-level :debug))
(cl-defmacro ement-debug (&rest args)
"Display a debug warning showing the runtime value of ARGS.
The warning automatically includes the name of the containing
function, and it is only displayed if `warning-minimum-log-level'
is `:debug' at expansion time (otherwise the macro expands to a
call to `ignore' with ARGS and is eliminated by the
byte-compiler). When debugging, the form also returns nil so,
e.g. it may be used in a conditional in place of nil.
Each of ARGS may be a string, which is displayed as-is, or a
symbol, the value of which is displayed prefixed by its name, or
a Lisp form, which is displayed prefixed by its first symbol.
Before the actual ARGS arguments, you can write keyword
arguments, i.e. alternating keywords and values. The following
keywords are supported:
:buffer BUFFER Name of buffer to pass to `display-warning'.
:level LEVEL Level passed to `display-warning', which see.
Default is :debug."
;; TODO: Can we use a compiler macro to handle this more elegantly?
(pcase-let* ((fn-name (when byte-compile-current-buffer
(with-current-buffer byte-compile-current-buffer
;; This is a hack, but a nifty one.
(save-excursion
(beginning-of-defun)
(cl-second (read (current-buffer)))))))
(plist-args (cl-loop while (keywordp (car args))
collect (pop args)
collect (pop args)))
((map (:buffer buffer) (:level level)) plist-args)
(level (or level :debug))
(string (cl-loop for arg in args
concat (pcase arg
((pred stringp) "%S ")
((pred symbolp)
(concat (upcase (symbol-name arg)) ":%S "))
((pred listp)
(concat "(" (upcase (symbol-name (car arg)))
(pcase (length arg)
(1 ")")
(_ "...)"))
":%S "))))))
(if (eq :debug warning-minimum-log-level)
`(let ((fn-name ,(if fn-name
`',fn-name
;; In an interpreted function: use `backtrace-frame' to get the
;; function name (we have to use a little hackery to figure out
;; how far up the frame to look, but this seems to work).
`(cl-loop for frame in (backtrace-frames)
for fn = (cl-second frame)
when (not (or (subrp fn)
(special-form-p fn)
(eq 'backtrace-frames fn)))
return (make-symbol (format "%s [interpreted]" fn))))))
(display-warning fn-name (format ,string ,@args) ,level ,buffer)
nil)
`(ignore ,@args))))
;;;; Macros
(defmacro ement-alist (&rest pairs)
"Expand to an alist of the keys and values in PAIRS."
`(list ,@(cl-loop for (key value) on pairs by #'cddr
collect `(cons ,key ,value))))
;;;;; Anaphoric
;; We could just depend on dash.el and use --first, and anaphora.el (only
;; on MELPA, not ELPA) has aprog1, but in order to reduce dependencies...
(defmacro ement-afirst (form list)
;; Sometimes checkdoc is really annoying. If I use "FORM returns" or
;; "FORM evaluates", it complains, so I can't have a clean linting.
"Return the first element of LIST for which FORM is non-nil.
In FORM, `it' is bound to the element being tested."
(declare (indent 1))
`(cl-loop for it in ,list
;; Avoid the `when' clause's implicit binding of `it'.
do (when ,form
(cl-return it))))
(defmacro ement-aprog1 (first &rest body)
"Like `prog1', but FIRST's value is bound to `it' around BODY."
(declare (indent 1))
`(let ((it ,first))
,@body
it))
(defmacro ement-singly (place-form &rest body)
"If PLACE-FORM is nil, set it non-nil and eval BODY.
BODY should set PLACE-FORM to nil when BODY is eligible to run
again."
(declare (indent defun))
`(unless ,place-form
(setf ,place-form t)
,@body))
;;;;; Progress reporters
;; MAYBE: Submit a `with-progress-reporter' macro to Emacs.
(defalias 'ement-progress-update #'ignore
"By default, this function does nothing. But inside
`ement-with-progress-reporter', it's bound to a function that
updates the current progress reporter.")
(defmacro ement-with-progress-reporter (args &rest body)
"Eval BODY with a progress reporter according to ARGS.
ARGS is a plist of these values:
:when If specified, a form evaluated at runtime to determine
whether to make and update a progress reporter. If not
specified, the reporter is always made and updated.
:reporter A list of arguments passed to
`make-progress-reporter', which see.
Around BODY, the function `ement-progress-update' is set to a
function that calls `progress-reporter-update' on the progress
reporter (or if the :when form evaluates to nil, the function is
set to `ignore'). It optionally takes a VALUE argument, and
without one, it automatically updates the value from the
reporter's min-value to its max-value."
(declare (indent defun))
(pcase-let* ((progress-reporter-sym (gensym))
(progress-value-sym (gensym))
(start-time-sym (gensym))
((map (:when when-form) (:reporter reporter-args)) args)
(`(,_message ,min-value ,_max-value) reporter-args)
(update-fn `(cl-function
(lambda (&optional (value (cl-incf ,progress-value-sym)))
(ement-debug "Updating progress reporter to" value)
(progress-reporter-update ,progress-reporter-sym value)))))
`(let* ((,start-time-sym (current-time))
(,progress-value-sym (or ,min-value 0))
(,progress-reporter-sym ,(if when-form
`(when ,when-form
(make-progress-reporter ,@reporter-args))
`(make-progress-reporter ,@reporter-args))))
;; We use `cl-letf' rather than `cl-labels', because labels expand to lambdas and funcalls,
;; so other functions that call `ement-progress-update' wouldn't call this definition.
(cl-letf (((symbol-function 'ement-progress-update)
,(if when-form
`(if ,when-form
,update-fn
#'ignore)
update-fn)))
,@body
(ement-debug (format "Ement: Progress reporter done (took %.2f seconds)"
(float-time (time-subtract (current-time) ,start-time-sym))))))))
;;;;; Room-related macros
;; Prevent compiler from complaining that `value' is an unknown slot.
(require 'magit-section)
(cl-defmacro ement-with-room-and-session (&rest body)
"Eval BODY with `ement-room' and `ement-session' bound.
If in an `ement-room-list-mode' buffer and `current-prefix-arg'
is nil, use the room and session at point. If in an `ement-room'
buffer and `current-prefix-arg' is nil, use buffer-local value of
`ement-room' and `ement-session'. Otherwise, prompt for them
with `ement-complete-room' or that given with :prompt-form.
BODY may begin with property list arguments, including:
:prompt-form A Lisp form evaluated for the binding of
`ement-room'."
(declare (indent defun))
(pcase-let* ((plist (cl-loop while (keywordp (car body))
append (list (car body) (cadr body))
and do (setf body (cddr body))))
(prompt-form (or (plist-get plist :prompt-form)
'(ement-complete-room :suggest t))))
`(pcase-let* ((`[,list-room ,list-session] (if (eq 'ement-room-list-mode major-mode)
(oref (magit-current-section) value)
[nil nil]))
(ement-room (or list-room ement-room))
(ement-session (or list-session ement-session)))
(when (or current-prefix-arg (not ement-room))
(pcase-let ((`(,room ,session) ,prompt-form))
(setf ement-room room
ement-session session)))
,@body)))
;;;; Variables
;;;; Customization
;;;; Commands
;;;; Functions
;;;; Footer
(provide 'ement-macros)
;;; ement-macros.el ends here
;;; ement-notify.el --- Notifications for Ement events -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library implements notifications for Ement events.
;;; Code:
;;;; Requirements
(require 'cl-lib)
(require 'map)
(require 'notifications)
(require 'ement-lib)
(require 'ement-room)
(eval-when-compile
(require 'ement-structs))
;;;; Variables
(declare-function ement-room-list "ement-room-list")
(defvar ement-notify-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "S-<return>") #'ement-notify-reply)
(define-key map (kbd "M-g M-l") #'ement-room-list)
(define-key map (kbd "M-g M-m") #'ement-notify-switch-to-mentions-buffer)
(define-key map (kbd "M-g M-n") #'ement-notify-switch-to-notifications-buffer)
(make-composed-keymap (list map button-buffer-map) 'view-mode-map))
"Map for Ement notification buffers.")
(defvar ement-notify-dbus-p
(and (featurep 'dbusbind)
(require 'dbus nil :no-error)
(dbus-ignore-errors (dbus-get-unique-name :session))
;; By default, emacs waits up to 25 seconds for a PONG. Realistically, if there's
;; no pong after 2000ms, there's pretty sure no notification service connected or
;; the system's setup has issues.
(dbus-ping :session "org.freedesktop.Notifications" 2000))
"Whether D-Bus notifications are usable.")
;;;; Customization
(defgroup ement-notify nil
"Notification options."
:group 'ement)
(defcustom ement-notify-ignore-predicates
'(ement-notify--event-not-message-p ement-notify--event-from-session-user-p)
"Display notification if none of these return non-nil for an event.
Each predicate is called with three arguments: the event, the
room, and the session (each the respective struct)."
:type '(repeat (choice (function-item ement-notify--event-not-message-p)
(function-item ement-notify--event-from-session-user-p)
(function :tag "Custom predicate"))))
(defcustom ement-notify-log-predicates
'(ement-notify--event-mentions-session-user-p
ement-notify--event-mentions-room-p
ement-notify--room-buffer-live-p
ement-notify--room-unread-p)
"Predicates to determine whether to log an event to the notifications buffer.
If one of these returns non-nil for an event, the event is logged."
:type 'hook
:options '(ement-notify--event-mentions-session-user-p
ement-notify--event-mentions-room-p
ement-notify--room-buffer-live-p
ement-notify--room-unread-p))
(defcustom ement-notify-mark-frame-urgent-predicates
'(ement-notify--event-mentions-session-user-p
ement-notify--event-mentions-room-p)
"Predicates to determine whether to mark a frame as urgent.
If one of these returns non-nil for an event, the frame that most
recently showed the event's room's buffer is marked
urgent. (Only works on X, not other GUI platforms.)"
:type 'hook
:options '(ement-notify--event-mentions-session-user-p
ement-notify--event-mentions-room-p))
(defcustom ement-notify-mention-predicates
'(ement-notify--event-mentions-session-user-p
ement-notify--event-mentions-room-p)
"Predicates to determine whether to log an event to the mentions buffer.
If one of these returns non-nil for an event, the event is logged."
:type 'hook
:options '(ement-notify--event-mentions-session-user-p
ement-notify--event-mentions-room-p))
(defcustom ement-notify-notification-predicates
'(ement-notify--event-mentions-session-user-p
ement-notify--event-mentions-room-p
ement-notify--room-buffer-live-p
ement-notify--room-unread-p)
"Predicates to determine whether to send a desktop notification.
If one of these returns non-nil for an event, the notification is sent."
:type 'hook
:options '(ement-notify--event-mentions-session-user-p
ement-notify--event-mentions-room-p
ement-notify--room-buffer-live-p
ement-notify--room-unread-p))
(defcustom ement-notify-sound nil
"Sound to play for notifications."
:type '(choice (file :tag "Sound file")
(string :tag "XDG sound name")
(const :tag "Default XDG message sound" "message-new-instant")
(const :tag "Don't play a sound" nil)))
(defcustom ement-notify-limit-room-name-width nil
"Limit the width of room display names in mentions and notifications buffers.
This prevents the margin from being made excessively wide."
:type '(choice (integer :tag "Maximum width")
(const :tag "Unlimited width" nil)))
(defcustom ement-notify-prism-background nil
"Add distinct background color by room to messages in notification buffers.
The color is specific to each room, generated automatically, and
can help distinguish messages by room."
:type 'boolean)
(defcustom ement-notify-room-avatars t
"Show room avatars in the notifications buffers.
This shows room avatars at the left of the window margin in
notification buffers. It's not customizeable beyond that due to
limitations and complexities of displaying strings and images in
margins in Emacs. But it's useful, anyway."
:type 'boolean)
;;;; Commands
(declare-function ement-room-goto-event "ement-room")
(defun ement-notify-button-action (button)
"Show BUTTON's event in its room buffer."
;; TODO: Is `interactive' necessary here?
(interactive)
(let* ((session (button-get button 'session))
(room (button-get button 'room))
(event (button-get button 'event)))
(ement-view-room room session)
(ement-room-goto-event event)))
(defun ement-notify-reply ()
"Send a reply to event at point."
(interactive)
(save-window-excursion
;; Not sure why `call-interactively' doesn't work for `push-button' but oh well.
(push-button)
(call-interactively #'ement-room-write-reply)))
(defun ement-notify-switch-to-notifications-buffer ()
"Switch to \"*Ement Notifications*\" buffer."
(interactive)
(switch-to-buffer (ement-notify--log-buffer "*Ement Notifications*")))
(defun ement-notify-switch-to-mentions-buffer ()
"Switch to \"*Ement Mentions*\" buffer."
(interactive)
(switch-to-buffer (ement-notify--log-buffer "*Ement Mentions*")))
;;;; Functions
(defun ement-notify (event room session)
"Send notifications for EVENT in ROOM on SESSION.
Sends if all of `ement-notify-ignore-predicates' return nil.
Does not do anything if session hasn't finished initial sync."
(when (and (ement-session-has-synced-p session)
(cl-loop for pred in ement-notify-ignore-predicates
never (funcall pred event room session)))
(when (and ement-notify-dbus-p
(run-hook-with-args-until-success 'ement-notify-notification-predicates event room session))
(ement-notify--notifications-notify event room session))
(when (run-hook-with-args-until-success 'ement-notify-log-predicates event room session)
(ement-notify--log-to-buffer event room session))
(when (run-hook-with-args-until-success 'ement-notify-mention-predicates event room session)
(ement-notify--log-to-buffer event room session :buffer-name "*Ement Mentions*"))
(when (run-hook-with-args-until-success 'ement-notify-mark-frame-urgent-predicates event room session)
(ement-notify--mark-frame-urgent event room session))))
(defun ement-notify--mark-frame-urgent (_event room _session)
"Mark frame showing ROOM's buffer as urgent.
If ROOM has no existing buffer, do nothing."
(cl-labels ((mark-frame-urgent
(frame) (let* ((prop "WM_HINTS")
(hints (cl-coerce
(x-window-property prop frame prop nil nil t)
'list)))
(setf (car hints) (logior (car hints) 256))
(x-change-window-property prop hints nil prop 32 t))))
(when-let* ((buffer (alist-get 'buffer (ement-room-local room)))
(frames (cl-loop for frame in (frame-list)
when (eq 'x (framep frame))
collect frame))
(frame (pcase (length frames)
(1 (car frames))
(_
;; Use the frame that most recently showed ROOM's buffer.
(car (sort frames
(lambda (frame-a frame-b)
(let ((a-pos (cl-position buffer (buffer-list frame-a)))
(b-pos (cl-position buffer (buffer-list frame-b))))
(cond ((and a-pos b-pos)
(< a-pos b-pos))
(a-pos)
(b-pos))))))))))
(mark-frame-urgent frame))))
(defun ement-notify--notifications-notify (event room _session)
"Call `notifications-notify' for EVENT in ROOM on SESSION."
(pcase-let* (((cl-struct ement-event sender content) event)
((cl-struct ement-room avatar (display-name room-displayname)) room)
((map body) content)
(room-name (or room-displayname (ement--room-display-name room)))
(sender-name (ement--user-displayname-in room sender))
(title (format "%s in %s" sender-name room-name)))
;; TODO: Encode HTML entities.
(when (stringp body)
;; If event has no body, it was probably redacted or something, so don't notify.
(truncate-string-to-width body 60)
(notifications-notify :title title :body body
:app-name "Ement.el"
:app-icon (when avatar
(ement-notify--temp-file
(plist-get (cdr (get-text-property 0 'display avatar)) :data)))
:category "im.received"
:timeout 5000
;; FIXME: Using :sound-file seems to do nothing, ever. Maybe a bug in notifications-notify?
:sound-file (when (and ement-notify-sound
(file-name-absolute-p ement-notify-sound))
ement-notify-sound)
:sound-name (when (and ement-notify-sound
(not (file-name-absolute-p ement-notify-sound)))
ement-notify-sound)
;; TODO: Show when action used.
;; :actions '("default" "Show")
;; :on-action #'ement-notify-show
))))
(cl-defun ement-notify--temp-file (content &key (timeout 5))
"Return a filename holding CONTENT, and delete it after TIMEOUT seconds."
(let ((filename (make-temp-file "ement-notify--temp-file-"))
(coding-system-for-write 'no-conversion))
(with-temp-file filename
(insert content))
(run-at-time timeout nil (lambda ()
(delete-file filename)))
filename))
(define-derived-mode ement-notify-mode ement-room-mode "Ement Notify"
(setf ement-room-sender-in-left-margin nil
left-margin-width 0
right-margin-width 8)
(setq-local ement-room-message-format-spec "[%o%O] %S> %B%R%t"
bookmark-make-record-function #'ement-notify-bookmark-make-record))
(cl-defun ement-notify--log-to-buffer (event room session &key (buffer-name "*Ement Notifications*"))
"Log EVENT in ROOM on SESSION to \"*Ement Notifications*\" buffer."
(with-demoted-errors "ement-notify--log-to-buffer: %S"
;; HACK: We only log "m.room.message" events for now. This shouldn't be necessary
;; since we have `ement-notify--event-message-p' in `ement-notify-predicates', but
;; just to be safe...
(when (equal "m.room.message" (ement-event-type event))
(with-current-buffer (ement-notify--log-buffer buffer-name)
(let* ((ement-session session)
(ement-room room)
(ement-room-sender-in-left-margin nil)
(ement-room-message-format-spec "%o%O »%W %S> %B%R%t")
(new-node (ement-room--insert-event event))
(inhibit-read-only t)
start end)
(ewoc-goto-node ement-ewoc new-node)
(setf start (point))
(if-let (next-node (ewoc-next ement-ewoc new-node))
(ewoc-goto-node ement-ewoc next-node)
(goto-char (point-max)))
(setf end (- (point) 2))
(add-text-properties start end
(list 'button '(t)
'category 'default-button
'action #'ement-notify-button-action
'session session
'room room
'event event))
;; Remove button face property.
(alter-text-property start end 'face
(lambda (face)
(pcase face
('button nil)
((pred listp) (remq 'button face))
(_ face))))
(when ement-notify-prism-background
(add-face-text-property start end (list :background (ement-notify--room-background-color room)
:extend t))))))))
(defun ement-notify--log-buffer (name)
"Return an Ement notifications buffer named NAME."
(or (get-buffer name)
(with-current-buffer (get-buffer-create name)
(ement-notify-mode)
(current-buffer))))
(defun ement-notify--room-background-color (room)
"Return a background color on which to display ROOM's messages."
(or (alist-get 'notify-background-color (ement-room-local room))
(setf (alist-get 'notify-background-color (ement-room-local room))
(let ((color (color-desaturate-name
(ement--prism-color (ement-room-id room) :contrast-with (face-foreground 'default))
50)))
(if (ement--color-dark-p (color-name-to-rgb (face-background 'default)))
(color-darken-name color 25)
(color-lighten-name color 25))))))
;;;;; Predicates
(defun ement-notify--event-mentions-session-user-p (event room session)
"Return non-nil if EVENT in ROOM mentions SESSION's user.
If EVENT's sender is SESSION's user, returns nil."
(pcase-let* (((cl-struct ement-session user) session)
((cl-struct ement-event sender) event))
(unless (equal (ement-user-id user) (ement-user-id sender))
(ement-room--event-mentions-user-p event user room))))
(defun ement-notify--room-buffer-live-p (_event room _session)
"Return non-nil if ROOM has a live buffer."
(buffer-live-p (alist-get 'buffer (ement-room-local room))))
(defun ement-notify--room-unread-p (_event room _session)
"Return non-nil if ROOM has unread notifications.
According to the room's notification configuration on the server."
(pcase-let* (((cl-struct ement-room unread-notifications) room)
((map notification_count highlight_count) unread-notifications))
(not (and (equal 0 notification_count)
(equal 0 highlight_count)))))
(defun ement-notify--event-message-p (event _room _session)
"Return non-nil if EVENT is an \"m.room.message\" event."
(equal "m.room.message" (ement-event-type event)))
(defun ement-notify--event-not-message-p (event _room _session)
"Return non-nil if EVENT is not an \"m.room.message\" event."
(not (equal "m.room.message" (ement-event-type event))))
(defun ement-notify--event-from-session-user-p (event _room session)
"Return non-nil if EVENT is sent by SESSION's user."
(equal (ement-user-id (ement-session-user session))
(ement-user-id (ement-event-sender event))))
(defalias 'ement-notify--event-mentions-room-p #'ement--event-mentions-room-p)
;;;; Bookmark support
;; Especially useful with Burly: <https://github.com/alphapapa/burly.el>
(require 'bookmark)
(defun ement-notify-bookmark-make-record ()
"Return a bookmark record for the current `ement-notify' buffer."
(list (buffer-name)
;; It seems silly to have to record the buffer name twice, but the
;; `bookmark-make-record' function seems to override the bookmark name sometimes,
;; which makes the result useless unless we save the buffer name separately.
(cons 'buffer-name (buffer-name))
(cons 'handler #'ement-notify-bookmark-handler)))
(defun ement-notify-bookmark-handler (bookmark)
"Show Ement notifications buffer for BOOKMARK."
(pcase-let ((`(,_bookmark-name . ,(map buffer-name)) bookmark))
(switch-to-buffer (ement-notify--log-buffer buffer-name))))
;;;; Footer
(provide 'ement-notify)
;;; ement-notify.el ends here
;; Generated package description from ement.el -*- no-byte-compile: t -*-
(define-package "ement" "0.9.2" "Matrix client" '((emacs "27.1") (map "2.1") (persist "0.5") (plz "0.2") (taxy "0.10") (taxy-magit-section "0.12.1") (svg-lib "0.2.5") (transient "0.3.7")) :commit "7f39fa5694232fa3f0a32b2104187fe1e886c202" :authors '(("Adam Porter" . "adam@alphapapa.net")) :maintainer '("Adam Porter" . "adam@alphapapa.net") :keywords '("comm") :url "https://github.com/alphapapa/ement.el")
;;; ement-room-list.el --- List Ement rooms -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library implements a room list view using `taxy' and `taxy-magit-section' for
;; dynamic, programmable grouping.
;;; Code:
(require 'button)
(require 'rx)
(require 'persist)
(require 'svg-lib)
(require 'taxy)
(require 'taxy-magit-section)
(require 'ement-lib)
(defgroup ement-room-list nil
"Group Ement rooms with Taxy."
:group 'ement)
;;;; Mouse commands
;; Since mouse-activated commands must handle mouse events, we define a simple macro to
;; wrap a command into a mouse-event-accepting one.
(defmacro ement-room-list-define-mouse-command (command)
"Define a command that calls COMMAND interactively with point at mouse event.
COMMAND should be a form that evaluates to a function symbol; if
a symbol, it should be unquoted.."
(let ((docstring (format "Call command `%s' interactively with point at EVENT." command))
(name (intern (format "ement-room-list-mouse-%s" command))))
`(defun ,name (event)
,docstring
(interactive "e")
(mouse-set-point event)
(call-interactively #',command))))
;;;; Variables
(declare-function ement-room-toggle-space "ement-room")
(defvar ement-room-list-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") #'ement-room-list-RET)
(define-key map (kbd "SPC") #'ement-room-list-next-unread)
(define-key map [tab] #'ement-room-list-section-toggle)
(define-key map [mouse-1] (ement-room-list-define-mouse-command ement-room-list-RET))
(define-key map [mouse-2] (ement-room-list-define-mouse-command ement-room-list-kill-buffer))
(define-key map (kbd "k") #'ement-room-list-kill-buffer)
(define-key map (kbd "s") #'ement-room-toggle-space)
map)
"Keymap for `ement-room-list' buffers.
See also `ement-room-list-button-map'.")
(defvar ement-room-list-button-map
;; This map is needed because some columns are propertized as buttons, which override
;; the main keymap.
;; TODO: Is it possible to adjust the button properties to obviate this map?
(let ((map (make-sparse-keymap)))
(define-key map [mouse-1] (ement-room-list-define-mouse-command ement-room-list-RET))
(define-key map [mouse-2] (ement-room-list-define-mouse-command ement-room-list-kill-buffer))
map)
"Keymap for buttonized text in `ement-room-list' buffers.")
(defvar ement-room-list-timestamp-colors nil
"List of colors used for timestamps.
Set automatically when `ement-room-list-mode' is activated.")
(defvar ement-room)
(defvar ement-session)
(defvar ement-sessions)
(defvar ement-room-prism-minimum-contrast)
;;;;; Persistent variables
(persist-defvar ement-room-list-visibility-cache nil
"Applied to `magit-section-visibility-cache', which see.")
;;;; Customization
(defcustom ement-room-list-auto-update t
"Automatically update the taxy-based room list buffer."
:type 'boolean)
(defcustom ement-room-list-avatars (display-images-p)
"Show room avatars in the room list."
:type 'boolean)
;;;;; Faces
(defface ement-room-list-direct
;; In case `font-lock-constant-face' is bold, we set the weight to normal, so it can be
;; made bold for unread rooms only.
'((t (:weight normal :inherit (font-lock-constant-face ement-room-list-name))))
"Direct rooms.")
(defface ement-room-list-favourite '((t (:inherit (font-lock-doc-face ement-room-list-name))))
"Favourite rooms.")
(defface ement-room-list-invited
'((t (:inherit italic ement-room-list-name)))
"Invited rooms.")
(defface ement-room-list-left
'((t (:strike-through t :inherit ement-room-list-name)))
"Left rooms.")
(defface ement-room-list-low-priority '((t (:inherit (font-lock-comment-face ement-room-list-name))))
"Low-priority rooms.")
(defface ement-room-list-name
'((t (:inherit font-lock-function-name-face button)))
"Non-direct rooms.")
(defface ement-room-list-space '((t (:inherit (font-lock-regexp-grouping-backslash ement-room-list-name))))
"Space rooms."
:group 'ement-room-list)
(defface ement-room-list-unread
'((t (:inherit bold ement-room-list-name)))
"Unread rooms.")
(defface ement-room-list-recent '((t (:inherit font-lock-warning-face)))
"Latest timestamp of recently updated rooms.
The foreground color is used to generate a gradient of colors
from recent to non-recent for rooms updated in the past 24
hours but at least one hour ago.")
(defface ement-room-list-very-recent '((t (:inherit error)))
"Latest timestamp of very recently updated rooms.
The foreground color is used to generate a gradient of colors
from recent to non-recent for rooms updated in the past hour.")
;;;; Keys
;; Since some of these keys need access to the session, and room
;; structs don't include the session, we use a two-element vector in
;; which the session is the second element.
(eval-and-compile
(taxy-define-key-definer ement-room-list-define-key
ement-room-list-keys "ement-room-list-key" "FIXME: Docstring."))
(ement-room-list-define-key membership (&key name status)
;; FIXME: Docstring: status should be a symbol of either `invite', `join', `leave'.
(cl-labels ((format-membership (membership)
(pcase membership
('join "Joined")
('invite "Invited")
('leave "[Left]"))))
(pcase-let ((`[,(cl-struct ement-room (status membership)) ,_session] item))
(if status
(when (equal status membership)
(or name (format-membership membership)))
(format-membership membership)))))
(ement-room-list-define-key alias (&key name regexp)
(pcase-let ((`[,(cl-struct ement-room canonical-alias) ,_session] item))
(when canonical-alias
(when (string-match-p regexp canonical-alias)
name))))
(ement-room-list-define-key buffer ()
(pcase-let ((`[,(cl-struct ement-room (local (map buffer))) ,_session] item))
(when buffer
#("Buffers" 0 7 (help-echo "Rooms with open buffers")))))
(ement-room-list-define-key direct ()
(pcase-let ((`[,room ,session] item))
(when (ement--room-direct-p room session)
"Direct")))
(ement-room-list-define-key people ()
(pcase-let ((`[,room ,session] item))
(when (ement--room-direct-p room session)
(propertize "People" 'face 'ement-room-list-direct))))
(ement-room-list-define-key space (&key name id)
(pcase-let* ((`[,room ,session] item)
((cl-struct ement-session rooms) session)
((cl-struct ement-room type (local (map parents))) room))
(cl-labels ((format-space
(id) (let* ((parent-room (cl-find id rooms :key #'ement-room-id :test #'equal))
(space-name (if parent-room
(ement-room-display-name parent-room)
id)))
(concat "Space: " space-name))))
(when-let ((key (if id
;; ID specified.
(cond ((or (member id parents)
(equal id (ement-room-id room)))
;; Room is in specified space.
(or name (format-space id)))
((and (equal type "m.space")
(equal id (ement-room-id room)))
;; Room is a specified space.
(or name (concat "Space: " (ement-room-display-name room)))
))
;; ID not specified.
(pcase (length parents)
(0 nil)
(1
;; TODO: Make the rooms list a hash table to avoid this lookup.
(format-space (car parents)))
(_
;; TODO: How to handle this better? (though it should be very rare)
(string-join (mapcar #'format-space parents) ", "))))))
(propertize key 'face 'ement-room-list-space)))))
(ement-room-list-define-key space-p ()
"Groups rooms that are themselves spaces."
(pcase-let* ((`[,room ,_session] item)
((cl-struct ement-room type) room))
(when (equal "m.space" type)
"Spaces")))
(ement-room-list-define-key name (&key name regexp)
(pcase-let* ((`[,room ,_session] item)
(display-name (ement--room-display-name room)))
(when display-name
(when (string-match-p regexp display-name)
(or name regexp)))))
(ement-room-list-define-key latest (&key name newer-than older-than)
(pcase-let* ((`[,room ,_session] item)
((cl-struct ement-room latest-ts) room)
(age))
(when latest-ts
(setf age (- (time-convert nil 'integer) (/ latest-ts 1000)))
(cond (newer-than
(when (<= age newer-than)
(or name (format "Newer than %s seconds" newer-than))))
(older-than
(when (>= age older-than)
(or name (format "Older than %s seconds" newer-than))))
(t
;; Default to rooms with traffic in the last day.
(if (<= age 86400)
"Last 24 hours"
"Older than 24 hours"))))))
(ement-room-list-define-key freshness
(&key (intervals '((86400 . "Past 24h")
(604800 . "Past week")
(2419200 . "Past month")
(31536000 . "Past year"))))
(pcase-let* ((`[,room ,_session] item)
((cl-struct ement-room latest-ts) room)
(age))
(when latest-ts
(setf age (- (time-convert nil 'integer) (/ latest-ts 1000)))
(or (alist-get age intervals nil nil #'>)
"Older than a year"))))
(ement-room-list-define-key session (&optional user-id)
(pcase-let ((`[,_room ,(cl-struct ement-session
(user (cl-struct ement-user id)))]
item))
(pcase user-id
(`nil id)
(_ (when (equal user-id id)
user-id)))))
(ement-room-list-define-key topic (&key name regexp)
(pcase-let ((`[,(cl-struct ement-room topic) ,_session] item))
(when (and topic (string-match-p regexp topic))
name)))
(ement-room-list-define-key unread ()
(pcase-let ((`[,room ,session] item))
(when (ement--room-unread-p room session)
"Unread")))
(ement-room-list-define-key favourite ()
:then #'identity
(pcase-let ((`[,room ,_session] item))
(when (ement--room-favourite-p room)
(propertize "Favourite" 'face 'ement-room-list-favourite))))
(ement-room-list-define-key low-priority ()
:then #'identity
(pcase-let ((`[,room ,_session] item))
(when (ement--room-low-priority-p room)
"Low-priority")))
(defcustom ement-room-list-default-keys
'(;; First, group all invitations (this group will appear first since the rooms are
;; already sorted first).
((membership :status 'invite))
;; Group all left rooms (this group will appear last, because the rooms are already
;; sorted last).
((membership :status 'leave))
;; Group all favorite rooms, which are already sorted first.
(favourite)
;; Group all low-priority rooms, which are already sorted last, and within that group,
;; group them by their space, if any.
(low-priority space)
;; Group other rooms which are opened in a buffer.
(buffer)
;; Group other rooms which are unread.
(unread)
;; Group other rooms which are in a space by freshness, then by space.
((and :name "Spaced"
:keys ((not space-p)
space))
freshness space)
;; Group spaces themselves by their parent space (since space headers can't also be
;; items, we have to handle them separately; a bit of a hack, but not too bad).
((and :name "Spaces" :keys (space-p))
space)
;; Group rooms which aren't in spaces by their freshness.
((and :name "Unspaced"
:keys ((not space)
(not people)))
freshness)
;; Group direct rooms by freshness.
(people freshness))
"Default keys."
:type 'sexp)
;;;; Columns
(eval-and-compile
(taxy-magit-section-define-column-definer "ement-room-list"))
(ement-room-list-define-column #("🐱" 0 1 (help-echo "Avatar")) (:align 'right)
(pcase-let* ((`[,room ,_session] item)
((cl-struct ement-room avatar display-name
(local (map room-list-avatar)))
room))
(if ement-room-list-avatars
(or room-list-avatar
(let ((new-avatar
(if avatar
;; NOTE: We resize every avatar to be suitable for this buffer, rather than using
;; the one cached in the room's struct. If the buffer's faces change height, this
;; will need refreshing, but it should be worth it to avoid resizing the images on
;; every update.
(propertize " " 'display
(ement--resize-image (get-text-property 0 'display avatar)
nil (frame-char-height)))
;; Room has no avatar: make one.
(let* ((string (or display-name (ement--room-display-name room)))
(ement-room-prism-minimum-contrast 1)
(color (ement--prism-color string :contrast-with "white")))
(when (string-match (rx bos (or "#" "!" "@")) string)
(setf string (substring string 1)))
(propertize " " 'display (svg-lib-tag (substring string 0 1) nil
:background color :foreground "white"
:stroke 0))))))
(setf (alist-get 'room-list-avatar (ement-room-local room)) new-avatar)))
;; Avatars disabled: use a two-space string.
" ")))
(ement-room-list-define-column "Name" (:max-width 25)
(pcase-let* ((`[,room ,session] item)
((cl-struct ement-room type) room)
(display-name (ement--room-display-name room))
(face))
(or (when display-name
;; TODO: Use code from ement-room-list and put in a dedicated function.
(setf face (cl-copy-list '(:inherit (ement-room-list-name))))
;; In concert with the "Unread" column, this is roughly equivalent to the
;; "red/gray/bold/idle" states listed in <https://github.com/matrix-org/matrix-react-sdk/blob/b0af163002e8252d99b6d7075c83aadd91866735/docs/room-list-store.md#list-ordering-algorithm-importance>.
(when (ement--room-unread-p room session)
;; For some reason, `push' doesn't work with `map-elt'...or does it?
(push 'ement-room-list-unread (map-elt face :inherit)))
(when (equal "m.space" type)
(push 'ement-room-list-space (map-elt face :inherit)))
(when (ement--room-direct-p room session)
(push 'ement-room-list-direct (map-elt face :inherit)))
(when (ement--room-favourite-p room)
(push 'ement-room-list-favourite (map-elt face :inherit)))
(when (ement--room-low-priority-p room)
(push 'ement-room-list-low-priority (map-elt face :inherit)))
(pcase (ement-room-status room)
('invite
(push 'ement-room-list-invited (map-elt face :inherit)))
('leave
(push 'ement-room-list-left (map-elt face :inherit))))
(propertize display-name
'face face
'mouse-face 'highlight
'keymap ement-room-list-button-map))
"")))
(ement-room-list-define-column #("Unread" 0 6 (help-echo "Unread events (Notifications:Highlights)")) (:align 'right)
(pcase-let* ((`[,(cl-struct ement-room unread-notifications) ,_session] item)
((map notification_count highlight_count) unread-notifications))
(if (or (not unread-notifications)
(and (equal 0 notification_count)
(equal 0 highlight_count)))
""
(concat (propertize (number-to-string notification_count)
'face (if (zerop highlight_count)
'default
'ement-room-mention))
":"
(propertize (number-to-string highlight_count)
'face 'highlight)))))
(ement-room-list-define-column "Latest" ()
(pcase-let ((`[,(cl-struct ement-room latest-ts) ,_session] item))
(if latest-ts
(let* ((difference-seconds (- (float-time) (/ latest-ts 1000)))
(n (cl-typecase difference-seconds
((number 0 3599) ;; <1 hour: 10-minute periods.
(truncate (/ difference-seconds 600)))
((number 3600 86400) ;; 1 hour to 1 day: 24 1-hour periods.
(+ 6 (truncate (/ difference-seconds 3600))))
(otherwise ;; Difference in weeks.
(min (/ (length ement-room-list-timestamp-colors) 2)
(+ 24 (truncate (/ difference-seconds 86400 7)))))))
(face (list :foreground (elt ement-room-list-timestamp-colors n)))
(formatted-ts (ement--human-format-duration difference-seconds 'abbreviate)))
(string-match (rx (1+ digit) (repeat 1 alpha)) formatted-ts)
(propertize (match-string 0 formatted-ts) 'face face
'help-echo formatted-ts))
"")))
(ement-room-list-define-column "Topic" (:max-width 35)
(pcase-let ((`[,(cl-struct ement-room topic status) ,_session] item))
;; FIXME: Can the status and type unified, or is this inherent to the spec?
(when topic
(setf topic (replace-regexp-in-string "\n" " " topic 'fixedcase 'literal)))
(pcase status
('invite (concat (propertize "[invited]"
'face 'ement-room-list-invited)
" " topic))
('leave (concat (propertize "[left]"
'face 'ement-room-list-left)
" " topic))
(_ (or topic "")))))
(ement-room-list-define-column "Members" (:align 'right)
(pcase-let ((`[,(cl-struct ement-room
(summary (map ('m.joined_member_count member-count))))
,_session]
item))
(if member-count
(number-to-string member-count)
"")))
(ement-room-list-define-column #("Notifications" 0 5 (help-echo "Notification state")) ()
(pcase-let* ((`[,room ,session] item))
(pcase (ement-room-notification-state room session)
('nil "default")
('all-loud "all (loud)")
('all "all")
('mentions-and-keywords "mentions")
('none "none"))))
(ement-room-list-define-column #("B" 0 1 (help-echo "Buffer exists for room")) ()
(pcase-let ((`[,(cl-struct ement-room (local (map buffer))) ,_session] item))
(if buffer
#("B" 0 1 (help-echo "Buffer exists for room"))
" ")))
(ement-room-list-define-column "Session" ()
(pcase-let ((`[,_room ,(cl-struct ement-session (user (cl-struct ement-user id)))] item))
id))
(unless ement-room-list-columns
;; TODO: Automate this or document it
(setq-default ement-room-list-columns
(get 'ement-room-list-columns 'standard-value)))
;;;; Bookmark support
;; Especially useful with Burly: <https://github.com/alphapapa/burly.el>
(require 'bookmark)
(defun ement-room-list-bookmark-make-record ()
"Return a bookmark record for the `ement-room-list' buffer."
(list "*Ement Room List*"
(cons 'handler #'ement-room-list-bookmark-handler)))
(defun ement-room-list-bookmark-handler (bookmark)
"Show `ement-room-list' room list buffer for BOOKMARK."
(pcase-let* ((`(,_bookmark-name . ,_) bookmark))
(unless ement-sessions
;; MAYBE: Automatically connect.
(user-error "No sessions connected: call `ement-connect' first"))
(ement-room-list)))
;;;; Commands
(defun ement-room-list-section-toggle ()
"Toggle the section at point."
;; HACK: For some reason, when a section's body is hidden, then the buffer is refreshed,
;; and then the section's body is shown again, the body is empty--but then, refreshing
;; the buffer shows its body. So we work around that by refreshing the buffer when a
;; section is toggled. In a way, it makes sense to do this anyway, so the user has the
;; most up-to-date information in the buffer. This hack also works around a minor
;; visual bug that sometimes causes room avatars to be displayed in a section heading
;; when a section is hidden.
(interactive)
(ignore-errors
;; Ignore an error in case point is past the top-level section.
(cl-typecase (aref (oref (magit-current-section) value) 0)
(ement-room
;; HACK: Don't hide rooms themselves (they end up permanently hidden).
nil)
(otherwise
(call-interactively #'magit-section-toggle)
(revert-buffer)))))
;;;###autoload
(defun ement-room-list--after-initial-sync (&rest _ignore)
"Call `ement-room-list', ignoring arguments.
To be called from `ement-after-initial-sync-hook'."
(ement-room-list))
;;;###autoload
(defalias 'ement-list-rooms 'ement-room-list)
;;;###autoload
(cl-defun ement-room-list (&key (buffer-name "*Ement Room List*")
(keys ement-room-list-default-keys)
(display-buffer-action '((display-buffer-reuse-window display-buffer-same-window)))
;; visibility-fn
)
"Show a buffer listing Ement rooms, grouped with Taxy KEYS.
After showing it, its window is selected. The buffer is named
BUFFER-NAME and is shown with DISPLAY-BUFFER-ACTION; or if
DISPLAY-BUFFER-ACTION is nil, the buffer is not displayed."
(interactive)
(let ((inhibit-read-only t)
pos format-table column-sizes window-start room-session-vectors)
(cl-labels (;; (heading-face
;; (depth) (list :inherit (list 'bufler-group (bufler-level-face depth))))
(format-item (item) (gethash item format-table))
;; NOTE: Since these functions take an "item" (which is a [room session]
;; vector), they're prefixed "item-" rather than "room-".
(item-latest-ts
(item) (or (ement-room-latest-ts (elt item 0))
;; Room has no latest timestamp. FIXME: This shouldn't
;; happen, but it can, maybe due to oversights elsewhere.
0))
(item-unread-p
(item) (pcase-let ((`[,room ,session] item))
(ement--room-unread-p room session)))
(item-left-p
(item) (pcase-let ((`[,(cl-struct ement-room status) ,_session] item))
(equal 'leave status)))
(item-buffer-p
(item) (pcase-let ((`[,(cl-struct ement-room (local (map buffer))) ,_session] item))
(buffer-live-p buffer)))
(taxy-unread-p
(taxy) (or (cl-some #'item-unread-p (taxy-items taxy))
(cl-some #'taxy-unread-p (taxy-taxys taxy))))
(item-space-p
(item) (pcase-let ((`[,(cl-struct ement-room type) ,_session] item))
(equal "m.space" type)))
(item-favourite-p
(item) (pcase-let ((`[,room ,_session] item))
(ement--room-favourite-p room)))
(item-low-priority-p
(item) (pcase-let ((`[,room ,_session] item))
(ement--room-low-priority-p room)))
(visible-p
;; This is very confusing and doesn't currently work.
(section) (let ((value (oref section value)))
(if (cl-typecase value
(taxy-magit-section (item-unread-p value))
(ement-room nil))
'show
'hide)))
(item-invited-p
(item) (pcase-let ((`[,(cl-struct ement-room status) ,_session] item))
(equal 'invite status)))
(taxy-latest-ts
(taxy) (apply #'max most-negative-fixnum
(delq nil
(list
(when (taxy-items taxy)
(item-latest-ts (car (taxy-items taxy))))
(when (taxy-taxys taxy)
(cl-loop for sub-taxy in (taxy-taxys taxy)
maximizing (taxy-latest-ts sub-taxy)))))))
(t<nil (a b) (and a (not b)))
(t>nil (a b) (and (not a) b))
(make-fn (&rest args)
(apply #'make-taxy-magit-section
:make #'make-fn
:format-fn #'format-item
:level-indent ement-room-list-level-indent
;; :visibility-fn #'visible-p
;; :heading-indent 2
:item-indent 2
;; :heading-face-fn #'heading-face
args)))
;; (when (get-buffer buffer-name)
;; (kill-buffer buffer-name))
(unless ement-sessions
(error "Ement: Not connected. Use `ement-connect' to connect"))
(setf room-session-vectors
(cl-loop for (_id . session) in ement-sessions
append (cl-loop for room in (ement-session-rooms session)
collect (vector room session))))
(with-current-buffer (get-buffer-create buffer-name)
(setf pos (point))
(ement-room-list-mode)
(delete-all-overlays)
(erase-buffer)
(if (not room-session-vectors)
(insert "No joined rooms. Use command `ement-join-room' to join a room, or `ement-directory' or `ement-directory-search' to find rooms.")
(let* ((taxy (cl-macrolet ((first-item
(pred) `(lambda (taxy)
(when (taxy-items taxy)
(,pred (car (taxy-items taxy))))))
(name= (name) `(lambda (taxy)
(equal ,name (taxy-name taxy)))))
(thread-last
(make-fn
:name "Ement Rooms"
:take (taxy-make-take-function keys ement-room-list-keys))
(taxy-fill room-session-vectors)
(taxy-sort #'> #'item-latest-ts)
(taxy-sort #'t<nil #'item-invited-p)
(taxy-sort #'t<nil #'item-favourite-p)
(taxy-sort #'t>nil #'item-low-priority-p)
(taxy-sort #'t<nil #'item-unread-p)
(taxy-sort #'t<nil #'item-space-p)
;; Within each taxy, left rooms should be sorted last so that one
;; can never be the first room in the taxy (unless it's the taxy
;; of left rooms), which would cause the taxy to be incorrectly
;; sorted last.
(taxy-sort #'t>nil #'item-left-p)
(taxy-sort* #'string< #'taxy-name)
(taxy-sort* #'> #'taxy-latest-ts)
(taxy-sort* #'t<nil (name= "Buffers"))
(taxy-sort* #'t<nil (first-item item-unread-p))
(taxy-sort* #'t<nil (first-item item-favourite-p))
(taxy-sort* #'t<nil (first-item item-invited-p))
(taxy-sort* #'t>nil (first-item item-space-p))
(taxy-sort* #'t>nil (name= "Low-priority"))
(taxy-sort* #'t>nil (first-item item-left-p)))))
(taxy-magit-section-insert-indent-items nil)
(format-cons (taxy-magit-section-format-items
ement-room-list-columns ement-room-list-column-formatters taxy))
(section-ident (when (magit-current-section)
(magit-section-ident (magit-current-section)))))
(setf format-table (car format-cons)
column-sizes (cdr format-cons)
header-line-format (taxy-magit-section-format-header
column-sizes ement-room-list-column-formatters)
window-start (if (get-buffer-window buffer-name)
(window-start (get-buffer-window buffer-name))
0))
(when ement-room-list-visibility-cache
(setf magit-section-visibility-cache ement-room-list-visibility-cache))
(add-hook 'kill-buffer-hook #'ement-room-list--cache-visibility nil 'local)
(save-excursion
(taxy-magit-section-insert taxy :items 'first
;; :blank-between-depth bufler-taxy-blank-between-depth
:initial-depth 0))
(goto-char pos)
(when (and section-ident (magit-get-section section-ident))
(goto-char (oref (magit-get-section section-ident) start))))))
(when display-buffer-action
(when-let ((window (display-buffer buffer-name display-buffer-action)))
(select-window window)))
(when (get-buffer-window buffer-name)
(set-window-start (get-buffer-window buffer-name) window-start))
;; NOTE: In order for `bookmark--jump-via' to work properly, the restored buffer
;; must be set as the current buffer, so we have to do this explicitly here.
(set-buffer buffer-name))))
(cl-defun ement-room-list-side-window (&key (side 'left))
"Show room list in side window on SIDE.
Interactively, with prefix, show on right side; otherwise, on
left."
(interactive (when current-prefix-arg
(list :side 'right)))
(let ((display-buffer-mark-dedicated t))
;; Not sure if binding `display-buffer-mark-dedicated' is still necessary.
(ement-room-list
:display-buffer-action `(display-buffer-in-side-window
(dedicated . t)
(side . ,side)
(window-parameters
(no-delete-other-windows . t))))))
(defun ement-room-list-revert (&optional _ignore-auto _noconfirm)
"Revert current Ement-Room-List buffer."
(interactive)
(with-current-buffer "*Ement Room List*"
;; FIXME: This caching of the visibility only supports the main buffer with the
;; default name, not any special ones with different names.
(setf ement-room-list-visibility-cache magit-section-visibility-cache))
(ement-room-list :display-buffer-action nil))
(defun ement-room-list-kill-buffer (room)
"Kill ROOM's buffer."
(interactive
(ement-with-room-and-session
(ignore ement-session)
(list ement-room)))
(pcase-let (((cl-struct ement-room (local (map buffer))) room)
(kill-buffer-query-functions))
(when (buffer-live-p buffer)
(kill-buffer buffer)
(ement-room-list-revert))))
(declare-function ement-view-room "ement-room")
(defun ement-room-list-RET ()
"View room at point, or cycle section at point."
(interactive)
(cl-etypecase (oref (magit-current-section) value)
(vector (pcase-let ((`[,room ,session] (oref (magit-current-section) value)))
(ement-view-room room session)))
(taxy-magit-section (call-interactively #'ement-room-list-section-toggle))
(null nil)))
(declare-function ement-room-goto-fully-read-marker "ement-room")
(defun ement-room-list-next-unread ()
"Show next unread room."
(interactive)
(unless (button-at (point))
(call-interactively #'forward-button))
(unless (cl-loop with starting-line = (line-number-at-pos)
for value = (oref (magit-current-section) value)
for room = (elt value 0)
for session = (elt value 1)
if (ement--room-unread-p room session)
do (progn
(goto-char (button-end (button-at (point))))
(push-button (1- (point)))
(ement-room-goto-fully-read-marker)
(cl-return t))
else do (call-interactively #'forward-button)
while (> (line-number-at-pos) starting-line))
;; No more unread rooms.
(message "No more unread rooms")))
(define-derived-mode ement-room-list-mode magit-section-mode "Ement-Room-List"
:global nil
(setq-local bookmark-make-record-function #'ement-room-list-bookmark-make-record
revert-buffer-function #'ement-room-list-revert
ement-room-list-timestamp-colors (ement-room-list--timestamp-colors)))
;;;; Functions
(defun ement-room-list--cache-visibility ()
"Save visibility cache.
Sets `ement-room-list-visibility-cache' to the value of
`magit-section-visibility-cache'. To be called in
`kill-buffer-hook'."
(ignore-errors
(when magit-section-visibility-cache
(setf ement-room-list-visibility-cache magit-section-visibility-cache))))
;;;###autoload
(defun ement-room-list-auto-update (_session)
"Automatically update the Taxy room list buffer.
+Does so when variable `ement-room-list-auto-update' is non-nil.
+To be called in `ement-sync-callback-hook'."
(when (and ement-room-list-auto-update
(buffer-live-p (get-buffer "*Ement Room List*")))
(with-current-buffer (get-buffer "*Ement Room List*")
(unless (region-active-p)
;; Don't refresh the list if the region is active (e.g. if the user is trying to
;; operate on multiple rooms).
(revert-buffer)))))
(defun ement-room-list--timestamp-colors ()
"Return a vector of generated latest-timestamp colors for rooms.
Used in `ement-tabulated-room-list' and `ement-room-list'."
(if (or (equal "unspecified-fg" (face-foreground 'default nil 'default))
(equal "unspecified-bg" (face-background 'default nil 'default)))
;; NOTE: On a TTY, the default face's foreground and background colors may be the
;; special values "unspecified-fg"/"unspecified-bg", in which case we can't generate
;; gradients, so we just return a vector of "unspecified-fg". See
;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=55623>.
(make-vector 134 "unspecified-fg")
(cl-coerce
(append (mapcar
;; One face per 10-minute period, from "recent" to 1-hour.
(lambda (rgb)
(pcase-let ((`(,r ,g ,b) rgb))
(color-rgb-to-hex r g b 2)))
(color-gradient (color-name-to-rgb (face-foreground 'ement-room-list-very-recent
nil 'default))
(color-name-to-rgb (face-foreground 'ement-room-list-recent
nil 'default))
6))
(mapcar
;; One face per hour, from "recent" to default.
(lambda (rgb)
(pcase-let ((`(,r ,g ,b) rgb))
(color-rgb-to-hex r g b 2)))
(color-gradient (color-name-to-rgb (face-foreground 'ement-room-list-recent
nil 'default))
(color-name-to-rgb (face-foreground 'default nil 'default))
24))
(mapcar
;; One face per week for the last year (actually we
;; generate colors for the past two years' worth so
;; that the face for one-year-ago is halfway to
;; invisible, and we don't use colors past that point).
(lambda (rgb)
(pcase-let ((`(,r ,g ,b) rgb))
(color-rgb-to-hex r g b 2)))
(color-gradient (color-name-to-rgb (face-foreground 'default nil 'default))
(color-name-to-rgb (face-background 'default nil 'default))
104)))
'vector)))
;;;; Footer
(provide 'ement-room-list)
;;; ement-room-list.el ends here
;;; ement-room.el --- Ement room buffers -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library implements buffers displaying events in a room.
;; EWOC is a great library. If I had known about it and learned it
;; sooner, it would have saved me a lot of time in other projects.
;; I'm glad I decided to try it for this one.
;;; Code:
;;;; Debugging
;; NOTE: Uncomment this form and `emacs-lisp-byte-compile-and-load' the file to enable
;; `ement-debug' messages. This is commented out by default because, even though the
;; messages are only displayed when `warning-minimum-log-level' is `:debug' at runtime, if
;; that is so at expansion time, the expanded macro calls format the message and check the
;; log level at runtime, which is not zero-cost.
;; (eval-and-compile
;; (setq-local warning-minimum-log-level nil)
;; (setq-local warning-minimum-log-level :debug))
;;;; Requirements
(require 'color)
(require 'ewoc)
(require 'mailcap)
(require 'shr)
(require 'subr-x)
(require 'mwheel)
(require 'dnd)
(require 'ement-api)
(require 'ement-lib)
(require 'ement-macros)
(require 'ement-structs)
;;;; Structs
(cl-defstruct ement-room-membership-events
"Struct grouping membership events.
After adding events, use `ement-room-membership-events--update'
to sort events and update other slots."
(events nil :documentation "Membership events, latest first.")
(earliest-ts nil :documentation "Timestamp of earliest event.")
(latest-ts nil :documentation "Timestamp of latest event."))
(defun ement-room-membership-events--update (struct)
"Return STRUCT having sorted its events and updated its slots."
;; Like the room timeline slot, events are sorted latest-first. We also deduplicate
;; them , because it seems that we can end up with multiple copies of a membership event
;; (e.g. when loading old messages).
(setf (ement-room-membership-events-events struct) (cl-delete-duplicates (ement-room-membership-events-events struct)
:key #'ement-event-id :test #'equal)
(ement-room-membership-events-events struct) (cl-sort (ement-room-membership-events-events struct) #'>
:key #'ement-event-origin-server-ts)
(ement-room-membership-events-earliest-ts struct) (ement-event-origin-server-ts
(car (last (ement-room-membership-events-events struct))))
(ement-room-membership-events-latest-ts struct) (ement-event-origin-server-ts
(car (ement-room-membership-events-events struct))))
struct)
;;;; Variables
(defvar-local ement-ewoc nil
"EWOC for Ement room buffers.")
(defvar-local ement-room nil
"Ement room for current buffer.")
(defvar-local ement-session nil
"Ement session for current buffer.")
(defvar-local ement-room-retro-loading nil
"Non-nil when earlier messages are being loaded.
Used to avoid overlapping requests.")
(defvar-local ement-room-replying-to-event nil
"When non-nil, the user is replying to this event.
Used by `ement-room-send-message'.")
(defvar-local ement-room-replying-to-overlay nil
"Used by `ement-room-write-reply'.")
(defvar-local ement-room-read-receipt-request nil
"Maps event ID to request updating read receipt to that event.
An alist of one entry.")
(defvar ement-room-read-string-setup-hook nil
"Normal hook run by `ement-room-read-string' after switching to minibuffer.
Should be used to, e.g. propagate variables to the minibuffer.")
(defvar ement-room-compose-hook nil
"Hook run in compose buffers when created.
Used to, e.g. call `ement-room-compose-org'.")
(declare-function ement-room-list "ement-room-list.el")
(declare-function ement-notify-switch-to-mentions-buffer "ement-notify")
(declare-function ement-notify-switch-to-notifications-buffer "ement-notify")
(defvar ement-room-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "?") #'ement-room-transient)
;; Movement
(define-key map (kbd "TAB") #'ement-room-goto-next)
(define-key map (kbd "<backtab>") #'ement-room-goto-prev)
(define-key map (kbd "SPC") #'ement-room-scroll-up-mark-read)
(define-key map (kbd "S-SPC") #'ement-room-scroll-down-command)
(define-key map (kbd "M-SPC") #'ement-room-goto-fully-read-marker)
(define-key map [remap scroll-down-command] #'ement-room-scroll-down-command)
(define-key map [remap mwheel-scroll] #'ement-room-mwheel-scroll)
;; Switching
(define-key map (kbd "M-g M-l") #'ement-room-list)
(define-key map (kbd "M-g M-r") #'ement-view-room)
(define-key map (kbd "M-g M-m") #'ement-notify-switch-to-mentions-buffer)
(define-key map (kbd "M-g M-n") #'ement-notify-switch-to-notifications-buffer)
(define-key map (kbd "q") #'quit-window)
;; Messages
(define-key map (kbd "RET") #'ement-room-send-message)
(define-key map (kbd "S-<return>") #'ement-room-write-reply)
(define-key map (kbd "M-RET") #'ement-room-compose-message)
(define-key map (kbd "<insert>") #'ement-room-edit-message)
(define-key map (kbd "C-k") #'ement-room-delete-message)
(define-key map (kbd "s r") #'ement-room-send-reaction)
(define-key map (kbd "s e") #'ement-room-send-emote)
(define-key map (kbd "s f") #'ement-room-send-file)
(define-key map (kbd "s i") #'ement-room-send-image)
(define-key map (kbd "v") #'ement-room-view-event)
;; Users
(define-key map (kbd "u RET") #'ement-send-direct-message)
(define-key map (kbd "u i") #'ement-invite-user)
(define-key map (kbd "u I") #'ement-ignore-user)
;; Room
(define-key map (kbd "M-s o") #'ement-room-occur)
(define-key map (kbd "r d") #'ement-describe-room)
(define-key map (kbd "r m") #'ement-list-members)
(define-key map (kbd "r t") #'ement-room-set-topic)
(define-key map (kbd "r f") #'ement-room-set-message-format)
(define-key map (kbd "r n") #'ement-room-set-notification-state)
(define-key map (kbd "r N") #'ement-room-override-name)
(define-key map (kbd "r T") #'ement-tag-room)
;; Room membership
(define-key map (kbd "R c") #'ement-create-room)
(define-key map (kbd "R j") #'ement-join-room)
(define-key map (kbd "R l") #'ement-leave-room)
(define-key map (kbd "R F") #'ement-forget-room)
(define-key map (kbd "R n") #'ement-room-set-display-name)
(define-key map (kbd "R s") #'ement-room-toggle-space)
;; Other
(define-key map (kbd "g") #'ement-room-sync)
map)
"Keymap for Ement room buffers.")
(defvar ement-room-minibuffer-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map minibuffer-local-map)
(define-key map (kbd "C-c '") #'ement-room-compose-from-minibuffer)
map)
"Keymap used in `ement-room-read-string'.")
(defvar ement-room-sender-in-headers nil
"Non-nil when sender is displayed in headers.
In that case, sender names are aligned to the margin edge.")
(defvar ement-room-messages-filter
'((lazy_load_members . t))
;; NOTE: The confusing differences between what /sync and /messages
;; expect. See <https://github.com/matrix-org/matrix-doc/issues/706>.
"Default RoomEventFilter for /messages requests.")
(defvar ement-room-typing-timer nil
"Timer used to send notifications while typing.")
(defvar ement-room-matrix.to-url-regexp
(rx "http" (optional "s") "://"
"matrix.to" "/#/"
(group (or "!" "#") (1+ (not (any "/"))))
(optional "/" (group "$" (1+ (not (any "?" "/")))))
(optional "?" (group (1+ anything))))
"Regexp matching \"matrix.to\" URLs.")
;; Variables from other files.
(defvar ement-sessions)
(defvar ement-syncs)
(defvar ement-auto-sync)
(defvar ement-users)
(defvar ement-images-queue)
(defvar ement-notify-limit-room-name-width)
(defvar ement-view-room-display-buffer-action)
;; Defined in Emacs 28.1: silence byte-compilation warning in earlier versions.
(defvar browse-url-handlers)
;;;; Customization
(defgroup ement-room nil
"Options for room buffers."
:group 'ement)
(defcustom ement-room-timestamp-header-align 'right
"Where to align timestamp headers."
:type '(choice (const :tag "Left" left)
(const :tag "Center" center)
(const :tag "Right" right)))
(defcustom ement-room-view-hook
'(ement-room-view-hook-room-list-auto-update)
"Functions called when `ement-room-view' is called.
Called with two arguments, the room and the session."
:type 'hook)
;;;;; Faces
(defface ement-room-name
'((t (:inherit font-lock-function-name-face)))
"Room name shown in header line.")
(defface ement-room-membership
'((t (:height 0.8 :inherit font-lock-comment-face)))
"Membership events (join/part).")
(defface ement-room-reactions
'((t (:inherit font-lock-comment-face :height 0.9)))
"Reactions to messages (including the user count).")
(defface ement-room-reactions-key
'((t (:inherit ement-room-reactions :height 1.5)))
"Reactions to messages (the key, i.e. the emoji part).
Uses a separate face to allow the key to be shown at a different
size, because in some fonts, emojis are too small relative to
normal text.")
(defface ement-room-timestamp
'((t (:inherit font-lock-comment-face)))
"Event timestamps.")
(defface ement-room-user
'((t (:inherit font-lock-function-name-face :weight bold :overline t)))
"Usernames.")
(defface ement-room-self
'((t (:inherit (font-lock-variable-name-face ement-room-user) :weight bold)))
"Own username.")
(defface ement-room-message-text
'((t (:inherit default)))
"Text message bodies.")
(defface ement-room-message-emote
'((t (:inherit italic)))
"Emote message bodies.")
(defface ement-room-redacted
'((t (:strike-through t)))
"Redacted messages.")
(defface ement-room-self-message
'((t (:inherit (font-lock-variable-name-face))))
"Oneself's message bodies.
Note that this does not need to inherit
`ement-room-message-text', because that face is combined with
this one automatically.")
(defface ement-room-timestamp-header
'((t (:inherit header-line :weight bold :height 1.1)))
"Timestamp headers.")
(defface ement-room-mention
(if (version< emacs-version "27.1")
'((t (:inherit hl-line)))
'((t (:inherit hl-line :extend t))))
"Messages that mention the local user.")
(defface ement-room-wrap-prefix
`((t :inherit highlight))
"Face applied to `ement-room-wrap-prefix', which see.")
;;;;; Options
(defcustom ement-room-ellipsis "⋮"
"String used when abbreviating certain strings."
:type 'string)
(defcustom ement-room-avatars (display-images-p)
"Show room avatars."
:type 'boolean)
(defcustom ement-room-avatar-max-width 32
"Maximum width in pixels of room avatars shown in header lines."
:type 'integer)
(defcustom ement-room-avatar-max-height 32
"Maximum height in pixels of room avatars shown in header lines."
:type 'integer)
(defcustom ement-room-coalesce-events t
"Coalesce certain events in room buffers.
For example, membership events can be overwhelming in large
rooms, especially ones bridged to IRC. This option groups them
together so they take less space."
:type 'boolean)
(defcustom ement-room-header-line-format
;; TODO: Show in new screenshots.
'(:eval (concat (if ement-room-avatars
(or (ement-room-avatar ement-room)
"")
"")
" " (propertize (ement-room--escape-%
(or (ement-room-display-name ement-room)
"[no room name]"))
'face 'ement-room-name)
": " (propertize (ement-room--escape-%
(or (ement-room-topic ement-room)
"[no topic]"))
;; Also set help-echo in case the topic is too wide to fit.
'help-echo (ement-room-topic ement-room))))
"Header line format for room buffers.
See Info node `(elisp)Header lines'."
:type 'sexp)
(put 'ement-room-header-line-format 'risky-local-variable t)
(defcustom ement-room-buffer-name-prefix "*Ement Room: "
"Prefix for Ement room buffer names."
:type 'string)
(defcustom ement-room-buffer-name-suffix "*"
"Suffix for Ement room buffer names."
:type 'string)
(defcustom ement-room-timestamp-format "%H:%M:%S"
"Format string for event timestamps.
See function `format-time-string'."
:type '(choice (const "%H:%M:%S")
(const "%Y-%m-%d %H:%M:%S")
string))
(defcustom ement-room-left-margin-width 0
"Width of left margin in room buffers.
When using a non-graphical display, this should be set slightly
wider than when using a graphical display, to prevent sender
display names from colliding with event text."
:type 'integer)
(defcustom ement-room-right-margin-width (length ement-room-timestamp-format)
"Width of right margin in room buffers."
:type 'integer)
(defcustom ement-room-sender-headers t
"Show sender headers.
Automatically set by setting `ement-room-message-format-spec',
but may be overridden manually."
:type 'boolean)
(defcustom ement-room-unread-only-counts-notifications t
"Only use notification counts to mark rooms unread.
Notification counts are set by the server based on each room's
notification settings. Otherwise, whether a room is marked
unread depends on the room's fully-read marker, read-receipt
marker, whether the local user sent the latest events, etc."
:type 'boolean)
(defvar ement-room-sender-in-left-margin nil
"Whether sender is shown in left margin.
Set by `ement-room-message-format-spec-setter'.")
(defun ement-room-message-format-spec-setter (option value &optional local)
"Set relevant options for `ement-room-message-format-spec', which see.
To be used as that option's setter. OPTION and VALUE are
received from setting the customization option. If LOCAL is
non-nil, set the variables buffer-locally (i.e. when called from
`ement-room-set-message-format'."
(cl-macrolet ((set-vars (&rest pairs)
;; Set variable-value pairs, locally if LOCAL is non-nil.
`(progn
,@(cl-loop for (symbol value) on pairs by #'cddr
collect `(if local
(set (make-local-variable ',symbol) ,value)
(set ',symbol ,value))))))
(if local
(set (make-local-variable option) value)
(set-default option value))
(pcase value
;; Try to set the margin widths smartly.
("%B%r%R%t" ;; "Elemental"
(set-vars ement-room-left-margin-width 0
ement-room-right-margin-width 8
ement-room-sender-headers t
ement-room-sender-in-headers t
ement-room-sender-in-left-margin nil))
("%S%L%B%r%R%t" ;; "IRC-style using margins"
(set-vars ement-room-left-margin-width 12
ement-room-right-margin-width 8
ement-room-sender-headers nil
ement-room-sender-in-headers nil
ement-room-sender-in-left-margin t))
("[%t] %S> %B%r" ;; "IRC-style without margins"
(set-vars ement-room-left-margin-width 0
ement-room-right-margin-width 0
ement-room-sender-headers nil
ement-room-sender-in-headers nil
ement-room-sender-in-left-margin nil))
(_ (set-vars ement-room-left-margin-width
(if (string-match-p "%L" value)
12 0)
ement-room-right-margin-width
(if (string-match-p "%R" value)
8 0)
ement-room-sender-in-left-margin
(if (string-match-p (rx (1+ anything) (or "%S" "%s") (1+ anything) "%L") value)
t nil)
;; NOTE: The following two variables may seem redundant, but one is an
;; option that the user may override, while the other is set
;; automatically.
ement-room-sender-headers
(if (string-match-p (or "%S" "%s") value)
;; If "%S" or "%s" isn't found, assume it's to be shown in headers.
nil t)
ement-room-sender-in-headers
(if (string-match-p (rx (or "%S" "%s")) value)
;; If "%S" or "%s" isn't found, assume it's to be shown in headers.
nil t))
(message "Ement: When using custom message format, setting margin widths may be necessary")))
(unless ement-room-sender-in-headers
;; HACK: Disable overline on sender face.
(require 'face-remap)
(if local
(progn
(face-remap-reset-base 'ement-room-user)
(face-remap-add-relative 'ement-room-user '(:overline nil)))
(set-face-attribute 'ement-room-user nil :overline nil)))
(unless local
(when (and (bound-and-true-p ement-sessions) (car ement-sessions))
;; Only display when a session is connected (not sure why `bound-and-true-p'
;; is required to avoid compilation warnings).
(message "Ement: Kill and reopen room buffers to display in new format")))))
(defcustom ement-room-message-format-spec "%S%L%B%r%R%t"
"Format messages according to this spec.
It may contain these specifiers:
%L End of left margin
%R Start of right margin
%W End of wrap-prefix
%b Message body (plain-text)
%B Message body (formatted if available)
%i Event ID
%O Room display name (used for mentions buffer)
%r Reactions
%s Sender ID
%S Sender display name
%t Event timestamp, formatted according to
`ement-room-timestamp-format'
Note that margin sizes must be set manually with
`ement-room-left-margin-width' and
`ement-room-right-margin-width'."
:type '(choice (const :tag "IRC-style using margins" "%S%L%B%r%R%t")
(const :tag "IRC-style without margins" "[%t] %S> %B%r")
(const :tag "IRC-style without margins, with wrap-prefix" "[%t] %S> %W%B%r")
(const :tag "IRC-style with right margin, with wrap-prefix" "%S> %W%B%r%R%t")
(const :tag "Elemental" "%B%r%R%t")
(string :tag "Custom format"))
:set #'ement-room-message-format-spec-setter
:set-after '(ement-room-left-margin-width ement-room-right-margin-width
ement-room-sender-headers)
;; This file must be loaded before calling the setter to define the
;; `ement-room-user' face used in it.
:require 'ement-room)
(defcustom ement-room-retro-messages-number 30
"Number of messages to retrieve when loading earlier messages."
:type 'integer)
(defcustom ement-room-timestamp-header-format " %H:%M "
"Format string for timestamp headers where date is unchanged.
See function `format-time-string'. If this string ends in a
newline, its background color will extend to the end of the
line."
:type '(choice (const :tag "Time-only" " %H:%M ")
(const :tag "Always show date" " %Y-%m-%d %H:%M ")
string))
(defcustom ement-room-timestamp-header-with-date-format " %Y-%m-%d (%A)\n"
;; FIXME: In Emacs 27+, maybe use :extend t instead of adding a newline.
"Format string for timestamp headers where date changes.
See function `format-time-string'. If this string ends in a
newline, its background color will extend to the end of the
line."
:type '(choice (const " %Y-%m-%d (%A)\n")
string))
(defcustom ement-room-replace-edited-messages t
"Replace edited messages with their new content.
When nil, edited messages are displayed as new messages, leaving
the original messages visible."
:type 'boolean)
(defcustom ement-room-shr-use-fonts nil
"Enable `shr' variable-pitch fonts for formatted bodies.
If non-nil, `shr' may use variable-pitch fonts for formatted
bodies (which include most replies), which means that some
messages won't display in the same font as others."
:type '(choice (const :tag "Disable variable-pitch fonts" nil)
(const :tag "Enable variable-pitch fonts" t)))
(defcustom ement-room-username-display-property '(raise -0.25)
"Display property applied to username strings.
See Info node `(elisp)Other Display Specs'."
:type '(choice (list :tag "Raise" (const raise :tag "Raise") (number :tag "Factor"))
(list :tag "Height" (const height)
(choice (list :tag "Larger" (const + :tag "Larger") (number :tag "Steps"))
(list :tag "Smaller" (const - :tag "Smaller") (number :tag "Steps"))
(number :tag "Factor")
(function :tag "Function")
(sexp :tag "Form"))) ))
(defcustom ement-room-event-separator-display-property '(space :ascent 50)
"Display property applied to invisible space string after events.
Allows visual separation between events without, e.g. inserting
newlines.
See Info node `(elisp)Specified Space'."
:type 'sexp)
(defcustom ement-room-timestamp-header-delta 600
"Show timestamp header where events are at least this many seconds apart."
:type 'integer)
(defcustom ement-room-send-message-filter nil
"Function through which to pass message content before sending.
Used to, e.g. send an Org-formatted message by exporting it to
HTML first."
:type '(choice (const :tag "Send messages as-is" nil)
(const :tag "Send messages in Org format" ement-room-send-org-filter)
(function :tag "Custom filter function"))
:set (lambda (option value)
(set-default option value)
(pcase value
('ement-room-send-org-filter
;; Activate in compose buffer by default.
(add-hook 'ement-room-compose-hook #'ement-room-compose-org))
(_ (remove-hook 'ement-room-compose-hook #'ement-room-compose-org)))))
(defcustom ement-room-mark-rooms-read t
"Mark rooms as read automatically.
Moves read and fully-read markers in rooms on the server when
`ement-room-scroll-up-mark-read' is called at the end of a
buffer. When `send', also marks room as read when sending a
message in it. When disabled, rooms may still be marked as read
manually by calling `ement-room-mark-read'. Note that this is
not strictly the same as read receipts."
:type '(choice (const :tag "When scrolling past end of buffer" t)
(const :tag "Also when sending" send)
(const :tag "Never" nil)))
(defcustom ement-room-send-typing t
"Send typing notifications to the server while typing a message."
:type 'boolean)
(defcustom ement-room-join-view-buffer t
"View room buffer when joining a room."
:type 'boolean)
(defcustom ement-room-leave-kill-buffer t
"Kill room buffer when leaving a room.
When disabled, the room's buffer will remain open, but
Matrix-related commands in it will fail."
:type 'boolean)
(defcustom ement-room-warn-for-already-seen-messages nil
"Warn when a sent message has already been seen.
Such a case could very rarely indicate a reused transaction ID,
which would prevent further messages from being sent (and would
be solved by logging in with a new session, generating a new
token), but most often it happens when the server echoes back a
sent message before acknowledging the sending of the
message (which is harmless and can be ignored)."
:type 'boolean)
(defcustom ement-room-wrap-prefix
(concat (propertize " "
'face 'ement-room-wrap-prefix)
" ")
"String prefixing certain events in room buffers.
Events include membership events, image attachments, etc.
Generally users should prefer to customize the face
`ement-room-wrap-prefix' rather than this option, because this
option's default value has that face applied to it where
appropriate; if users customize this option, they will need to
apply the face to the string themselves, if desired."
:type 'string)
(defgroup ement-room-prism nil
"Colorize usernames and messages in rooms."
:group 'ement-room)
(defcustom ement-room-prism 'name
"Display users' names and messages in unique colors."
:type '(choice (const :tag "Name only" name)
(const :tag "Name and message" both)
(const :tag "Neither" nil)))
(defcustom ement-room-prism-addressee t
"Show addressees' names in their respective colors.
Applies to room member names at the beginning of messages,
preceded by a colon or comma.
Note that a limitation applies to the current implementation: if
a message from the addressee is not yet visible in a room at the
time the addressed message is formatted, the color may not be
applied."
;; FIXME: When we keep a hash table of members in a room, make this
;; smarter.
:type 'boolean)
(defcustom ement-room-prism-color-adjustment 0
"Number used to tweak computed username colors.
This may be used to adjust your favorite users' colors if you
don't like the default ones. (The only way to do it is by
experimentation--there is no direct mapping available, nor a
per-user setting.)
The number is added to the hashed user ID before converting it to
a color. Note that, since user ID hashes are ratioed against
`most-positive-fixnum', this number must be very large in order
to have any effect; it should be at least 1e13.
After changing this option, a room's buffer must be killed and
recreated to see the effect."
:type 'number
:set (lambda (option value)
(unless (or (= 0 value) (>= value 1e13))
(user-error "This option must be a very large number, at least 1e13"))
(set-default option value)))
(defcustom ement-room-prism-minimum-contrast 6
"Attempt to enforce this minimum contrast ratio for user faces.
This should be a reasonable number from, e.g. 0-7 or so."
;; Prot would almost approve of this default. :) I would go all the way
;; to 7, but 6 already significantly dilutes the colors in some cases.
:type 'number)
(defcustom ement-room-prism-message-desaturation 25
"Desaturate user colors by this percent for message bodies.
Makes message bodies a bit less intense."
:type 'integer)
(defcustom ement-room-prism-message-lightening 10
"Lighten user colors by this percent for message bodies.
Makes message bodies a bit less intense.
When using a light theme, it may be necessary to use a negative
number (to darken rather than lighten)."
:type 'integer)
;;;; Macros
(defmacro ement-room-with-highlighted-event-at (position &rest body)
"Highlight event at POSITION while evaluating BODY."
;; MAYBE: Accept a marker for POSITION.
(declare (indent 1))
`(let* ((node (ewoc-locate ement-ewoc ,position))
(event (ewoc-data node))
ement-room-replying-to-event ement-room-replying-to-overlay)
(unless (and (ement-event-p event)
(ement-event-id event))
(error "No event at point"))
(unwind-protect
(progn
(setf ement-room-replying-to-event event
ement-room-replying-to-overlay
(make-overlay (ewoc-location node)
;; NOTE: It doesn't seem possible to get the end position of
;; a node, so if there is no next node, we use point-max.
;; But this might break if we were to use an EWOC footer.
(if (ewoc-next ement-ewoc node)
(ewoc-location (ewoc-next ement-ewoc node))
(point-max))))
(overlay-put ement-room-replying-to-overlay 'face 'highlight)
,@body)
(when (overlayp ement-room-replying-to-overlay)
(delete-overlay ement-room-replying-to-overlay))
(setf ement-room-replying-to-event nil
ement-room-replying-to-overlay nil))))
(defmacro ement-room-with-typing (&rest body)
"Send typing notifications around BODY.
When `ement-room-send-typing' is enabled, typing notifications
are sent while BODY is executing. BODY is wrapped in an
`unwind-protect' form that cancels `ement-room-typing-timer' and
sends a not-typing notification."
(declare (indent defun))
`(unwind-protect
(progn
(when ement-room-send-typing
(when ement-room-typing-timer
;; In case there are any stray ones (e.g. a user typing in
;; more than room at once, which is possible but unlikely).
(cancel-timer ement-room-typing-timer))
(setf ement-room-typing-timer (run-at-time nil 15 #'ement-room--send-typing ement-session ement-room)))
,@body)
(when ement-room-send-typing
(when ement-room-typing-timer
(cancel-timer ement-room-typing-timer)
(setf ement-room-typing-timer nil))
;; Cancel typing notifications after sending a message. (The
;; spec doesn't say whether this is needed, but it seems to be.)
(ement-room--send-typing ement-session ement-room :typing nil))))
(defmacro ement-room-wrap-prefix (string-form &rest properties)
"Wrap STRING-FORM with `ement-room-wrap-prefix'.
Concats `ement-room-wrap-prefix' to STRING-FORM and applies it as
the `wrap-prefix' property. Also applies any PROPERTIES."
(declare (indent defun))
`(concat ement-room-wrap-prefix
(propertize ,string-form
'wrap-prefix ement-room-wrap-prefix
,@properties)))
(defsubst ement-room--concat-property (string property value &optional append)
"Return STRING having concatted VALUE with PROPERTY on it.
If APPEND, append it; otherwise prepend. Assumes PROPERTY is
constant throughout STRING."
(declare (indent defun))
(let* ((old-value (get-text-property 0 property string))
(new-value (if append
(concat old-value value)
(concat value old-value))))
(propertize string property new-value)))
;;;;; Event formatting
;; NOTE: When adding specs, also add them to docstring
;; for `ement-room-message-format-spec'.
(defvar ement-room-event-formatters nil
"Alist mapping characters to event-formatting functions.
Each function is called with three arguments: the event, the
room, and the session. See macro
`ement-room-define-event-formatter'.")
(defvar ement-room--format-message-margin-p nil
"Set by margin-related event formatters.")
(defvar ement-room--format-message-wrap-prefix nil
"Set by margin-related event formatters.")
(defmacro ement-room-define-event-formatter (char docstring &rest body)
"Define an event formatter for CHAR with DOCSTRING and BODY.
BODY is wrapped in a lambda form that binds `event', `room', and
`session', and the lambda is added to the variable
`ement-room-event-formatters', which see."
(declare (indent defun))
`(setf (alist-get ,char ement-room-event-formatters nil nil #'equal)
(lambda (event room session)
,docstring
,@body)))
(ement-room-define-event-formatter ?L
"Text before this is shown in the left margin."
(ignore event room session)
(setf ement-room--format-message-margin-p t)
(propertize " " 'left-margin-end t))
(ement-room-define-event-formatter ?R
"Text after this is shown in the right margin."
(ignore event room session)
(setf ement-room--format-message-margin-p t)
(propertize " " 'right-margin-start t))
(ement-room-define-event-formatter ?W
"Text before this is the length of the event's wrap-prefix.
This emulates the effect of using the left margin (the \"%L\"
spec) without requiring all events to use the same margin width."
(ignore event room session)
(setf ement-room--format-message-wrap-prefix t)
(propertize " " 'wrap-prefix-end t))
(ement-room-define-event-formatter ?b
"Plain-text body content."
;; NOTE: `save-match-data' is required around calls to `ement-room--format-message-body'.
(let ((body (save-match-data
(ement-room--format-message-body event :formatted-p nil)))
(face (ement-room--event-body-face event room session)))
(add-face-text-property 0 (length body) face 'append body)
(when ement-room-prism-addressee
(ement-room--add-member-face body room))
body))
(ement-room-define-event-formatter ?B
"Formatted body content (i.e. rendered HTML)."
(let ((body (save-match-data
(ement-room--format-message-body event)))
(face (ement-room--event-body-face event room session)))
(add-face-text-property 0 (length body) face 'append body)
(when ement-room-prism-addressee
(ement-room--add-member-face body room))
body))
(ement-room-define-event-formatter ?i
"Event ID."
;; Probably only useful for debugging, so might remove later.
(ignore room session)
(ement-event-id event))
(ement-room-define-event-formatter ?o
"Room avatar."
(ignore event session)
(or (alist-get 'room-list-avatar (ement-room-local room)) ""))
(ement-room-define-event-formatter ?O
"Room display name."
(ignore event session)
(let ((room-name (propertize (or (ement-room-display-name room)
(ement--room-display-name room))
'face 'ement-room-name
'help-echo (or (ement-room-canonical-alias room)
(ement-room-id room)))))
;; HACK: This will probably only be used in the notifications buffers, anyway.
(when ement-notify-limit-room-name-width
(setf room-name (truncate-string-to-width room-name ement-notify-limit-room-name-width
nil nil ement-room-ellipsis)))
room-name))
;; NOTE: In ?s and ?S, we add nearly-invisible ASCII unit-separator characters ("")
;; to prevent, e.g. `dabbrev-expand' from expanding display names with body text.
(ement-room-define-event-formatter ?s
"Sender MXID."
(ignore room session)
(concat (propertize (ement-user-id (ement-event-sender event))
'face 'ement-room-user)
""))
(ement-room-define-event-formatter ?S
"Sender display name."
(ignore session)
(pcase-let ((sender (ement--format-user (ement-event-sender event) room))
((cl-struct ement-room (local (map buffer))) room))
;; NOTE: When called from an `ement-notify' function, ROOM may have no buffer. In
;; that case, just use the current buffer (which should be a temp buffer used to
;; format the event).
(with-current-buffer (or buffer (current-buffer))
(when ement-room-sender-in-left-margin
;; Sender in left margin: truncate/pad appropriately.
(setf sender
(if (< (string-width sender) ement-room-left-margin-width)
;; Using :align-to or :width space display properties doesn't
;; seem to have any effect in the margin, so we make a string.
(concat (make-string (- ement-room-left-margin-width (string-width sender))
? )
sender)
;; String wider than margin: truncate it.
(ement-room--concat-property
(truncate-string-to-width sender ement-room-left-margin-width nil nil "…")
'help-echo (concat sender " "))))))
;; NOTE: I'd like to add a help-echo function to display the sender ID, but the Emacs
;; manual says that there is currently no way to make text in the margins mouse-sensitive.
;; So `ement--format-user' returns a string propertized with `help-echo' as a string.
(concat sender "")))
(ement-room-define-event-formatter ?r
"Reactions."
(ignore room session)
(ement-room--format-reactions event))
(ement-room-define-event-formatter ?t
"Timestamp."
(ignore room session)
(propertize (format-time-string ement-room-timestamp-format ;; Timestamps are in milliseconds.
(/ (ement-event-origin-server-ts event) 1000))
'face 'ement-room-timestamp
'help-echo (format-time-string "%Y-%m-%d %H:%M:%S"
(/ (ement-event-origin-server-ts event) 1000))))
(defun ement-room--event-body-face (event room session)
"Return face definition for EVENT in ROOM on SESSION."
(ignore room) ;; Unused for now, but keeping for consistency.
;; This used to be a macro in --format-message, which is probably better for
;; performance, but using a function is clearer, and avoids premature optimization.
(pcase-let* (((cl-struct ement-event sender
(content (map msgtype))
(unsigned (map ('redacted_by unsigned-redacted-by)))
(local (map ('redacted-by local-redacted-by))))
event)
((cl-struct ement-user (id sender-id)) sender)
((cl-struct ement-session user) session)
((cl-struct ement-user (id user-id)) user)
(self-message-p (equal sender-id user-id))
(type-face (pcase msgtype
("m.emote" 'ement-room-message-emote)
(_ 'ement-room-message-text)))
(context-face (cond (self-message-p
'ement-room-self-message)
((or (ement-room--event-mentions-user-p event user)
(ement--event-mentions-room-p event))
'ement-room-mention)))
(prism-color (unless self-message-p
(when (eq 'both ement-room-prism)
(or (ement-user-message-color sender)
(setf (ement-user-message-color sender)
(let ((message-color (color-desaturate-name (ement--user-color sender)
ement-room-prism-message-desaturation)))
(if (ement--color-dark-p (color-name-to-rgb (face-background 'default)))
(color-lighten-name message-color ement-room-prism-message-lightening)
(color-darken-name message-color ement-room-prism-message-lightening))))))))
(redacted-face (when (or local-redacted-by unsigned-redacted-by)
'ement-room-redacted))
(body-face (list :inherit (delq nil (list redacted-face context-face type-face)))))
(if prism-color
(plist-put body-face :foreground prism-color)
body-face)))
(defun ement-room--add-member-face (string room)
"Add member faces in ROOM to STRING.
If STRING begins with the name of a member in ROOM followed by a
colon or comma (as if STRING is a message addressing that
member), apply that member's displayname color face to that part
of the string.
Note that, if ROOM has no buffer, STRING is returned unchanged."
;; This only looks for a member name at the beginning of the string. It would be neat to add
;; colors to every member mentioned in a message, but that would probably not perform well.
;; NOTE: This function may be called by `ement-notify' functions even when the room has
;; no buffer, and this function is designed to use events in a room buffer to more
;; quickly find the data it needs, so, for now, if the room has no buffer, we return
;; STRING unchanged.
(pcase-let (((cl-struct ement-room (local (map buffer))) room))
(if (buffer-live-p buffer)
(save-match-data
;; This function may be called from a chain of others that use the match data, so
;; rather than depending on all of them to save the match data, we do it here.
;; FIXME: Member names containing spaces aren't matched. Can this even be fixed reasonably?
(when (string-match (rx bos (group (1+ (not blank))) (or ":" ",") (1+ blank)) string)
(when-let* ((member-name (match-string 1 string))
;; HACK: Since we don't currently keep a list of all
;; members in a room, we look to see if this displayname
;; has any mentions in the room so far.
(user (save-match-data
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(cl-labels ((found-sender-p
(ewoc-data)
(when (ement-event-p ewoc-data)
(equal member-name
(gethash (ement-event-sender ewoc-data) (ement-room-displaynames room))))))
(cl-loop with regexp = (regexp-quote member-name)
while (re-search-forward regexp nil t)
;; NOTE: I don't know why, but sometimes the regexp
;; search ends on a non-event line, like a timestamp
;; header, so for now we just try to handle that case.
for maybe-event = (ewoc-data (ewoc-locate ement-ewoc))
when (found-sender-p maybe-event)
return (ement-event-sender maybe-event)))))))
(prism-color (or (ement-user-color user)
(setf (ement-user-color user)
(ement-room--user-color user)))))
(add-face-text-property (match-beginning 1) (match-end 1)
(list :foreground prism-color) nil string))))
;; Room has no buffer: return STRING as-is.
string)))
;;;; Bookmark support
;; Especially useful with Burly: <https://github.com/alphapapa/burly.el>
(require 'bookmark)
(defun ement-room-bookmark-make-record ()
"Return a bookmark record for the current `ement-room' buffer."
(pcase-let* (((cl-struct ement-room (id room-id) canonical-alias display-name) ement-room)
((cl-struct ement-session user) ement-session)
((cl-struct ement-user (id session-id)) user))
;; MAYBE: Support bookmarking specific events in a room.
(list (concat "Ement room: " display-name " (" canonical-alias ")")
(cons 'session-id session-id)
(cons 'room-id room-id)
(cons 'handler #'ement-room-bookmark-handler))))
(defun ement-room-bookmark-handler (bookmark)
"Show Ement room buffer for BOOKMARK."
(pcase-let* ((`(,_name . ,(map session-id room-id)) bookmark)
(session (ement-aprog1
(alist-get session-id ement-sessions nil nil #'equal)
(unless it
;; MAYBE: Automatically connect.
(user-error "Session %s not connected: call `ement-connect' first" session-id))))
(room (ement-aprog1
(ement-afirst (equal room-id (ement-room-id it))
(ement-session-rooms session))
(cl-assert it nil "Room %S not found on session %S" room-id session-id))))
(ement-view-room room session)
;; HACK: Put point at the end of the room buffer. This seems unusually difficult,
;; apparently because the bookmark library itself moves point after jumping to a
;; bookmark. My attempts at setting the buffer's and window's points after calling
;; `ement-view-room' have had no effect. `bookmark-after-jump-hook' sounds ideal, but
;; it does not seem to actually get run, so we use a timer that runs immediately after
;; `bookmark-jump' returns.
(run-at-time nil nil (lambda ()
(goto-char (point-max))))))
;;;; Commands
(defun ement-room-override-name (name room session)
"Set display NAME override for ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room. If NAME is the empty string, remove
the override.
Sets account-data event of type
\"org.matrix.msc3015.m.room.name.override\". This name is only
used by clients that respect this proposed override. See
<https://github.com/matrix-org/matrix-spec-proposals/pull/3015#issuecomment-1451017296>."
(interactive
(ement-with-room-and-session
(let* ((prompt (format "Set name override (%s): " (ement--format-room ement-room)))
(name (read-string prompt nil nil (ement-room-display-name ement-room))))
(list name ement-room ement-session))))
(ement-put-account-data session "org.matrix.msc3015.m.room.name.override"
(if (string-empty-p name)
;; `json-encode' wants an empty hash table to represent an empty map. And
;; apparently there's no way to DELETE account-data events, so we have to re-PUT
;; it with empty content.
(make-hash-table)
(ement-alist "name" name))
:room room))
(defun ement-room-flush-colors ()
"Flush generated username/message colors.
Also, redisplay events in all open buffers. The colors will be
regenerated according to the current background color. Helpful
when switching themes or adjusting `ement-prism' options."
(interactive)
(cl-loop for user being the hash-values of ement-users
do (setf (ement-user-color user) nil
(ement-user-message-color user) nil))
(dolist (buffer (buffer-list))
(when (eq 'ement-room-mode (buffer-local-value 'major-mode buffer))
(with-current-buffer buffer
(ewoc-refresh ement-ewoc))))
;; Flush notify-background-color colors.
(cl-loop for (_id . session) in ement-sessions
do (cl-loop for room in (ement-session-rooms session)
do (setf (alist-get 'notify-background-color (ement-room-local room)) nil)))
;; NOTE: The notifications buffer can't be refreshed because each event is from a
;; different room, and the `ement-room' variable is unset in the buffer.
;; (when-let (buffer (get-buffer "*Ement Notifications*"))
;; (with-current-buffer buffer
;; (ewoc-refresh ement-ewoc)))
)
(defun ement-room-browse-url (url &rest args)
"Browse URL, using Ement for matrix.to URLs when possible.
Otherwise, fall back to `browse-url'. When called outside of an
`ement-room' buffer, the variable `ement-session' must be bound
to the session in which to look for URL's room and event. ARGS
are passed to `browse-url'."
(interactive)
(when (string-match ement-room-matrix.to-url-regexp url)
(let* ((room-id (when (string-prefix-p "!" (match-string 1 url))
(match-string 1 url)))
(room-alias (when (string-prefix-p "#" (match-string 1 url))
(match-string 1 url)))
(event-id (match-string 2 url))
(room (when (or
;; Compare with current buffer's room.
(and room-id (equal room-id (ement-room-id ement-room)))
(and room-alias (equal room-alias (ement-room-canonical-alias ement-room)))
;; Compare with other rooms on session.
(and room-id (cl-find room-id (ement-session-rooms ement-session)
:key #'ement-room-id))
(and room-alias (cl-find room-alias (ement-session-rooms ement-session)
:key #'ement-room-canonical-alias)))
ement-room)))
(if room
(progn
;; Found room in current session: view it and find the event.
(ement-view-room room ement-session)
(when event-id
(ement-room-find-event event-id)))
;; Room not joined: offer to join it or load link in browser.
(pcase-exhaustive (completing-read
(format "Room <%s> not joined on current session. Join it, or load link with browser?"
(or room-alias room-id))
'("Join room" "Load link with browser") nil t)
("Join room" (ement-join-room (or room-alias room-id) ement-session
:then (when event-id
(lambda (room session)
(ement-view-room room session)
(ement-room-find-event event-id)))))
("Load link with browser" (apply #'browse-url url args)))))))
(defun ement-room-find-event (event-id)
"Go to EVENT-ID in current buffer."
(interactive)
(cl-labels ((goto-event
(event-id) (progn
(push-mark)
(goto-char
(ewoc-location
(ement-room--ewoc-last-matching ement-ewoc
(lambda (data)
(and (ement-event-p data)
(equal event-id (ement-event-id data))))))))))
(if (or (cl-find event-id (ement-room-timeline ement-room)
:key #'ement-event-id :test #'equal)
(cl-find event-id (ement-room-state ement-room)
:key #'ement-event-id :test #'equal))
;; Found event in timeline: it should be in the EWOC, so go to it.
(goto-event event-id)
;; Event not found in timeline: try to retro-load it.
(message "Event %s not seen in current room. Looking in history..." event-id)
(let ((room ement-room))
(ement-room-retro-to ement-room ement-session event-id
;; TODO: Add an ELSE argument to `ement-room-retro-to' and use it to give
;; a useful error here.
:then (lambda ()
(with-current-buffer (alist-get 'buffer (ement-room-local room))
(goto-event event-id))))))))
(defun ement-room-set-composition-format (&optional localp)
"Set message composition format.
If LOCALP (interactively, with prefix), set in current room's
buffer. Sets `ement-room-send-message-filter'."
(interactive (list current-prefix-arg))
(let* ((formats (list (cons "Plain-text" nil)
(cons "Org-mode" #'ement-room-send-org-filter)))
(selected-name (completing-read "Composition format: " formats nil 'require-match nil nil
ement-room-send-message-filter))
(selected-filter (alist-get selected-name formats nil nil #'equal)))
(if localp
(setq-local ement-room-send-message-filter selected-filter)
(setq ement-room-send-message-filter selected-filter))))
(defun ement-room-set-message-format (format-spec)
"Set `ement-room-message-format-spec' in current buffer to FORMAT-SPEC.
Interactively, prompts for the spec using suggested values of the
option."
(interactive (list (let* ((choices (thread-last
(get 'ement-room-message-format-spec 'custom-type)
cdr
(seq-filter (lambda (it)
(eq (car it) 'const)))
(mapcar (lambda (it)
(cons (nth 2 it) (nth 3 it))))))
(choice (completing-read "Format: " (mapcar #'car choices))))
(or (alist-get choice choices nil nil #'equal)
choice))))
(cl-assert ement-ewoc)
(ement-room-message-format-spec-setter 'ement-room-message-format-spec format-spec 'local)
(setf left-margin-width ement-room-left-margin-width
right-margin-width ement-room-right-margin-width)
(set-window-margins nil left-margin-width right-margin-width)
(if ement-room-sender-in-headers
(ement-room--insert-sender-headers ement-ewoc)
(ewoc-filter ement-ewoc (lambda (node-data)
;; Return non-nil for nodes that should stay.
(not (ement-user-p node-data)))))
(ewoc-refresh ement-ewoc))
(defun ement-room-set-topic (session room topic)
"Set ROOM's TOPIC on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room."
(interactive
(ement-with-room-and-session
(list ement-session ement-room
(read-string (format "New topic (%s): "
(ement-room-display-name ement-room))
(ement-room-topic ement-room) nil nil 'inherit-input-method))))
(pcase-let* (((cl-struct ement-room (id room-id) display-name) room)
(endpoint (format "rooms/%s/state/m.room.topic" (url-hexify-string room-id)))
(data (ement-alist "topic" topic)))
(ement-api session endpoint :method 'put :data (json-encode data)
:then (lambda (_data)
(message "Topic set (%s): %s" display-name topic)))))
(cl-defun ement-room-send-file (file body room session &key (msgtype "m.file"))
"Send FILE to ROOM on SESSION, using message BODY and MSGTYPE.
Interactively, with prefix, prompt for room and session,
otherwise use current room."
;; TODO: Support URLs to remote files.
(interactive
(ement-with-room-and-session
(ement-room-with-typing
(let* ((file (read-file-name (format "Send file (%s): " (ement-room-display-name ement-room))
nil nil 'confirm))
(body (ement-room-read-string (format "Message body (%s): " (ement-room-display-name ement-room))
(file-name-nondirectory file) nil nil 'inherit-input-method)))
(list file body ement-room ement-session)))))
;; NOTE: The typing notification won't be quite right, because it'll be canceled while waiting
;; for the file to upload. It would be awkward to handle that, so this will do for now.
(when (yes-or-no-p (format "Upload file %S to room %S? "
file (ement-room-display-name room)))
(pcase-let* ((filename (file-name-nondirectory file))
(extension (or (file-name-extension file) ""))
(mime-type (mailcap-extension-to-mime extension))
(data (with-temp-buffer
;; NOTE: Using (set-buffer-multibyte nil) doesn't
;; seem to be necessary, but I don't know why not.
(insert-file-contents file)
(buffer-string)))
(size (length data)))
(ement-upload session :data data :filename filename :content-type mime-type
:then (lambda (data)
(message "Uploaded file %S. Sending message..." file)
(pcase-let* (((map ('content_uri content-uri)) data)
((cl-struct ement-room (id room-id)) room)
(endpoint (format "rooms/%s/send/%s/%s" (url-hexify-string room-id)
"m.room.message" (ement--update-transaction-id session)))
;; TODO: Image height/width (maybe not easy to get in Emacs).
(content (ement-alist "msgtype" msgtype
"url" content-uri
"body" body
"filename" filename
"info" (ement-alist "mimetype" mime-type
"size" size))))
(ement-api session endpoint :method 'put :data (json-encode content)
:then (apply-partially #'ement-room-send-event-callback
:room room :session session :content content :data))))))))
(defun ement-room-send-image (file body room session)
"Send image FILE to ROOM on SESSION, using message BODY.
Interactively, with prefix, prompt for room and session,
otherwise use current room."
;; TODO: Support URLs to remote files.
(interactive
(ement-with-room-and-session
(ement-room-with-typing
(let* ((file (read-file-name (format "Send image file (%s): " (ement-room-display-name ement-room))
nil nil 'confirm))
(body (ement-room-read-string (format "Message body (%s): " (ement-room-display-name ement-room))
(file-name-nondirectory file) nil nil 'inherit-input-method)))
(list file body ement-room ement-session)))))
(ement-room-send-file file body room session :msgtype "m.image"))
(defun ement-room-dnd-upload-file (uri _action)
"Upload the file as specified by URI to the current room."
(when-let ((file (dnd-get-local-file-name uri t)))
(ement-room-send-file file (file-name-nondirectory file) ement-room ement-session
:msgtype (if (string-prefix-p "image/" (mailcap-file-name-to-mime-type file))
"m.image"
"m.file"))))
(declare-function ement-tabulated-room-list-next-unread "ement-tabulated-room-list")
(declare-function ement-room-list-next-unread "ement-room-list")
(defun ement-room-scroll-up-mark-read ()
"Scroll buffer up, marking read and burying when at end."
(interactive)
(if (= (window-point) (point-max))
(progn
;; At the bottom of the buffer: mark read and show next unread room.
(when ement-room-mark-rooms-read
(ement-room-mark-read ement-room ement-session
:read-event (ewoc-data (ement-room--ewoc-last-matching ement-ewoc
(lambda (data) (ement-event-p data))))
:fully-read-event (ewoc-data (ement-room--ewoc-last-matching ement-ewoc
(lambda (data) (ement-event-p data))))))
(set-buffer-modified-p nil)
(if-let ((rooms-window (cl-find-if (lambda (window)
(member (buffer-name (window-buffer window))
'("*Ement Taxy*" "*Ement Rooms*")))
(window-list))))
;; Rooms buffer already displayed: select its window and move to next unread room.
(progn
(select-window rooms-window)
(funcall (pcase-exhaustive major-mode
('ement-tabulated-room-list-mode #'ement-tabulated-room-list-next-unread)
('ement-room-list-mode #'ement-room-list-next-unread))))
;; Rooms buffer not displayed: bury this room buffer, which should usually
;; result in another room buffer or the rooms list buffer being displayed.
(bury-buffer))
(when (member major-mode '(ement-tabulated-room-list-mode ement-room-list-mode))
;; Back in the room-list buffer: revert it.
(revert-buffer)))
;; Not at the bottom of the buffer: scroll.
(condition-case _err
(scroll-up-command)
(end-of-buffer (set-window-point nil (point-max))))))
(cl-defun ement-room-join (id-or-alias session &key then)
"Join room by ID-OR-ALIAS on SESSION.
THEN may be a function to call after joining the room (and when
`ement-room-join-view-buffer' is non-nil, after viewing the room
buffer). It receives two arguments, the room and the session."
(interactive (list (read-string "Join room (ID or alias): ")
(or ement-session
(ement-complete-session))))
(cl-assert id-or-alias) (cl-assert session)
(unless (string-match-p
;; According to tulir in #matrix-dev:matrix.org, ": is not
;; allowed in the localpart, all other valid unicode is
;; allowed. (user ids and room ids are the same over
;; federation). it's mostly a lack of validation in
;; synapse (arbitrary unicode isn't intentionally allowed,
;; but it's not disallowed either)". See
;; <https://matrix.to/#/!jxlRxnrZCsjpjDubDX:matrix.org/$Cnb53UQdYnGFizM49Aje_Xs0BxVdt-be7Dnm7_k-0ho>.
(rx bos (or "#" "!") (1+ (not (any ":")))
":" (1+ (or alnum (any "-."))))
id-or-alias)
(user-error "Invalid room ID or alias (use, e.g. \"#ROOM-ALIAS:SERVER\")"))
(let ((endpoint (format "join/%s" (url-hexify-string id-or-alias))))
(ement-api session endpoint :method 'post :data ""
:then (lambda (data)
;; NOTE: This generates a symbol and sets its function value to a lambda
;; which removes the symbol from the hook, removing itself from the hook.
;; TODO: When requiring Emacs 27, use `letrec'.
(pcase-let* (((map ('room_id room-id)) data)
(then-fns (delq nil
(list (when ement-room-join-view-buffer
(lambda (room session)
(ement-view-room room session)))
then)))
(then-fn-symbol (gensym (format "ement-join-%s" id-or-alias)))
(then-fn (lambda (session)
(when-let ((room (cl-loop for room in (ement-session-rooms session)
when (equal room-id (ement-room-id room))
return room)))
;; In case the join event is not in this next sync
;; response, make sure the room is found before removing
;; the function and joining the room.
(remove-hook 'ement-sync-callback-hook then-fn-symbol)
;; FIXME: Probably need to unintern the symbol.
(dolist (fn then-fns)
(funcall fn room session))))))
(setf (symbol-function then-fn-symbol) then-fn)
(add-hook 'ement-sync-callback-hook then-fn-symbol)
(message "Joined room: %s" room-id)))
:else (lambda (plz-error)
(pcase-let* (((cl-struct plz-error response) plz-error)
((cl-struct plz-response status body) response)
((map error) (json-read-from-string body)))
(pcase status
((or 403 429) (error "Unable to join room %s: %s" id-or-alias error))
(_ (error "Unable to join room %s: %s %S" id-or-alias status plz-error))))))))
(defalias 'ement-join-room #'ement-room-join)
(defun ement-room-goto-prev ()
"Go to the previous message in buffer."
(interactive)
(if (>= (point) (- (point-max) 2))
;; Point is actually on the last event, but it doesn't appear to be: move point to
;; the beginning of that event.
(ewoc-goto-node ement-ewoc (ewoc-locate ement-ewoc))
;; Go to previous event.
(ement-room-goto-next :next-fn #'ewoc-prev)))
(cl-defun ement-room-goto-next (&key (next-fn #'ewoc-next))
"Go to the next message in buffer.
NEXT-FN is passed to `ement-room--ewoc-next-matching', which
see."
(interactive)
(if-let (node (ement-room--ewoc-next-matching ement-ewoc
(ewoc-locate ement-ewoc) #'ement-event-p next-fn))
(ewoc-goto-node ement-ewoc node)
(user-error "End of events")))
(defun ement-room-scroll-down-command ()
"Scroll down, and load NUMBER earlier messages when at top."
(interactive)
(condition-case _err
(scroll-down nil)
(beginning-of-buffer
(call-interactively #'ement-room-retro))))
(defun ement-room-mwheel-scroll (event)
"Scroll according to EVENT, loading earlier messages when at top."
(interactive "e")
(with-selected-window (posn-window (event-start event))
(let ((start (window-start)))
(mwheel-scroll event)
(when (= start (window-start))
(call-interactively #'ement-room-retro)))))
;; TODO: Unify these retro-loading functions.
(cl-defun ement-room-retro
(room session number &key buffer
(then (apply-partially #'ement-room-retro-callback room session)))
;; FIXME: Naming things is hard.
"Retrieve NUMBER older messages in ROOM on SESSION."
(interactive (list ement-room ement-session
(cl-typecase current-prefix-arg
(null ement-room-retro-messages-number)
(list (read-number "Number of messages: "))
(number current-prefix-arg))
:buffer (current-buffer)))
(unless ement-room-retro-loading
(pcase-let* (((cl-struct ement-room id prev-batch) room)
(endpoint (format "rooms/%s/messages" (url-hexify-string id))))
;; We use a timeout of 30, because sometimes the server can take a while to
;; respond, especially if loading, e.g. hundreds or thousands of events.
(ement-api session endpoint :timeout 30
:params (list (list "from" prev-batch)
(list "dir" "b")
(list "limit" (number-to-string number))
(list "filter" (json-encode ement-room-messages-filter)))
:then then
:else (lambda (plz-error)
(when buffer
(with-current-buffer buffer
(setf ement-room-retro-loading nil)))
(signal 'ement-api-error (list (format "Loading %s earlier messages failed" number)
plz-error))))
(message "Loading %s earlier messages..." number)
(setf ement-room-retro-loading t))))
(cl-defun ement-room-retro-to (room session event-id &key then (batch-size 100) (limit 1000))
"Retrieve messages in ROOM on SESSION back to EVENT-ID.
When event is found, call function THEN. Search in batches of
BATCH-SIZE events up to a total of LIMIT."
(declare (indent defun))
(cl-assert
;; Ensure the event hasn't already been retrieved.
(not (gethash event-id (ement-session-events session))))
(let* ((total-retrieved 0)
;; TODO: Use letrec someday.
(callback-symbol (gensym "ement-room-retro-to-callback-"))
(callback (lambda (data)
(ement-room-retro-callback room session data)
(if (gethash event-id (ement-session-events session))
(progn
(message "Found event %S" event-id)
;; FIXME: Probably need to unintern the symbol.
(when then
(funcall then)))
;; FIXME: What if it hits the beginning of the timeline?
(if (>= (cl-incf total-retrieved batch-size) limit)
(message "%s older events retrieved without finding event %S"
limit event-id)
(message "Looking back for event %S (%s/%s events retrieved)"
event-id total-retrieved limit)
(ement-room-retro room session batch-size
:buffer (alist-get 'buffer (ement-room-local room))
:then callback-symbol))))))
(fset callback-symbol callback)
(ement-room-retro room session batch-size
:buffer (alist-get 'buffer (ement-room-local room))
:then callback-symbol)))
(cl-defun ement-room-retro-to-token (room session from to
&key (batch-size 100) (limit 1000))
"Retrieve messages in ROOM on SESSION back from FROM to TO.
Retrieve batches of BATCH-SIZE up to total LIMIT. FROM and TO
are sync batch tokens. Used for, e.g. filling gaps in
\"limited\" sync responses."
;; NOTE: We don't set `ement-room-retro-loading' since the room may
;; not have a buffer. This could theoretically allow a user to
;; overlap manual scrollback-induced loading of old messages with
;; this gap-filling loading, but that shouldn't matter, and probably
;; would be very rare, anyway.
(pcase-let* (((cl-struct ement-room id) room)
(endpoint (format "rooms/%s/messages" (url-hexify-string id)))
(then
(lambda (data)
(ement-room-retro-callback room session data
:set-prev-batch nil)
(pcase-let* (((map end chunk) data))
;; HACK: Comparing the END and TO tokens ought to
;; work for determining whether we are done
;; filling, but it isn't (maybe the server isn't
;; returning the TO token as END when there are no
;; more events), so instead we'll check the length
;; of the chunk.
(unless (< (length chunk) batch-size)
;; More pages remain to be loaded.
(let ((remaining-limit (- limit batch-size)))
(if (not (> remaining-limit 0))
;; FIXME: This leaves a gap if it's larger than 1,000 events.
;; Probably, the limit should be configurable, but it would be good
;; to find some way to remember the gap and fill it if the user
;; scrolls to it later (although that might be very awkward to do).
(display-warning 'ement-room-retro-to-token
(format "Loaded events in %S (%S) without filling gap; not filling further"
(ement-room-display-name room)
(or (ement-room-canonical-alias room)
(ement-room-id room))))
;; FIXME: Remove this message after further testing.
(message "Ement: Continuing to fill gap in %S (%S) (remaining limit: %s)"
(ement-room-display-name room)
(or (ement-room-canonical-alias room)
(ement-room-id room))
remaining-limit)
(ement-room-retro-to-token
room session end to :limit remaining-limit))))))))
;; FIXME: Remove this message after further testing.
(message "Ement: Filling gap in %S (%S)"
(ement-room-display-name room)
(or (ement-room-canonical-alias room)
(ement-room-id room)))
(ement-api session endpoint :timeout 30
:params (list (list "from" from)
(list "to" to)
(list "dir" "b")
(list "limit" (number-to-string batch-size))
(list "filter" (json-encode ement-room-messages-filter)))
:then then
:else (lambda (plz-error)
(signal 'ement-api-error
(list (format "Filling gap in %S (%S) failed"
(ement-room-display-name room)
(or (ement-room-canonical-alias room)
(ement-room-id room)))
plz-error))))))
;; NOTE: `declare-function' doesn't recognize cl-defun forms, so this declaration doesn't work.
(declare-function ement--sync "ement.el" t t)
(defun ement-room-sync (session &optional force)
"Sync SESSION (interactively, current buffer's).
If FORCE (interactively, with prefix), cancel any outstanding
sync requests. Also, update any room list buffers."
(interactive (list ement-session current-prefix-arg))
(ement--sync session :force force)
(cl-loop for buffer in (buffer-list)
when (member (buffer-local-value 'major-mode buffer)
'(ement-room-list-mode ement-tabulated-room-list-mode))
do (with-current-buffer buffer
(revert-buffer))))
(defun ement-room-view-event (event)
"Pop up buffer showing details of EVENT (interactively, the one at point).
EVENT should be an `ement-event' or `ement-room-membership-events' struct."
(interactive (list (ewoc-data (ewoc-locate ement-ewoc))))
(require 'pp)
(cl-labels ((event-alist
(event) (ement-alist :id (ement-event-id event)
:sender (ement-user-id (ement-event-sender event))
:content (ement-event-content event)
:origin-server-ts (ement-event-origin-server-ts event)
:type (ement-event-type event)
:state-key (ement-event-state-key event)
:unsigned (ement-event-unsigned event)
:receipts (ement-event-receipts event)
:local (ement-event-local event))))
(let* ((buffer-name (format "*Ement event: %s*"
(cl-typecase event
(ement-room-membership-events "[multiple events]")
(ement-event (ement-event-id event)))))
(event (cl-typecase event
(ement-room-membership-events
(mapcar #'event-alist (ement-room-membership-events-events event)))
(ement-event (event-alist event))))
(inhibit-read-only t))
(with-current-buffer (get-buffer-create buffer-name)
(erase-buffer)
(pp event (current-buffer))
(view-mode)
(pop-to-buffer (current-buffer))))))
(cl-defun ement-room-send-message (room session &key body formatted-body replying-to-event)
"Send message to ROOM on SESSION with BODY and FORMATTED-BODY.
Interactively, with prefix, prompt for room and session,
otherwise use current room.
REPLYING-TO-EVENT may be an event the message is in reply to; the
message will reference it appropriately.
If `ement-room-send-message-filter' is non-nil, the message's
content alist is passed through it before sending. This may be
used to, e.g. process the BODY into another format and add it to
the content (e.g. see `ement-room-send-org-filter')."
(interactive
(ement-with-room-and-session
(let* ((prompt (format "Send message (%s): " (ement-room-display-name ement-room)))
(body (ement-room-with-typing
(ement-room-read-string prompt nil nil nil
'inherit-input-method))))
(list ement-room ement-session :body body))))
(ement-send-message room session :body body :formatted-body formatted-body
:replying-to-event replying-to-event :filter ement-room-send-message-filter
:then #'ement-room-send-event-callback)
;; NOTE: This assumes that the selected window is the buffer's window. For now
;; this is almost surely the case, but in the future, we might let the function
;; send messages to other rooms more easily, so this assumption might not hold.
(when-let* ((buffer (alist-get 'buffer (ement-room-local room)))
(window (get-buffer-window buffer)))
(with-selected-window window
(when (>= (window-point) (ewoc-location (ewoc-nth ement-ewoc -1)))
;; Point is on last event: advance it to eob so that when the event is received
;; back, the window will scroll. (This might not always be desirable, because
;; the user might have point on that event for a reason, but I think in most
;; cases, it will be what's expected and most helpful.)
(setf (window-point) (point-max))))))
(cl-defun ement-room-send-emote (room session &key body)
"Send emote to ROOM on SESSION with BODY.
Interactively, with prefix, prompt for room and session,
otherwise use current room.
If `ement-room-send-message-filter' is non-nil, the message's
content alist is passed through it before sending. This may be
used to, e.g. process the BODY into another format and add it to
the content (e.g. see `ement-room-send-org-filter')."
(interactive
(ement-with-room-and-session
(let* ((prompt (format "Send emote (%s): " (ement-room-display-name ement-room)))
(body (ement-room-with-typing
(ement-room-read-string prompt nil nil nil
'inherit-input-method))))
(list ement-room ement-session :body body))))
(cl-assert (not (string-empty-p body)))
(pcase-let* (((cl-struct ement-room (id room-id) (local (map buffer))) room)
(window (when buffer (get-buffer-window buffer)))
(endpoint (format "rooms/%s/send/m.room.message/%s" (url-hexify-string room-id)
(ement--update-transaction-id session)))
(content (ement-aprog1
(ement-alist "msgtype" "m.emote"
"body" body))))
(when ement-room-send-message-filter
(setf content (funcall ement-room-send-message-filter content room)))
(ement-api session endpoint :method 'put :data (json-encode content)
:then (apply-partially #'ement-room-send-event-callback :room room :session session
:content content :data)) ;; Data is added when calling back.
;; NOTE: This assumes that the selected window is the buffer's window. For now
;; this is almost surely the case, but in the future, we might let the function
;; send messages to other rooms more easily, so this assumption might not hold.
(when window
(with-selected-window window
(when (>= (window-point) (ewoc-location (ewoc-nth ement-ewoc -1)))
;; Point is on last event: advance it to eob so that when the event is received
;; back, the window will scroll. (This might not always be desirable, because
;; the user might have point on that event for a reason, but I think in most
;; cases, it will be what's expected and most helpful.)
(setf (window-point) (point-max)))))))
(cl-defun ement-room-send-event-callback (&key data room session content)
"Callback for event-sending functions.
DATA is the parsed JSON object. If DATA's event ID is already
present in SESSION's events table, show an appropriate warning
mentioning the ROOM and CONTENT."
(pcase-let* (((map ('event_id event-id)) data))
(when (and ement-room-warn-for-already-seen-messages
(gethash event-id (ement-session-events session)))
(let ((message (format "Event ID %S already seen in session %S. This may indicate a reused transaction ID, which could mean that the event was not sent to the room (%S). You may need to disconnect, delete the `ement-sessions-file', and connect again to start a new session. Alternatively, this can happen if the event's sent-confirmation is received after the event itself is received in the next sync response, in which case no action is needed."
event-id (ement-user-id (ement-session-user session))
(ement-room-display-name room))))
(when content
(setf message (concat message (format " Event content: %S" content))))
(display-warning 'ement-room-send-event-callback message)))
(when (eq 'send ement-room-mark-rooms-read)
;; Move read markers.
(when-let ((buffer (alist-get 'buffer (ement-room-local room))))
(with-current-buffer buffer
;; NOTE: The new event may not exist in the buffer yet, so
;; we just have to use the last one.
;; FIXME: When we add local echo, this can be fixed.
(save-excursion
(goto-char (ewoc-location
(ement-room--ewoc-last-matching ement-ewoc #'ement-event-p)))
(call-interactively #'ement-room-mark-read)))))))
(defun ement-room-edit-message (event room session body)
"Edit EVENT in ROOM on SESSION to have new BODY.
The message must be one sent by the local user."
(interactive (ement-room-with-highlighted-event-at (point)
(cl-assert ement-session) (cl-assert ement-room)
(pcase-let* ((event (ewoc-data (ewoc-locate ement-ewoc)))
((cl-struct ement-session user) ement-session)
((cl-struct ement-event sender
(content (map body ('m.relates_to relates-to))))
event))
(unless (equal (ement-user-id sender) (ement-user-id user))
(user-error "You may only edit your own messages"))
(when relates-to
;; FIXME: This isn't quite right. When we show edits by replacing
;; the original event, this will need to be changed.
(user-error "Only original messages may be edited, not the edit events themselves"))
;; Remove any leading asterisk from the plain-text body.
(setf body (replace-regexp-in-string (rx bos "*" (1+ space)) "" body t t))
(ement-room-with-typing
(let* ((prompt (format "Edit message (%s): "
(ement-room-display-name ement-room)))
(body (ement-room-read-string prompt body nil nil
'inherit-input-method)))
(when (string-empty-p body)
(user-error "To delete a message, use command `ement-room-delete-message'"))
(when (yes-or-no-p (format "Edit message to: %S? " body))
(list event ement-room ement-session body)))))))
(let* ((endpoint (format "rooms/%s/send/%s/%s" (url-hexify-string (ement-room-id room))
"m.room.message" (ement--update-transaction-id session)))
(new-content (ement-alist "body" body
"msgtype" "m.text"))
(_ (when ement-room-send-message-filter
(setf new-content (funcall ement-room-send-message-filter new-content room))))
(content (ement-alist "msgtype" "m.text"
"body" body
"m.new_content" new-content
"m.relates_to" (ement-alist "rel_type" "m.replace"
"event_id" (ement-event-id event)))))
;; Prepend the asterisk after the filter may have modified the content. Note that the
;; "m.new_content" body does not get the leading asterisk, only the "content" body,
;; which is intended as a fallback.
(setf body (concat "* " body))
(ement-api session endpoint :method 'put :data (json-encode content)
:then (apply-partially #'ement-room-send-event-callback :room room :session session
:content content :data))))
(defun ement-room-delete-message (event room session &optional reason)
"Delete EVENT in ROOM on SESSION, optionally with REASON."
(interactive (ement-room-with-highlighted-event-at (point)
(if (yes-or-no-p "Delete this event? ")
(list (ewoc-data (ewoc-locate ement-ewoc))
ement-room ement-session (read-string "Reason (optional): " nil nil nil 'inherit-input-method))
;; HACK: This isn't really an error, but is there a cleaner way to cancel?
(user-error "Message not deleted"))))
(ement-redact event room session reason))
(defun ement-room-write-reply ()
"Send a reply to event at point."
(interactive)
(cl-assert ement-ewoc) (cl-assert ement-room) (cl-assert ement-session)
(cl-assert (ement-event-p (ewoc-data (ewoc-locate ement-ewoc))))
(ement-room-with-highlighted-event-at (point)
(pcase-let* ((event (ewoc-data (ewoc-locate ement-ewoc)))
(room ement-room)
(session ement-session)
(prompt (format "Send reply (%s): " (ement-room-display-name room)))
(ement-room-read-string-setup-hook
(lambda ()
(setq-local ement-room-replying-to-event event)))
(body (ement-room-with-typing
(ement-room-read-string prompt nil nil nil 'inherit-input-method))))
(ement-room-send-message room session :body body :replying-to-event event))))
(defun ement-room-send-reaction (key position)
"Send reaction of KEY to event at POSITION.
Interactively, send reaction to event at point. KEY should be a
reaction string, e.g. \"👍\"."
(interactive
(list (char-to-string (read-char-by-name "Reaction (prepend \"*\" for substring search): "))
(point)))
;; SPEC: MSC2677 <https://github.com/matrix-org/matrix-doc/pull/2677>
;; HACK: We could simplify this by storing the key in a text property...
(ement-room-with-highlighted-event-at position
(pcase-let* ((event (or (ewoc-data (ewoc-locate ement-ewoc position))
(user-error "No event at point")))
;; NOTE: Sadly, `face-at-point' doesn't work here because, e.g. if
;; hl-line-mode is enabled, it only returns the hl-line face.
((cl-struct ement-event (id event-id)) event)
((cl-struct ement-room (id room-id)) ement-room)
(endpoint (format "rooms/%s/send/m.reaction/%s" (url-hexify-string room-id)
(ement--update-transaction-id ement-session)))
(content (ement-alist "m.relates_to"
(ement-alist "rel_type" "m.annotation"
"event_id" event-id
"key" key))))
(ement-api ement-session endpoint :method 'put :data (json-encode content)
:then (apply-partially #'ement-room-send-event-callback
:room ement-room :session ement-session :content content
:data)))))
(defun ement-room-toggle-reaction (key event room session)
"Toggle reaction of KEY to EVENT in ROOM on SESSION."
(interactive
(cl-labels
((face-at-point-p
(face) (let ((face-at-point (get-text-property (point) 'face)))
(or (eq face face-at-point)
(and (listp face-at-point)
(member face face-at-point)))))
(buffer-substring-while
(beg pred &key (forward-fn #'forward-char))
"Return substring of current buffer from BEG while PRED is true."
(save-excursion
(goto-char beg)
(cl-loop while (funcall pred)
do (funcall forward-fn)
finally return (buffer-substring-no-properties beg (point)))))
(key-at
(pos) (cond ((face-at-point-p 'ement-room-reactions-key)
(buffer-substring-while
pos (lambda () (face-at-point-p 'ement-room-reactions-key))))
((face-at-point-p 'ement-room-reactions)
;; Point is in a reaction button but after the key.
(buffer-substring-while
(button-start (button-at pos))
(lambda () (face-at-point-p 'ement-room-reactions-key)))))))
(list (or (key-at (point))
(char-to-string (read-char-by-name "Reaction (prepend \"*\" for substring search): ")))
(ewoc-data (ewoc-locate ement-ewoc))
ement-room ement-session)))
(pcase-let* (((cl-struct ement-event (local (map reactions))) event)
((cl-struct ement-session user) session)
((cl-struct ement-user (id user-id)) user))
(if-let (reaction-event (cl-find-if (lambda (event)
(and (equal user-id (ement-user-id (ement-event-sender event)))
(equal key (map-nested-elt (ement-event-content event) '(m.relates_to key)))))
reactions))
;; Already sent this reaction: redact it.
(ement-redact reaction-event room session)
;; Send reaction.
(ement-room-send-reaction key (point)))))
(defun ement-room-reaction-button-action (button)
"Push reaction BUTTON at point."
;; TODO: Toggle reactions off with redactions (not in spec yet, but Element does it).
(save-excursion
(goto-char (button-start button))
(call-interactively #'ement-room-toggle-reaction)))
(defun ement-room-toggle-space (room space session)
;; Naming things is hard, but this seems the best balance between concision, ambiguity,
;; and consistency. The docstring is always there. (Or there's the sci-fi angle:
;; "spacing" a room...)
"Toggle ROOM's membership in SPACE on SESSION."
(interactive
(ement-with-room-and-session
:prompt-form (ement-complete-room :session ement-session
:predicate (lambda (room) (not (ement--room-space-p room))) )
(pcase-let* ((prompt (format "Toggle room %S's membership in space: "
(ement--format-room ement-room)))
;; TODO: Use different face for spaces the room is already in.
(`(,space ,_session) (ement-complete-room :session ement-session :prompt prompt :suggest nil
:predicate #'ement--room-space-p)))
(list ement-room space ement-session))))
(pcase-let* (((cl-struct ement-room (id child-id)) room)
(routing-server (progn
(string-match (rx (1+ (not (any ":"))) ":" (group (1+ anything))) child-id)
(match-string 1 child-id)))
(action (if (ement--room-in-space-p room space)
'remove 'add))
(data (pcase action
('add (ement-alist "via" (vector
;; FIXME: Finish and use the routing function.
;; (ement--room-routing room)
routing-server)))
('remove (make-hash-table)))))
(ement-put-state space "m.space.child" child-id data session
:then (lambda (response-data)
;; It appears that the server doesn't send the new event in the next sync (at
;; least, not to the client that put the state), so we must simulate receiving it.
(pcase-let* (((map event_id) response-data)
((cl-struct ement-session user) session)
((cl-struct ement-room (id child-id)) room)
(fake-event (make-ement-event :id event_id :type "m.space.child"
:sender user :state-key child-id
:content (json-read-from-string (json-encode data)))))
(push fake-event (ement-room-timeline space))
(run-hook-with-args 'ement-event-hook fake-event space session))
(ement-message "Room %S %s space %S"
(ement--format-room room)
(pcase action
('add "added to")
('remove "removed from"))
(ement--format-room space))))))
;;;; Functions
(defun ement-room-view (room session)
"Switch to a buffer showing ROOM on SESSION.
Uses action `ement-view-room-display-buffer-action', which see."
(interactive (ement-complete-room :session (ement-complete-session) :suggest nil
:predicate (lambda (room)
(not (ement--room-space-p room)))))
(pcase-let* (((cl-struct ement-room (local (map buffer))) room))
(unless (buffer-live-p buffer)
(setf buffer (ement-room--buffer session room (ement-room--buffer-name room))
(alist-get 'buffer (ement-room-local room)) buffer))
;; FIXME: This doesn't seem to work as desired, e.g. when
;; `ement-view-room-display-buffer-action' is set to `display-buffer-no-window'; I
;; guess because `pop-to-buffer' selects a window.
(pop-to-buffer buffer ement-view-room-display-buffer-action)
(run-hook-with-args 'ement-room-view-hook room session)))
(defalias 'ement-view-room #'ement-room-view)
(defun ement-room-view-hook-room-list-auto-update (_room session)
"Call `ement-room-list-auto-update' with SESSION.
To be used in `ement-room-view-hook', which see."
;; This function is necessary because the hook is called with the room argument, which
;; `ement-room-list-auto-update' doesn't need.
(declare (function ement-room-list-auto-update "ement-room-list"))
(ement-room-list-auto-update session))
(defun ement-room--buffer-name (room)
"Return name for ROOM's buffer."
(concat ement-room-buffer-name-prefix
(or (ement-room-display-name room)
(setf (ement-room-display-name room)
(ement--room-display-name room)))
ement-room-buffer-name-suffix))
(defun ement-room-goto-event (event)
"Go to EVENT in current buffer."
(if-let ((node (ement-room--ewoc-last-matching ement-ewoc
(lambda (data)
(and (ement-event-p data)
(equal (ement-event-id event) (ement-event-id data)))))))
(goto-char (ewoc-location node))
(error "Event not found in buffer: %S" (ement-event-id event))))
(cl-defun ement-room-retro-callback (room session data
&key (set-prev-batch t))
"Push new DATA to ROOM on SESSION and add events to room buffer.
If SET-PREV-BATCH is nil, don't set ROOM's prev-batch slot to the
\"prev_batch\" token in response DATA (this should be set,
e.g. when filling timeline gaps as opposed to retrieving messages
before the earliest-seen message)."
(declare (function ement--make-event "ement.el")
(function ement--put-event "ement.el"))
(pcase-let* (((cl-struct ement-room local) room)
((map _start end chunk state) data)
((map buffer) local)
(num-events (length chunk))
;; We do 3 things for chunk events, so we count them 3 times when
;; reporting progress. (We also may receive some state events for
;; these chunk events, but we don't bother to include them in the
;; count, and we don't report progress for them, because they are
;; likely very few compared to the number of timeline events, which is
;; what the user is interested in (e.g. when loading 1000 earlier
;; messages in #emacs:matrix.org, only 31 state events were received).
(progress-max-value (* 3 num-events)))
;; NOTE: Put the newly retrieved events at the end of the slots, because they should be
;; older events. But reverse them first, because we're using "dir=b", which the
;; spec says causes the events to be returned in reverse-chronological order, and we
;; want to process them oldest-first (important because a membership event having a
;; user's displayname should be older than a message event sent by the user).
;; NOTE: The events in `chunk' and `state' are vectors, so we
;; convert them to a list before appending.
(ement-debug num-events progress-max-value)
(setf chunk (nreverse chunk)
state (nreverse state))
;; FIXME: Like `ement--push-joined-room-events', this should probably run the `ement-event-hook' on the newly seen events.
;; Append state events.
(cl-loop for event across-ref state
do (setf event (ement--make-event event))
finally do (setf (ement-room-state room)
(append (ement-room-state room) (append state nil))))
(ement-with-progress-reporter (:reporter ("Ement: Processing earlier events..." 0 progress-max-value))
;; Append timeline events (in the "chunk").
(cl-loop for event across-ref chunk
do (setf event (ement--make-event event))
;; HACK: Put events on events table. See FIXME above about using the event hook.
(ement--put-event event nil session)
(ement-progress-update)
finally do (setf (ement-room-timeline room)
(append (ement-room-timeline room) (append chunk nil))))
(when buffer
;; Insert events into the room's buffer.
(with-current-buffer buffer
(save-window-excursion
;; NOTE: See note in `ement--update-room-buffers'.
(when-let ((buffer-window (get-buffer-window buffer)))
(select-window buffer-window))
;; FIXME: Use retro-loading in event handlers, or in --handle-events, anyway.
(ement-room--process-events chunk)
(when set-prev-batch
;; This feels a little hacky, but maybe not too bad.
(setf (ement-room-prev-batch room) end))
(setf ement-room-retro-loading nil)))))
(message "Ement: Loaded %s earlier events." num-events)))
(defun ement-room--insert-events (events &optional retro)
"Insert EVENTS into current buffer.
Calls `ement-room--insert-event' for each event and inserts
timestamp headers into appropriate places while maintaining
point's position. If RETRO is non-nil, assume EVENTS are earlier
than any existing events, and only insert timestamp headers up to
the previously oldest event."
(let (buffer-window point-node orig-first-node point-max-p)
(when (get-buffer-window (current-buffer))
;; HACK: See below.
(setf buffer-window (get-buffer-window (current-buffer))
point-max-p (= (point) (point-max))))
(when (and buffer-window retro)
(setf point-node (ewoc-locate ement-ewoc (window-start buffer-window))
orig-first-node (ewoc-nth ement-ewoc 0)))
(save-window-excursion
;; NOTE: When inserting some events, seemingly only replies, if a different buffer's
;; window is selected, and this buffer's window-point is at the bottom, the formatted
;; events may be inserted into the wrong place in the buffer, even though they are
;; inserted into the EWOC at the right place. We work around this by selecting the
;; buffer's window while inserting events, if it has one. (I don't know if this is a bug
;; in EWOC or in this file somewhere. But this has been particularly nasty to debug.)
(when buffer-window
(select-window buffer-window))
(cl-loop for event being the elements of events
do (ement-room--process-event event)
do (ement-progress-update)))
;; Since events can be received in any order, we have to check the whole buffer
;; for where to insert new timestamp headers. (Avoiding that would require
;; getting a list of newly inserted nodes and checking each one instead of every
;; node in the buffer. Doing that now would probably be premature optimization,
;; though it will likely be necessary if users keep buffers open for busy rooms
;; for a long time, as the time to do this in each buffer will increase with the
;; number of events. At least we only do it once per batch of events.)
(ement-room--insert-ts-headers nil (when retro orig-first-node))
(when ement-room-sender-in-headers
(ement-room--insert-sender-headers ement-ewoc))
(when buffer-window
(cond (retro (with-selected-window buffer-window
(set-window-start buffer-window (ewoc-location point-node))
;; TODO: Experiment with this.
(forward-line -1)))
(point-max-p (set-window-point buffer-window (point-max)))))))
(cl-defun ement-room--send-typing (session room &key (typing t))
"Send a typing notification for ROOM on SESSION."
(pcase-let* (((cl-struct ement-session user) session)
((cl-struct ement-user (id user-id)) user)
((cl-struct ement-room (id room-id)) room)
(endpoint (format "rooms/%s/typing/%s"
(url-hexify-string room-id) (url-hexify-string user-id)))
(data (ement-alist "typing" typing "timeout" 20000)))
(ement-api session endpoint :method 'put :data (json-encode data)
;; We don't really care about the response, I think.
:then #'ignore)))
(define-derived-mode ement-room-mode fundamental-mode
`("Ement-Room"
(:eval (unless (map-elt ement-syncs ement-session)
(propertize ":Not-syncing"
'face 'font-lock-warning-face
'help-echo "Automatic syncing was interrupted; press \"g\" to resume"))))
"Major mode for Ement room buffers.
This mode initializes a buffer to be used for showing events in
an Ement room. It kills all local variables, removes overlays,
and erases the buffer."
(let ((inhibit-read-only t))
(erase-buffer))
(remove-overlays)
(setf buffer-read-only t
left-margin-width ement-room-left-margin-width
right-margin-width ement-room-right-margin-width
imenu-create-index-function #'ement-room--imenu-create-index-function
;; TODO: Use EWOC header/footer for, e.g. typing messages.
ement-ewoc (ewoc-create #'ement-room--pp-thing))
;; Set the URL handler. Note that `browse-url-handlers' was added in 28.1;
;; prior to that `browse-url-browser-function' served double-duty.
;; TODO: Remove compat code when requiring Emacs >=28.
(let ((handler (cons ement-room-matrix.to-url-regexp #'ement-room-browse-url)))
(if (boundp 'browse-url-handlers)
(setq-local browse-url-handlers (cons handler browse-url-handlers))
(setq-local browse-url-browser-function
(cons handler
(if (consp browse-url-browser-function)
browse-url-browser-function
(and browse-url-browser-function
(list (cons "." browse-url-browser-function))))))))
(setq-local completion-at-point-functions
'(ement-room--complete-members-at-point ement-room--complete-rooms-at-point))
(setq-local dnd-protocol-alist (append '(("^file:///" . ement-room-dnd-upload-file)
("^file:" . ement-room-dnd-upload-file))
dnd-protocol-alist)))
(add-hook 'ement-room-mode-hook 'visual-line-mode)
(defun ement-room-read-string (prompt &optional initial-input history default-value inherit-input-method)
"Call `read-from-minibuffer', binding variables and keys for Ement.
Arguments PROMPT, INITIAL-INPUT, HISTORY, DEFAULT-VALUE, and
INHERIT-INPUT-METHOD are as those expected by `read-string',
which see. Runs hook `ement-room-read-string-setup-hook', which
see."
(let ((room ement-room)
(session ement-session))
(minibuffer-with-setup-hook
(lambda ()
"Bind keys and variables locally (to be called in minibuffer)."
(setq-local ement-room room)
(setq-local ement-session session)
(setq-local completion-at-point-functions
'(ement-room--complete-members-at-point ement-room--complete-rooms-at-point))
(visual-line-mode 1)
(run-hooks 'ement-room-read-string-setup-hook))
(read-from-minibuffer prompt initial-input ement-room-minibuffer-map
nil history default-value inherit-input-method))))
(defun ement-room--buffer (session room name)
"Return buffer named NAME showing ROOM's events on SESSION.
If ROOM has no buffer, one is made and stored in the room's local
data slot."
(or (map-elt (ement-room-local room) 'buffer)
(let ((new-buffer (generate-new-buffer name)))
(with-current-buffer new-buffer
(ement-room-mode)
(setf header-line-format (when ement-room-header-line-format
'ement-room-header-line-format)
ement-session session
ement-room room
list-buffers-directory (or (ement-room-canonical-alias room)
(ement-room-id room))
;; Track buffer in room's slot.
(map-elt (ement-room-local room) 'buffer) (current-buffer))
(add-hook 'kill-buffer-hook
(lambda ()
(setf (map-elt (ement-room-local room) 'buffer) nil))
nil 'local)
(setq-local bookmark-make-record-function #'ement-room-bookmark-make-record)
;; Set initial header and footer. (Do this before processing events, which
;; might cause the header/footer to be changed (e.g. a tombstone event).
(let ((header (if (cl-loop for state in (list (ement-room-state ement-room)
(ement-room-invite-state ement-room))
thereis (cl-find "m.room.encryption" state
:test #'equal :key #'ement-event-type))
(propertize "This appears to be an encrypted room, which is not natively supported by Ement.el. (See information about using Pantalaimon in Ement.el documentation.)"
'face 'font-lock-warning-face)
""))
(footer (pcase (ement-room-status ement-room)
;; Set header and footer for an invited room.
('invite
(concat (propertize "You've been invited to this room. "
'face 'font-lock-warning-face)
(propertize "[Join this room]"
'button '(t)
'category 'default-button
'mouse-face 'highlight
'follow-link t
'action (lambda (_button)
;; Kill the room buffer so it can be recreated after joining
;; (which will cleanly update the room's name, footer, etc).
(let ((room ement-room)
(session ement-session))
(kill-buffer)
(message "Joining room... (buffer will be reopened after joining)")
(ement-room-join (ement-room-id room) session))))))
(_ ""))))
(ewoc-set-hf ement-ewoc header footer))
(setf
;; Clear new-events, because those only matter when a buffer is already open.
(alist-get 'new-events (ement-room-local room)) nil
;; Set the new buffer in the room's local alist so that it
;; can be used by event-inserting functions before this
;; function returns, e.g. `ement-room--add-member-face'.
(alist-get 'buffer (ement-room-local room)) new-buffer)
;; We don't use `ement-room--insert-events' to avoid extra
;; calls to `ement-room--insert-ts-headers'.
;; NOTE: We handle the events in chronological order (i.e. the reverse of the
;; stored order, which is latest-first), because some logic depends on this
;; (e.g. processing a message-edit event before the edited event would mean the
;; edited event would not yet be in the buffer).
(ement-room--process-events (reverse (ement-room-state room)))
(ement-room--process-events (reverse (ement-room-timeline room)))
(ement-room--insert-ts-headers)
(when ement-room-sender-in-headers
(ement-room--insert-sender-headers ement-ewoc))
(ement-room-move-read-markers room
:read-event (when-let ((event (alist-get "m.read" (ement-room-account-data room) nil nil #'equal)))
(map-nested-elt event '(content event_id)))
:fully-read-event (when-let ((event (alist-get "m.fully_read" (ement-room-account-data room) nil nil #'equal)))
(map-nested-elt event '(content event_id)))))
;; Return the buffer!
new-buffer)))
(defun ement-room--event-data (id)
"Return event struct for event ID in current buffer."
;; Search from bottom, most likely to be faster.
(cl-loop with node = (ewoc-nth ement-ewoc -1)
while node
for data = (ewoc-data node)
when (and (ement-event-p data)
(equal id (ement-event-id data)))
return data
do (setf node (ewoc-prev ement-ewoc node))))
(defun ement-room--escape-% (string)
"Return STRING with \"%\" escaped.
Needed to display things in the header line."
(replace-regexp-in-string (rx "%") "%%" string t t))
;;;;; Imenu
(defconst ement-room-timestamp-header-imenu-format "%Y-%m-%d (%A) %H:%M"
"Format string for timestamps in Imenu indexes.")
(defun ement-room--imenu-create-index-function ()
"Return Imenu index for the current buffer.
For use as `imenu-create-index-function'."
(let ((timestamp-nodes (ement-room--ewoc-collect-nodes
ement-ewoc (lambda (node)
(pcase (ewoc-data node)
(`(ts . ,_) t))))))
(cl-loop for node in timestamp-nodes
collect (pcase-let*
((`(ts ,timestamp) (ewoc-data node))
(formatted (format-time-string ement-room-timestamp-header-imenu-format timestamp)))
(cons formatted (ewoc-location node))))))
;;;;; Occur
(defvar-local ement-room-occur-pred nil
"Predicate used to refresh `ement-room-occur' buffers.")
(define-derived-mode ement-room-occur-mode ement-room-mode "Ement-Room-Occur")
(progn
(define-key ement-room-occur-mode-map [remap ement-room-send-message] #'ement-room-occur-find-event)
(define-key ement-room-occur-mode-map (kbd "g") #'revert-buffer)
(define-key ement-room-occur-mode-map (kbd "n") #'ement-room-occur-next)
(define-key ement-room-occur-mode-map (kbd "p") #'ement-room-occur-prev))
(cl-defun ement-room-occur (&key user-id regexp pred header)
"Show known events in current buffer matching args in a new buffer.
If REGEXP, show events whose sender or body content match it. Or
if USER-ID, show events from that user. Or if PRED, show events
matching it. HEADER is used if given, or set according to other
arguments."
(interactive (let* ((regexp (read-regexp "Regexp (leave empty to select user instead)"))
(user-id (when (string-empty-p regexp)
(ement-complete-user-id))))
(list :regexp regexp :user-id user-id)))
(let* ((session ement-session)
(room ement-room)
(occur-buffer (get-buffer-create (format "*Ement Room Occur: %s*" (ement-room-display-name room))))
(pred (cond (pred)
((not (string-empty-p regexp))
(lambda (data)
(and (ement-event-p data)
(or (string-match regexp (ement-user-id (ement-event-sender data)))
(when-let ((room-display-name
(gethash (ement-event-sender data) (ement-room-displaynames room))))
(string-match regexp room-display-name))
(when-let ((body (alist-get 'body (ement-event-content data))))
(string-match regexp body))))))
(user-id
(lambda (data)
(and (ement-event-p data)
(equal user-id (ement-user-id (ement-event-sender data))))))))
(header (cond (header)
((not (string-empty-p regexp))
(format "Events matching %S in %s" regexp (ement-room-display-name room)))
(user-id
(format "Events from %s in %s" user-id (ement-room-display-name room))))))
(with-current-buffer occur-buffer
(let ((inhibit-read-only t))
(erase-buffer))
(ement-room-occur-mode)
(setf header-line-format header
ement-session session
ement-room room)
(setq-local revert-buffer-function (lambda (&rest _)
(interactive)
(let ((event-at-point (ewoc-data (ewoc-locate ement-ewoc))))
(with-current-buffer (alist-get 'buffer (ement-room-local room))
(ement-room-occur :pred pred :header header)
(when-let ((node (ement-room--ewoc-last-matching ement-ewoc
(lambda (data)
(eq event-at-point data)))))
(ewoc-goto-node ement-ewoc node))))))
(ement-room--process-events (reverse (ement-room-state room)))
(ement-room--process-events (reverse (ement-room-timeline room)))
(ewoc-filter ement-ewoc pred)
;; TODO: Insert date header before first event.
(ement-room--insert-ts-headers))
(pop-to-buffer occur-buffer)))
(defun ement-room-occur-find-event (event)
"Find EVENT in room's main buffer."
(interactive (list (ewoc-data (ewoc-locate ement-ewoc))))
(pcase-let* (((cl-struct ement-room (local (map buffer))) ement-room)
((cl-struct ement-event id) event))
(display-buffer buffer)
(with-selected-window (get-buffer-window buffer)
(ement-room-find-event id))))
(cl-defun ement-room-occur-next (&optional (n 1))
"Go to Nth next event."
(interactive)
(let ((command (if (> n 0)
#'ement-room-goto-next
#'ement-room-goto-prev)))
(cl-loop for i below (abs n)
do (call-interactively command))
(ement-room-occur-find-event (ewoc-data (ewoc-locate ement-ewoc)))))
(cl-defun ement-room-occur-prev (&optional (n 1))
"Go to Nth previous event."
(interactive)
(ement-room-occur-next (- n)))
;;;;; Events
;; Functions to handle types of events.
;; NOTE: At the moment, this only handles "m.typing" ephemeral events. Message
;; events are handled elsewhere. A better framework should be designed...
;; TODO: Define other handlers this way.
;; MAYBE: Should we intern these functions? That means every event
;; handled has to concat and intern. Should we use lambdas in an
;; alist or hash-table instead? For now let's use an alist.
(defvar ement-users)
(defvar ement-room-event-fns nil
"Alist mapping event types to functions which process events in room buffers.")
;; NOTE: While transitioning to the defevent-based handler system, we
;; define both a handle-events and handle-event function that do the
;; same thing.
;; TODO: Tidy this up.
;; NOTE: --handle-events and --handle-event need to be called in the room
;; buffer's window, when it has one. This is absolutely necessary,
;; otherwise the events may be inserted at the wrong place. (I'm not
;; sure if this is a bug in EWOC or in my code, but doing this fixes it.)
(defun ement-room--process-events (events)
"Process EVENTS in current buffer.
Calls `ement-progress-update' for each event. Calls
`ement-room--insert-ts-headers' when done. Uses handlers defined
in `ement-room-event-fns'. The current buffer should be a room's
buffer."
;; FIXME: Calling `ement-room--insert-ts-headers' is convenient, but it
;; may also be called in functions that call this function, which may
;; result in it being called multiple times for a single set of events.
(cl-loop for event being the elements of events ;; EVENTS may be a list or array.
for handler = (alist-get (ement-event-type event) ement-room-event-fns nil nil #'equal)
when handler
do (funcall handler event)
do (ement-progress-update))
(ement-room--insert-ts-headers))
(defun ement-room--process-event (event)
"Process EVENT in current buffer.
Uses handlers defined in `ement-room-event-fns'. The current
buffer should be a room's buffer."
(when-let ((handler (alist-get (ement-event-type event) ement-room-event-fns nil nil #'equal)))
;; We demote any errors that happen while processing events, because it's possible for
;; events to be malformed in unexpected ways, and that could cause an error, which
;; would stop processing of other events and prevent further syncing. See,
;; e.g. <https://github.com/alphapapa/ement.el/pull/61>.
(with-demoted-errors "Ement (ement-room--process-event): Error processing event: %S"
(funcall handler event))))
;;;;;; Event handlers
(defmacro ement-room-defevent (type &rest body)
"Define an event handling function for events of TYPE.
Around the BODY, the variable `event' is bound to the event being
processed. The function is called in the room's buffer. Adds
function to `ement-room-event-fns', which see."
(declare (debug (stringp def-body))
(indent defun))
`(setf (alist-get ,type ement-room-event-fns nil nil #'string=)
(lambda (event)
,(concat "`ement-room' handler function for " type " events.")
,@body)))
(ement-room-defevent "m.reaction"
(pcase-let* (((cl-struct ement-event content) event)
((map ('m.relates_to relates-to)) content)
((map ('event_id related-id) ('rel_type rel-type) _key) relates-to))
;; TODO: Handle other rel_types?
(pcase rel-type
("m.annotation"
;; Look for related event in timeline.
(if-let ((related-event (cl-loop with fake-event = (make-ement-event :id related-id)
for timeline-event in (ement-room-timeline ement-room)
when (ement--events-equal-p fake-event timeline-event)
return timeline-event)))
;; Found related event: add reaction to local slot and invalidate node.
(progn
;; Every time a room buffer is made, these reaction events are processed again, so we use pushnew to
;; avoid duplicates. (In the future, as event-processing is refactored, this may not be necessary.)
(cl-pushnew event (map-elt (ement-event-local related-event) 'reactions))
(when-let ((nodes (ement-room--ewoc-last-matching ement-ewoc
(lambda (data)
(and (ement-event-p data)
(equal related-id (ement-event-id data)))))))
(ewoc-invalidate ement-ewoc nodes)))
;; No known related event: discard.
;; TODO: Is this the correct thing to do?
(ement-debug "No known related event for" event))))))
(ement-room-defevent "m.room.power_levels"
(ement-room--insert-event event))
(defun ement-room--format-power-levels-event (event room _session)
"Return power-levels EVENT in ROOM formatted as a string."
(pcase-let (((cl-struct ement-event sender
(content (map ('users new-users)))
(unsigned (map ('prev_content (map ('users old-users))))))
event))
(when old-users
(pcase-let* ((sender-id (ement-user-id sender))
(sender-displayname (ement--user-displayname-in room sender))
(`(,changed-user-id-symbol . ,new-level)
(cl-find-if (lambda (new-user)
(let ((old-user (cl-find (car new-user) old-users
:key #'car)))
(or (not old-user)
(not (equal (cdr new-user) (cdr old-user))))))
new-users))
(changed-user-id (symbol-name changed-user-id-symbol))
(changed-user (when changed-user-id-symbol
(gethash changed-user-id ement-users)))
(user-displayname (if changed-user
(ement--user-displayname-in room changed-user)
changed-user-id)))
(ement-room-wrap-prefix
(if (not changed-user)
(format "%s sent a power-level event"
(propertize sender-displayname
'help-echo sender-id))
(format "%s set %s's power level to %s"
(propertize sender-displayname
'help-echo sender-id)
(propertize user-displayname 'help-echo changed-user-id)
new-level))
'face 'ement-room-membership)))))
(ement-room-defevent "m.room.canonical_alias"
(ement-room--insert-event event))
(defun ement-room--format-canonical-alias-event (event room _session)
"Return canonical alias EVENT in ROOM formatted as a string."
(pcase-let (((cl-struct ement-event sender
;; TODO: Include alt_aliases, maybe.
;; TODO: Include old alias when it is being replaced.
(content (map alias)))
event))
(ement-room-wrap-prefix
(format "%s set the canonical alias to <%s>"
(propertize (ement--user-displayname-in room sender)
'help-echo (ement-user-id sender))
alias)
'face 'ement-room-membership)))
(ement-room-defevent "m.room.redaction"
;; We handle redaction events here rather than an `ement-defevent' handler. This way we
;; do less work for events in rooms that the user isn't looking at, at the cost of doing
;; a bit more work when a room's buffer is prepared.
(pcase-let* (((cl-struct ement-event (local (map ('redacts redacted-id)))) event)
((cl-struct ement-room timeline) ement-room)
(redacted-event (cl-find redacted-id timeline
:key #'ement-event-id :test #'equal)))
(when redacted-event
(pcase-let* (((cl-struct ement-event (content
(map ('m.relates_to
(map ('event_id related-id)
('rel_type rel-type))))))
redacted-event))
;; Record the redaction in the redacted event's local slot.
(cl-pushnew event (alist-get 'redacted-by (ement-event-local redacted-event)))
(pcase rel-type
("m.annotation"
;; Redacted annotation/reaction. NOTE: Since we link annotations in a -room
;; event handler (rather than in a non-room handler), we also unlink redacted
;; ones here.
(when-let (annotated-event (cl-find related-id timeline
:key #'ement-event-id :test #'equal))
;; Remove it from the related event's local slot.
(setf (map-elt (ement-event-local annotated-event) 'reactions)
(cl-remove redacted-id (map-elt (ement-event-local annotated-event) 'reactions)
:key #'ement-event-id :test #'equal))
;; Invalidate the related event's node.
(when-let (node (ement-room--ewoc-last-matching ement-ewoc
(lambda (data)
(and (ement-event-p data)
(equal related-id (ement-event-id data))))))
(ewoc-invalidate ement-ewoc node)))))
;; Invalidate the redacted event's node.
(when-let (node (ement-room--ewoc-last-matching ement-ewoc
(lambda (data)
(and (ement-event-p data)
(equal redacted-id (ement-event-id data))))))
(ewoc-invalidate ement-ewoc node))))))
(ement-room-defevent "m.typing"
(pcase-let* (((cl-struct ement-session user) ement-session)
((cl-struct ement-user (id local-user-id)) user)
((cl-struct ement-event content) event)
((map ('user_ids user-ids)) content)
(usernames) (footer))
(setf user-ids (delete local-user-id user-ids))
(if (zerop (length user-ids))
(setf footer "")
(setf usernames (cl-loop for id across user-ids
for user = (gethash id ement-users)
if user
collect (ement--user-displayname-in ement-room user)
else collect id)
footer (propertize (concat "Typing: " (string-join usernames ", "))
'face 'font-lock-comment-face)))
(with-silent-modifications
(ewoc-set-hf ement-ewoc "" footer))))
(ement-room-defevent "m.room.avatar"
(ement-room--insert-event event))
(ement-room-defevent "org.matrix.msc3015.m.room.name.override"
(ignore event)
(setf (ement-room-display-name ement-room) (ement--room-display-name ement-room))
(rename-buffer (ement-room--buffer-name ement-room)))
(ement-room-defevent "m.room.member"
(with-silent-modifications
(ement-room--insert-event event)))
(ement-room-defevent "m.room.message"
(pcase-let* (((cl-struct ement-event content unsigned) event)
((map ('m.relates_to (map ('rel_type rel-type) ('event_id replaces-event-id)))) content)
((map ('m.relations (map ('m.replace (map ('event_id replaced-by-id)))))) unsigned))
(if (and ement-room-replace-edited-messages
replaces-event-id (equal "m.replace" rel-type))
;; Event replaces existing event: find and replace it in buffer if possible, otherwise insert it.
(or (ement-room--replace-event event)
(progn
(ement-debug "Unable to replace event ID: inserting instead." replaces-event-id)
(ement-room--insert-event event)))
;; New event.
(if replaced-by-id
(ement-debug "Event replaced: not inserting." replaced-by-id)
;; Not replaced: insert it.
(ement-room--insert-event event)))))
(ement-room-defevent "m.room.tombstone"
(pcase-let* (((cl-struct ement-event content) event)
((map body ('replacement_room new-room-id)) content)
(session ement-session)
(button (ement--button-buttonize
(propertize new-room-id 'help-echo "Join replacement room")
(lambda (_)
(ement-room-join new-room-id session))))
(banner (format "This room has been replaced. Explanation:%S Replacement room: <%s>" body button)))
(add-face-text-property 0 (length banner) 'font-lock-warning-face t banner)
;; NOTE: We assume that no more typing events will be received,
;; which would replace the footer.
(ement-room--insert-event event)
(ewoc-set-hf ement-ewoc banner banner)))
;;;;; Read markers
;; Marking rooms as read and showing lines where marks are.
(ement-room-defevent "m.read"
(ement-room-move-read-markers ement-room
:read-event (ement-event-id event)))
(ement-room-defevent "m.fully_read"
(ement-room-move-read-markers ement-room
:fully-read-event (ement-event-id event)))
(defvar-local ement-room-read-receipt-marker nil
"EWOC node for the room's read-receipt marker.")
(defvar-local ement-room-fully-read-marker nil
"EWOC node for the room's fully-read marker.")
(defface ement-room-read-receipt-marker
'((t (:inherit show-paren-match)))
"Read marker line in rooms.")
(defface ement-room-fully-read-marker
'((t (:inherit isearch)))
"Fully read marker line in rooms.")
(defcustom ement-room-send-read-receipts t
"Whether to send read receipts.
Also controls whether the read-receipt marker in a room is moved
automatically."
:type 'boolean
:group 'ement-room)
(defun ement-room-read-receipt-idle-timer ()
"Update read receipts in visible Ement room buffers.
To be called from timer stored in
`ement-read-receipt-idle-timer'."
(when ement-room-send-read-receipts
(dolist (window (window-list))
(when (and (eq 'ement-room-mode (buffer-local-value 'major-mode (window-buffer window)))
(buffer-local-value 'ement-room (window-buffer window)))
(ement-room-update-read-receipt window)))))
(defun ement-room-update-read-receipt (window)
"Update read receipt for room displayed in WINDOW.
Also, mark room's buffer as unmodified."
(with-selected-window window
(let ((read-receipt-node (ement-room--ewoc-last-matching ement-ewoc
(lambda (node-data)
(eq 'ement-room-read-receipt-marker node-data))))
(window-end-node (or (ewoc-locate ement-ewoc (window-end nil t))
(ewoc-nth ement-ewoc -1))))
(when (or
;; The window's end has been scrolled to or past the position of the
;; receipt marker.
(and read-receipt-node
(>= (window-end nil t) (ewoc-location read-receipt-node)))
;; The read receipt is outside of retrieved events.
(not read-receipt-node))
(let* ((event-node (when window-end-node
;; It seems like `window-end-node' shouldn't ever be nil,
;; but just in case...
(cl-typecase (ewoc-data window-end-node)
(ement-event window-end-node)
(t (ement-room--ewoc-next-matching ement-ewoc window-end-node
#'ement-event-p #'ewoc-prev)))))
(node-after-event (ewoc-next ement-ewoc event-node))
(event))
(when event-node
(unless (or (when node-after-event
(<= (ewoc-location node-after-event) (window-end nil t)))
(>= (window-end) (point-max)))
;; The entire event is not visible: use the previous event. (NOTE: This
;; isn't quite perfect, because apparently `window-end' considers a position
;; visible if even one pixel of its line is visible. This will have to be
;; good enough for now.)
;; FIXME: Workaround that an entire line's height need not be displayed for it to be considered so.
(setf event-node (ement-room--ewoc-next-matching ement-ewoc event-node
#'ement-event-p #'ewoc-prev)))
(setf event (ewoc-data event-node))
;; Mark the buffer as not modified so that will not contribute to its being
;; considered unread. NOTE: This will mean that any room buffer displayed in
;; a window will have its buffer marked unmodified when this function is
;; called. This is probably for the best.
(set-buffer-modified-p nil)
(unless (alist-get event ement-room-read-receipt-request)
;; No existing request for this event: cancel any outstanding request and
;; send a new one.
(when-let ((request-process (car (map-values ement-room-read-receipt-request))))
(when (process-live-p request-process)
(interrupt-process request-process)))
(setf ement-room-read-receipt-request nil)
(setf (alist-get event ement-room-read-receipt-request)
(ement-room-mark-read ement-room ement-session
:read-event event)))))))))
(defun ement-room-goto-fully-read-marker ()
"Move to the fully-read marker in the current room."
(interactive)
(if-let ((fully-read-pos (when ement-room-fully-read-marker
(ewoc-location ement-room-fully-read-marker))))
(setf (point) fully-read-pos (window-start) fully-read-pos)
;; Unlike the fully-read marker, there doesn't seem to be a
;; simple way to get the user's read-receipt marker. So if
;; we haven't seen either marker in the retrieved events, we
;; go back to the fully-read marker.
(if-let* ((fully-read-event (alist-get "m.fully_read" (ement-room-account-data ement-room) nil nil #'equal))
(fully-read-event-id (map-nested-elt fully-read-event '(content event_id))))
;; Fully-read account-data event is known.
(if (gethash fully-read-event-id (ement-session-events ement-session))
;; The fully-read event (i.e. the message event that was read, not the
;; account-data event) is already retrieved, but the marker is not present in
;; the buffer (this shouldn't happen, but somehow, it can): Reset the marker,
;; which should work around the problem.
(ement-room-mark-read ement-room ement-session
:fully-read-event (gethash fully-read-event-id (ement-session-events ement-session)))
;; Fully-read event not retrieved: search for it in room history.
(let ((buffer (current-buffer)))
(message "Searching for first unread event...")
(ement-room-retro-to ement-room ement-session fully-read-event-id
:then (lambda ()
(with-current-buffer buffer
;; HACK: Should probably call this function elsewhere, in a hook or something.
(ement-room-move-read-markers ement-room)
(ement-room-goto-fully-read-marker))))))
(error "Room has no fully-read event"))))
(cl-defun ement-room-mark-read (room session &key read-event fully-read-event)
"Mark ROOM on SESSION as read on the server.
Set \"m.read\" to READ-EVENT and \"m.fully_read\" to
FULLY-READ-EVENT. Return the API request.
Interactively, mark both types as read up to event at point."
(declare (indent defun))
(interactive
(progn
(cl-assert (equal 'ement-room-mode major-mode) nil
"This command is to be used in `ement-room-mode' buffers")
(let* ((node (ewoc-locate ement-ewoc))
(event-at-point (cl-typecase (ewoc-data node)
(ement-event (ewoc-data node))
(t (when-let ((prev-event-node (ement-room--ewoc-next-matching ement-ewoc node
#'ement-event-p #'ewoc-prev)))
(ewoc-data prev-event-node)))))
(last-event (ewoc-data (ement-room--ewoc-last-matching ement-ewoc #'ement-event-p)))
(event-to-mark-read (if (eq event-at-point last-event)
;; The node is at the end of the buffer: use the last event in the timeline
;; instead of the last node in the EWOC, because the last event in the timeline
;; might not be the last event in the EWOC (e.g. a reaction to an earlier event).
(car (ement-room-timeline ement-room))
event-at-point)))
(list ement-room ement-session
:read-event event-to-mark-read
:fully-read-event event-to-mark-read))))
(cl-assert room) (cl-assert session) (cl-assert (or read-event fully-read-event))
(if (not fully-read-event)
;; Sending only a read receipt, which uses a different endpoint
;; than when setting the fully-read marker or both.
(ement-room-send-receipt room session read-event)
;; Setting the fully-read marker, and maybe the "m.read" one too.
(pcase-let* (((cl-struct ement-room (id room-id)) room)
(endpoint (format "rooms/%s/read_markers" (url-hexify-string room-id)))
(data (ement-alist "m.fully_read" (ement-event-id fully-read-event))))
(when read-event
(push (cons "m.read" (ement-event-id read-event)) data))
;; NOTE: See similar code in `ement-room-update-read-receipt'.
(let ((request-process (ement-api session endpoint :method 'post :data (json-encode data)
:then (lambda (_data)
(ement-room-move-read-markers room
:read-event read-event :fully-read-event fully-read-event))
:else (lambda (plz-error)
(pcase (plz-error-message plz-error)
("curl process interrupted"
;; Ignore this, because it happens when we
;; update a read marker before the previous
;; update request is completed.
nil)
(_ (signal 'ement-api-error
(list (format "Ement: (ement-room-mark-read) Unexpected API error: %s"
plz-error)
plz-error))))))))
(when-let ((room-buffer (alist-get 'buffer (ement-room-local room))))
;; NOTE: Ideally we would do this before sending the new request, but to make
;; the code much simpler, we do it afterward.
(with-current-buffer room-buffer
(when-let ((request-process (car (map-values ement-room-read-receipt-request))))
(when (process-live-p request-process)
(interrupt-process request-process)))
(setf ement-room-read-receipt-request nil
(alist-get read-event ement-room-read-receipt-request) request-process)))))))
(cl-defun ement-room-send-receipt (room session event &key (type "m.read"))
"Send receipt of TYPE for EVENT to ROOM on SESSION."
(pcase-let* (((cl-struct ement-room (id room-id)) room)
((cl-struct ement-event (id event-id)) event)
(endpoint (format "rooms/%s/receipt/%s/%s"
(url-hexify-string room-id) type
(url-hexify-string event-id))))
(ement-api session endpoint :method 'post :data "{}"
:then (pcase type
("m.read" (lambda (_data)
(ement-room-move-read-markers room
:read-event event)))
;; No other type is yet specified.
(_ #'ignore)))))
(cl-defun ement-room-move-read-markers
(room &key
(read-event (when-let ((event (alist-get "m.read" (ement-room-account-data room) nil nil #'equal)))
(map-nested-elt event '(content event_id))))
(fully-read-event (when-let ((event (alist-get "m.fully_read" (ement-room-account-data room) nil nil #'equal)))
(map-nested-elt event '(content event_id)))))
"Move read markers in ROOM to READ-EVENT and FULLY-READ-EVENT.
Each event may be an `ement-event' struct or an event ID. This
updates the markers in ROOM's buffer, not on the server; see
`ement-room-mark-read' for that."
(declare (indent defun))
(cl-labels ((update-marker (symbol to-event)
(let* ((old-node (symbol-value symbol))
(new-event-id (cl-etypecase to-event
(ement-event (ement-event-id to-event))
(string to-event)))
(event-node (ement-room--ewoc-last-matching ement-ewoc
(lambda (data)
(and (ement-event-p data)
(equal (ement-event-id data) new-event-id)))))
(inhibit-read-only t))
(with-silent-modifications
(when old-node
(ewoc-delete ement-ewoc old-node))
(set symbol (when event-node
;; If the event hasn't been inserted into the buffer yet,
;; this might be nil. That shouldn't happen, but...
(ewoc-enter-after ement-ewoc event-node symbol)))))))
(when-let ((buffer (alist-get 'buffer (ement-room-local room))))
;; MAYBE: Error if no buffer? Or does it matter?
(with-current-buffer buffer
(when read-event
(update-marker 'ement-room-read-receipt-marker read-event))
(when fully-read-event
(update-marker 'ement-room-fully-read-marker fully-read-event))))
;; NOTE: Return nil so that, in the event this function is called manually with `eval-expression',
;; it does not cause an error due to the return value being an EWOC node, which is a structure too
;; big and/or circular to print. (This was one of those bugs that only happens WHEN debugging.)
nil))
;;;;; EWOC
(cl-defun ement-room--ewoc-next-matching (ewoc node pred &optional (move-fn #'ewoc-next))
"Return the next node in EWOC after NODE that PRED is true of.
PRED is called with node's data. Moves to next node by MOVE-FN."
(declare (indent defun))
(cl-loop do (setf node (funcall move-fn ewoc node))
until (or (null node)
(funcall pred (ewoc-data node)))
finally return node))
(defun ement-room--ewoc-last-matching (ewoc predicate)
"Return the last node in EWOC matching PREDICATE.
PREDICATE is called with node's data. Searches backward from
last node."
(declare (indent defun))
;; Intended to be like `ewoc-collect', but returning as soon as a match is found.
(cl-loop with node = (ewoc-nth ewoc -1)
while node
when (funcall predicate (ewoc-data node))
return node
do (setf node (ewoc-prev ewoc node))))
(defun ement-room--ewoc-collect-nodes (ewoc predicate)
"Collect all nodes in EWOC matching PREDICATE.
PREDICATE is called with the full node."
;; Intended to be like `ewoc-collect', but working with the full node instead of just the node's data.
(cl-loop with node = (ewoc-nth ewoc 0)
do (setf node (ewoc-next ewoc node))
while node
when (funcall predicate node)
collect node))
(defun ement-room--insert-ts-headers (&optional start-node end-node)
"Insert timestamp headers into current buffer's `ement-ewoc'.
Inserts headers between START-NODE and END-NODE, which default to
the first and last nodes in the buffer, respectively."
(let* ((type-predicate (lambda (node-data)
(and (ement-event-p node-data)
(not (equal "m.room.member" (ement-event-type node-data))))))
(ewoc ement-ewoc)
(end-node (or end-node
(ewoc-nth ewoc -1)))
(end-pos (if end-node
(ewoc-location end-node)
;; HACK: Trying to work around a bug in case the
;; room doesn't seem to have any events yet.
(point-max)))
(node-b (or start-node (ewoc-nth ewoc 0)))
node-a)
;; On the first loop iteration, node-a is set to the first matching
;; node after node-b; then it's set to the first node after node-a.
(while (and (setf node-a (ement-room--ewoc-next-matching ewoc (or node-a node-b) type-predicate)
node-b (when node-a
(ement-room--ewoc-next-matching ewoc node-a type-predicate)))
(not (or (> (ewoc-location node-a) end-pos)
(when node-b
(> (ewoc-location node-b) end-pos)))))
(cl-labels ((format-event
(event) (format "TS:%S (%s) Sender:%s Message:%S"
(/ (ement-event-origin-server-ts (ewoc-data event)) 1000)
(format-time-string "%Y-%m-%d %H:%M:%S"
(/ (ement-event-origin-server-ts (ewoc-data event)) 1000))
(ement-user-id (ement-event-sender (ewoc-data event)))
(when (alist-get 'body (ement-event-content (ewoc-data event)))
(substring-no-properties
(truncate-string-to-width (alist-get 'body (ement-event-content (ewoc-data event))) 20))))))
(ement-debug "Comparing event timestamps:"
(list 'A (format-event node-a))
(list 'B (format-event node-b))))
;; NOTE: Matrix timestamps are in milliseconds.
(let* ((a-ts (/ (ement-event-origin-server-ts (ewoc-data node-a)) 1000))
(b-ts (/ (ement-event-origin-server-ts (ewoc-data node-b)) 1000))
(diff-seconds (- b-ts a-ts))
(ement-room-timestamp-header-format ement-room-timestamp-header-format))
(when (and (>= diff-seconds ement-room-timestamp-header-delta)
(not (when-let ((node-after-a (ewoc-next ewoc node-a)))
(pcase (ewoc-data node-after-a)
(`(ts . ,_) t)
((or 'ement-room-read-receipt-marker 'ement-room-fully-read-marker) t)))))
(unless (equal (time-to-days a-ts) (time-to-days b-ts))
;; Different date: bind format to print date.
(let ((ement-room-timestamp-header-format ement-room-timestamp-header-with-date-format))
;; Insert the date-only header.
(setf node-a (ewoc-enter-after ewoc node-a (list 'ts b-ts)))))
(with-silent-modifications
;; Avoid marking a buffer as modified just because we inserted a ts
;; header (this function may be called after other events which shouldn't
;; cause it to be marked modified, like moving the read markers).
(ewoc-enter-after ewoc node-a (list 'ts b-ts))))))))
(cl-defun ement-room--insert-sender-headers
(ewoc &optional (start-node (ewoc-nth ewoc 0)) (end-node (ewoc-nth ewoc -1)))
;; TODO: Use this in appropriate places.
"Insert sender headers into EWOC.
Inserts headers between START-NODE and END-NODE, which default to
the first and last nodes in the buffer, respectively."
(cl-labels ((read-marker-p
(data) (member data '(ement-room-fully-read-marker
ement-room-read-receipt-marker)))
(message-event-p
(data) (and (ement-event-p data)
(equal "m.room.message" (ement-event-type data))))
(insert-sender-before
(node) (ewoc-enter-before ewoc node (ement-event-sender (ewoc-data node)))))
(let* ((event-node (if (ement-event-p (ewoc-data start-node))
start-node
(ement-room--ewoc-next-matching ewoc start-node
#'ement-event-p)))
(prev-node (when event-node
;; Just in case...
(ewoc-prev ewoc event-node))))
(while (and event-node
;; I don't like looking up the location of these nodes on every loop
;; iteration, but it seems like the only reliable way to determine
;; whether we've reached the end node. However, when this function is
;; called for short batches of events (or even a single event, like when
;; called from `ement-room--insert-event'), the overhead should be
;; minimal.
(<= (ewoc-location event-node) (ewoc-location end-node)))
(when (message-event-p (ewoc-data event-node))
(if (not prev-node)
;; No previous node and event is a message: insert header.
(insert-sender-before event-node)
;; Previous node exists.
(when (read-marker-p (ewoc-data prev-node))
;; Previous node is a read marker: we want to act as if they don't exist, so
;; we set `prev-node' to the non-marker node before it.
(setf prev-node (ement-room--ewoc-next-matching ewoc prev-node
(lambda (data)
(not (read-marker-p data)))
#'ewoc-prev)))
(when prev-node
;; A previous node still exists: maybe we need to add a header.
(cl-typecase (ewoc-data prev-node)
(ement-event
;; Previous node is an event.
(when (and (message-event-p (ewoc-data prev-node))
(not (equal (ement-event-sender (ewoc-data prev-node))
(ement-event-sender (ewoc-data event-node)))))
;; Previous node is a message event with a different sender: insert
;; header.
(insert-sender-before event-node)))
((or ement-user ement-room-membership-events)
;; Previous node is a user or coalesced membership events: do not insert
;; header.
nil)
(t
;; Previous node is not an event and not a read marker: insert header.
(insert-sender-before event-node))))))
(setf event-node (ement-room--ewoc-next-matching ewoc event-node
#'ement-event-p)
prev-node (when event-node
(ewoc-prev ewoc event-node)))))))
(defun ement-room--coalesce-nodes (a b ewoc)
"Try to coalesce events in nodes A and B in EWOC.
Return absorbing node if coalesced."
(cl-labels ((coalescable-p
(node) (or (and (ement-event-p (ewoc-data node))
(member (ement-event-type (ewoc-data node)) '("m.room.member")))
(ement-room-membership-events-p (ewoc-data node)))))
(when (and (coalescable-p a) (coalescable-p b))
(let* ((absorbing-node (if (or (ement-room-membership-events-p (ewoc-data a))
(not (ement-room-membership-events-p (ewoc-data b))))
a b))
(absorbed-node (if (eq absorbing-node a) b a)))
(cl-etypecase (ewoc-data absorbing-node)
(ement-room-membership-events nil)
(ement-event (setf (ewoc-data absorbing-node) (ement-room-membership-events--update
(make-ement-room-membership-events
:events (list (ewoc-data absorbing-node)))))))
(push (ewoc-data absorbed-node) (ement-room-membership-events-events (ewoc-data absorbing-node)))
(ement-room-membership-events--update (ewoc-data absorbing-node))
(ewoc-delete ewoc absorbed-node)
(ewoc-invalidate ewoc absorbing-node)
absorbing-node))))
(defun ement-room--insert-event (event)
"Insert EVENT into current buffer."
(cl-labels ((format-event
(event) (format "TS:%S (%s) Sender:%s Message:%S"
(/ (ement-event-origin-server-ts event) 1000)
(format-time-string "%Y-%m-%d %H:%M:%S"
(/ (ement-event-origin-server-ts event) 1000))
(ement-user-id (ement-event-sender event))
(when (alist-get 'body (ement-event-content event))
(substring-no-properties
(truncate-string-to-width (alist-get 'body (ement-event-content event)) 20)))))
(find-node-if
(ewoc pred &key (move #'ewoc-prev) (start (ewoc-nth ewoc -1)))
"Return node in EWOC whose data matches PRED.
Search starts from node START and moves by NEXT."
(cl-loop for node = start then (funcall move ewoc node)
while node
when (funcall pred (ewoc-data node))
return node))
(timestamped-node-p (data)
(pcase data
((pred ement-event-p) t)
((pred ement-room-membership-events-p) t)
(`(ts . ,_) t)))
(node-ts (data)
(pcase data
((pred ement-event-p) (ement-event-origin-server-ts data))
((pred ement-room-membership-events-p)
;; Not sure whether to use earliest or latest ts; let's try this for now.
(ement-room-membership-events-earliest-ts data))
(`(ts ,ts)
;; Matrix server timestamps are in ms, so we must convert back.
(* 1000 ts))))
(node< (a b)
"Return non-nil if event A's timestamp is before B's."
(< (node-ts a) (node-ts b))))
(ement-debug "INSERTING NEW EVENT: " (format-event event))
(let* ((ewoc ement-ewoc)
(event-node-before (ement-room--ewoc-node-before ewoc event #'node< :pred #'timestamped-node-p))
new-node)
;; HACK: Insert after any read markers.
(cl-loop for node-after-node-before = (ewoc-next ewoc event-node-before)
while node-after-node-before
while (not (ement-event-p (ewoc-data node-after-node-before)))
do (setf event-node-before node-after-node-before))
(setf new-node (if (not event-node-before)
(progn
(ement-debug "No event before it: add first.")
(if-let ((first-node (ewoc-nth ewoc 0)))
(progn
(ement-debug "EWOC not empty.")
(if (and (ement-user-p (ewoc-data first-node))
(equal (ement-event-sender event)
(ewoc-data first-node)))
(progn
(ement-debug "First node is header for this sender: insert after it, instead.")
(setf event-node-before first-node)
(ewoc-enter-after ewoc first-node event))
(ement-debug "First node is not header for this sender: insert first.")
(ewoc-enter-first ewoc event)))
(ement-debug "EWOC empty: add first.")
(ewoc-enter-first ewoc event)))
(ement-debug "Found event before new event: insert after it.")
(when-let ((next-node (ewoc-next ewoc event-node-before)))
(when (and (ement-user-p (ewoc-data next-node))
(equal (ement-event-sender event)
(ewoc-data next-node)))
(ement-debug "Next node is header for this sender: insert after it, instead.")
(setf event-node-before next-node)))
(ement-debug "Inserting after event"
;; NOTE: `format-event' is only for debugging, and it
;; doesn't handle user headers, so commenting it out or now.
;; (format-event (ewoc-data event-node-before))
;; NOTE: And it's *Very Bad* to pass the raw node data
;; to `ement-debug', because it makes event insertion
;; *Very Slow*. So we just comment that out for now.
;; (ewoc-data event-node-before)
)
(ewoc-enter-after ewoc event-node-before event)))
(when ement-room-coalesce-events
;; Try to coalesce events.
;; TODO: Move this to a separate function and call it from where this function is called.
(setf new-node (or (when event-node-before
(ement-room--coalesce-nodes event-node-before new-node ewoc))
(when (ewoc-next ewoc new-node)
(ement-room--coalesce-nodes new-node (ewoc-next ewoc new-node) ewoc))
new-node)))
(when ement-room-sender-in-headers
(ement-room--insert-sender-headers ewoc new-node new-node))
;; Return new node.
new-node)))
(defun ement-room--replace-event (new-event)
"Replace appropriate event with NEW-EVENT in current buffer.
If replaced event is not found, return nil, otherwise non-nil."
(let* ((ewoc ement-ewoc)
(old-event-node (ement-room--ewoc-last-matching ewoc
(lambda (data)
(cl-typecase data
(ement-event (ement--events-equal-p data new-event)))))))
(when old-event-node
;; TODO: Record old events in new event's local data, and make it accessible when inspecting the new event.
(let ((node-before (ewoc-prev ewoc old-event-node))
(inhibit-read-only t))
(ewoc-delete ewoc old-event-node)
(if node-before
(ewoc-enter-after ewoc node-before new-event)
(ewoc-enter-first ewoc new-event))))))
(cl-defun ement-room--ewoc-node-before (ewoc data <-fn
&key (from 'last) (pred #'identity))
"Return node in EWOC that matches PRED and belongs before DATA by <-FN.
Search from FROM (either `first' or `last')."
(cl-assert (member from '(first last)))
(if (null (ewoc-nth ewoc 0))
(ement-debug "EWOC is empty: returning nil.")
(ement-debug "EWOC has data: add at appropriate place.")
(cl-labels ((next-matching
(ewoc node next-fn pred) (cl-loop do (setf node (funcall next-fn ewoc node))
until (or (null node)
(funcall pred (ewoc-data node)))
finally return node)))
(let* ((next-fn (pcase from ('first #'ewoc-next) ('last #'ewoc-prev)))
(start-node (ewoc-nth ewoc (pcase from ('first 0) ('last -1)))))
(unless (funcall pred (ewoc-data start-node))
(setf start-node (next-matching ewoc start-node next-fn pred)))
(if (funcall <-fn (ewoc-data start-node) data)
(progn
(ement-debug "New data goes before start node.")
start-node)
(ement-debug "New data goes after start node: find node before new data.")
(let ((compare-node start-node))
(cl-loop while (setf compare-node (next-matching ewoc compare-node next-fn pred))
until (funcall <-fn (ewoc-data compare-node) data)
finally return (if compare-node
(progn
(ement-debug "Found place: enter there.")
compare-node)
(ement-debug "Reached end of collection: insert there.")
(pcase from
('first (ewoc-nth ewoc -1))
('last nil))))))))))
;;;;; Formatting
(defun ement-room--pp-thing (thing)
"Pretty-print THING.
To be used as the pretty-printer for `ewoc-create'. THING may be
an `ement-event' or `ement-user' struct, or a list like `(ts
TIMESTAMP)', where TIMESTAMP is a Unix timestamp number of
seconds."
;; TODO: Use handlers to insert so e.g. membership events can be inserted silently.
;; TODO: Use `cl-defmethod' and define methods for each of these THING types. (I've
;; benchmarked thoroughly and found no difference in performance between using
;; `cl-defmethod' and using a `defun' with `pcase', so as long as the `cl-defmethod'
;; specializer is sufficient, I see no reason not to use it.)
(pcase-exhaustive thing
((pred ement-event-p)
(insert "" (ement-room--format-event thing ement-room ement-session)))
((pred ement-user-p)
(insert (propertize (ement--format-user thing)
'display ement-room-username-display-property)))
(`(ts ,(and (pred numberp) ts)) ;; Insert a date header.
(let* ((string (format-time-string ement-room-timestamp-header-format ts))
(width (string-width string))
(maybe-newline (if (equal ement-room-timestamp-header-format ement-room-timestamp-header-with-date-format)
;; HACK: Rather than using another variable, compare the format strings to
;; determine whether the date is changing: if so, add a newline before the header.
(progn
(cl-incf width 3)
"\n")
""))
(alignment-space (pcase ement-room-timestamp-header-align
('right (propertize " "
'display `(space :align-to (- text ,(1+ width)))))
('center (propertize " "
'display `(space :align-to (- center ,(/ (1+ width) 2)))))
(_ " "))))
(insert maybe-newline
alignment-space
(propertize string
'face 'ement-room-timestamp-header))))
((or 'ement-room-read-receipt-marker 'ement-room-fully-read-marker)
(insert (propertize " "
'display '(space :width text :height (1))
'face thing)))
((pred ement-room-membership-events-p)
(let ((formatted-events (ement-room--format-membership-events thing ement-room)))
(add-face-text-property 0 (length formatted-events)
'ement-room-membership 'append formatted-events)
(insert (ement-room-wrap-prefix formatted-events))))))
;; (defun ement-room--format-event (event)
;; "Format `ement-event' EVENT."
;; (pcase-let* (((cl-struct ement-event sender type content origin-server-ts) event)
;; ((map body format ('formatted_body formatted-body)) content)
;; (ts (/ origin-server-ts 1000)) ; Matrix timestamps are in milliseconds.
;; (body (if (not formatted-body)
;; body
;; (pcase format
;; ("org.matrix.custom.html"
;; (ement-room--render-html formatted-body))
;; (_ (format "[unknown formatted-body format: %s] %s" format body)))))
;; (timestamp (propertize
;; " " 'display `((margin left-margin)
;; ,(propertize (format-time-string ement-room-timestamp-format ts)
;; 'face 'ement-room-timestamp))))
;; (body-face (pcase type
;; ("m.room.member" 'ement-room-membership)
;; (_ (if (equal (ement-user-id sender)
;; (ement-user-id (ement-session-user ement-session)))
;; 'ement-room-self-message 'default))))
;; (string (pcase type
;; ("m.room.message" body)
;; ("m.room.member" "")
;; (_ (format "[unknown event-type: %s] %s" type body)))))
;; (add-face-text-property 0 (length body) body-face 'append body)
;; (prog1 (concat timestamp string)
;; ;; Hacky or elegant? We return the string, but for certain event
;; ;; types, we also insert a widget (this function is called by
;; ;; EWOC with point at the insertion position). Seems to work...
;; (pcase type
;; ("m.room.member"
;; (widget-create 'ement-room-membership
;; :button-face 'ement-room-membership
;; :value (list (alist-get 'membership content))))))))
(defun ement-room--format-event (event room session)
"Return EVENT in ROOM on SESSION formatted.
Formats according to `ement-room-message-format-spec', which see."
(concat (pcase (ement-event-type event)
;; TODO: Define these with a macro, like the defevent and format-spec ones.
("m.room.message" (ement-room--format-message event room session))
("m.room.member"
(widget-create 'ement-room-membership
:button-face 'ement-room-membership
:value event)
"")
("m.reaction"
;; Handled by defevent-based handler.
"")
("m.room.avatar"
(ement-room-wrap-prefix
(format "%s changed the room's avatar."
(propertize (ement--user-displayname-in room (ement-event-sender event))
'help-echo (ement-user-id (ement-event-sender event))))
'face 'ement-room-membership))
("m.room.power_levels"
(ement-room--format-power-levels-event event room session))
("m.room.canonical_alias"
(ement-room--format-canonical-alias-event event room session))
(_ (ement-room-wrap-prefix
(format "[sender:%s type:%s]"
(ement-user-id (ement-event-sender event))
(ement-event-type event))
'help-echo (format "%S" (ement-event-content event)))))
(propertize " "
'display ement-room-event-separator-display-property)))
(defun ement-room--format-reactions (event)
"Return formatted reactions to EVENT."
;; TODO: Like other events, pop to a buffer showing the raw reaction events when a key is pressed.
(if-let ((reactions (map-elt (ement-event-local event) 'reactions)))
(cl-labels ((format-reaction
(ks) (pcase-let* ((`(,key . ,senders) ks)
(key (propertize key 'face 'ement-room-reactions-key))
(count (propertize (format " (%s)" (length senders))
'face 'ement-room-reactions))
(string
(propertize (concat key count)
'button '(t)
'category 'default-button
'action #'ement-room-reaction-button-action
'follow-link t
'help-echo (lambda (_window buffer _pos)
;; NOTE: If the reaction key string is a Unicode character composed
;; with, e.g. "VARIATION SELECTOR-16", `string-to-char' ignores the
;; composed modifier/variation-selector and just returns the first
;; character of the string. This should be fine, since it's just
;; for the tooltip.
(concat
(get-char-code-property (string-to-char key) 'name) ": "
(senders-names senders (buffer-local-value 'ement-room buffer))))))
(local-user-p (cl-member (ement-user-id (ement-session-user ement-session)) senders
:key #'ement-user-id :test #'equal)))
(when local-user-p
(add-face-text-property 0 (length string) '(:box (:style pressed-button) :inverse-video t)
nil string))
(ement--remove-face-property string 'button)
string))
(senders-names
(senders room) (cl-loop for sender in senders
collect (ement--user-displayname-in room sender)
into names
finally return (string-join names ", "))))
(cl-loop with keys-senders
for reaction in reactions
for key = (map-nested-elt (ement-event-content reaction) '(m.relates_to key))
for sender = (ement-event-sender reaction)
do (push sender (alist-get key keys-senders nil nil #'string=))
finally do (setf keys-senders (cl-sort keys-senders #'> :key (lambda (pair) (length (cdr pair)))))
finally return (concat "\n " (mapconcat #'format-reaction keys-senders " "))))
""))
(cl-defun ement-room--format-message (event room session &optional (format ement-room-message-format-spec))
"Return EVENT in ROOM on SESSION formatted according to FORMAT.
Format defaults to `ement-room-message-format-spec', which see."
;; Bind this locally so formatters can modify it for this call.
(let ((ement-room--format-message-margin-p)
(left-margin-width ement-room-left-margin-width)
(right-margin-width ement-room-right-margin-width))
;; Copied from `format-spec'.
(with-temp-buffer
;; Pretend this is a room buffer.
(setf ement-session session
ement-room room)
;; HACK: Setting these buffer-locally in a temp buffer is ugly.
(setq-local ement-room-left-margin-width left-margin-width)
(setq-local ement-room-right-margin-width right-margin-width)
(insert format)
(goto-char (point-min))
(while (search-forward "%" nil t)
(cond
((eq (char-after) ?%)
;; Quoted percent sign.
(delete-char 1))
((looking-at "\\([-0-9.]*\\)\\([a-zA-Z]\\)")
;; Valid format spec.
(let* ((num (match-string 1))
(spec (string-to-char (match-string 2)))
(_
;; We delete the specifier now, because the formatter may change the
;; match data, and we already have what we need.
(delete-region (1- (match-beginning 0)) (match-end 0)))
(formatter (or (alist-get spec ement-room-event-formatters)
(error "Invalid format character: `%%%c'" spec)))
(val (or (funcall formatter event room session)
(let ((print-level 1))
(propertize (format "[Event has no value for spec \"?%s\"]" (char-to-string spec))
'face 'font-lock-comment-face
'help-echo (format "%S" event)))))
;; Pad result to desired length.
(text (format (concat "%" num "s") val)))
(insert text)))
(t
;; Signal an error on bogus format strings.
(error "ement-room--format-message: Invalid format string: %S" format))))
;; Propertize margin text.
(when ement-room--format-message-wrap-prefix
(when-let ((wrap-prefix-end (next-single-property-change (point-min) 'wrap-prefix-end)))
(let* ((prefix-width (string-width
(buffer-substring-no-properties (point-min) wrap-prefix-end)))
(prefix (propertize " " 'display `((space :width ,prefix-width)))))
(goto-char wrap-prefix-end)
(delete-char 1)
;; We apply the prefix to the entire event as `wrap-prefix', and to just the
;; body as `line-prefix'.
(put-text-property (point-min) (point-max) 'wrap-prefix prefix)
(put-text-property (point) (point-max) 'line-prefix prefix))))
(when ement-room--format-message-margin-p
(when-let ((left-margin-end (next-single-property-change (point-min) 'left-margin-end)))
(goto-char left-margin-end)
(delete-char 1)
(let ((left-margin-text-width (string-width (buffer-substring-no-properties (point-min) (point)))))
;; It would be preferable to not have to allocate a string to
;; calculate the display width, but I don't know of another way.
(put-text-property (point-min) (point)
'display `((margin left-margin)
,(buffer-substring (point-min) (point))))
(save-excursion
(goto-char (point-min))
;; Insert a string with a display specification that causes it to be displayed in the
;; left margin as a space that displays with the width of the difference between the
;; left margin's width and the display width of the text in the left margin (whew).
;; This is complicated, but it seems to work (minus a possible Emacs/Gtk bug that
;; sometimes causes the space to have a little "junk" displayed in it at times, but
;; that's not our fault). (And this is another example of how well-documented Emacs
;; is: this was only possible by carefully reading the Elisp manual.)
(insert (propertize " " 'display `((margin left-margin)
(space :width (- left-margin ,left-margin-text-width))))))))
(when-let ((right-margin-start (next-single-property-change (point-min) 'right-margin-start)))
(goto-char right-margin-start)
(delete-char 1)
(let ((string (buffer-substring (point) (point-max))))
;; Relocate its text to the beginning so it won't be
;; displayed at the last line of wrapped messages.
(delete-region (point) (point-max))
(goto-char (point-min))
(insert-and-inherit
(propertize " "
'display `((margin right-margin) ,string))))))
(buffer-string))))
(cl-defun ement-room--format-message-body (event &key (formatted-p t))
"Return formatted body of \"m.room.message\" EVENT.
If FORMATTED-P, return the formatted body content, when available."
(pcase-let* (((cl-struct ement-event content
(unsigned (map ('redacted_by unsigned-redacted-by)))
(local (map ('redacted-by local-redacted-by))))
event)
((map ('body main-body) msgtype ('format content-format) ('formatted_body formatted-body)
('m.relates_to (map ('rel_type rel-type)))
('m.new_content (map ('body new-body) ('formatted_body new-formatted-body)
('format new-content-format))))
content)
(body (or new-body main-body))
(formatted-body (or new-formatted-body formatted-body))
(body (if (or (not formatted-p) (not formatted-body))
;; Copy the string so as not to add face properties to the one in the struct.
(copy-sequence body)
(pcase (or new-content-format content-format)
("org.matrix.custom.html"
(save-match-data
(ement-room--render-html formatted-body)))
(_ (format "[unknown body format: %s] %s"
(or new-content-format content-format) body)))))
(appendix (pcase msgtype
;; TODO: Face for m.notices.
((or "m.text" "m.emote" "m.notice") nil)
("m.image" (ement-room--format-m.image event))
("m.file" (ement-room--format-m.file event))
("m.video" (ement-room--format-m.video event))
(_ (if (or local-redacted-by unsigned-redacted-by)
nil
(format "[unsupported msgtype: %s]" msgtype ))))))
(when body
;; HACK: Once I got an error when body was nil, so let's avoid that.
(setf body (ement-room--linkify-urls body)))
;; HACK: Ensure body isn't nil (e.g. redacted messages can have empty bodies).
(unless body
(setf body (copy-sequence
;; Yes, copying this string is necessary here too, otherwise a single
;; string will be used across every call to this function, whose face
;; properties will be added to every time in other functions, which will
;; make a very big mess of face properties if a room's buffer is opened
;; and closed a few times.
(if (or local-redacted-by unsigned-redacted-by)
"[redacted]"
"[message has no body content]"))))
(when appendix
(setf body (concat body " " appendix)))
(when (equal "m.replace" rel-type)
;; Message is an edit.
(setf body (concat body " " (propertize "[edited]" 'face 'font-lock-comment-face))))
body))
(defun ement-room--render-html (string)
"Return rendered version of HTML STRING.
HTML is rendered to Emacs text using `shr-insert-document'."
(with-temp-buffer
(insert string)
(save-excursion
;; NOTE: We workaround `shr`'s not indenting the blockquote properly (it
;; doesn't seem to compensate for the margin). I don't know exactly how
;; `shr-tag-blockquote' and `shr-mark-fill' and `shr-fill-line' and
;; `shr-indentation' work together, but through trial-and-error, this
;; seems to work. It even seems to work properly when a window is
;; resized (i.e. the wrapping is adjusted automatically by redisplay
;; rather than requiring the message to be re-rendered to HTML).
(let ((shr-use-fonts ement-room-shr-use-fonts)
(old-fn (symbol-function 'shr-tag-blockquote))) ;; Bind to a var to avoid unknown-function linting errors.
(cl-letf (((symbol-function 'shr-fill-line) #'ignore)
((symbol-function 'shr-tag-blockquote)
(lambda (dom)
(let ((beg (point-marker)))
(funcall old-fn dom)
(add-text-properties beg (point-max)
'(wrap-prefix " "
line-prefix " "))))))
(shr-insert-document
(libxml-parse-html-region (point-min) (point-max))))))
(string-trim (buffer-substring (point) (point-max)))))
(cl-defun ement-room--event-mentions-user-p (event user &optional (room ement-room))
"Return non-nil if EVENT in ROOM mentions USER."
(pcase-let* (((cl-struct ement-event content) event)
((map body formatted_body) content)
(body (or formatted_body body)))
;; FIXME: `ement--user-displayname-in' may not be returning the right result for the
;; local user, so test the displayname slot too. (But even that may be nil sometimes?
;; Something needs to be fixed...)
;; HACK: So we use the username slot, which was created just for this, for now.
(when body
(cl-macrolet ((matches-body-p
(form) `(when-let ((string ,form))
(string-match-p (regexp-quote string) body))))
(or (matches-body-p (ement-user-username user))
(matches-body-p (ement--user-displayname-in room user))
(matches-body-p (ement-user-id user)))))))
(defun ement-room--linkify-urls (string)
"Return STRING with URLs in it made clickable."
;; Is there an existing Emacs function to do this? I couldn't find one.
;; Yes, maybe: `goto-address-mode'. TODO: Try goto-address-mode.
(with-temp-buffer
(insert string)
(goto-char (point-min))
(cl-loop while (re-search-forward (rx bow "http" (optional "s") "://" (1+ (not space)))
nil 'noerror)
do (make-text-button (match-beginning 0) (match-end 0)
'mouse-face 'highlight
'face 'link
'help-echo (match-string 0)
'action #'browse-url-at-mouse
'follow-link t))
(buffer-string)))
;; NOTE: This function is not useful when displaynames are shown in the margin, because
;; margins are not mouse-interactive in Emacs, therefore the help-echo function is called
;; with the string and the position in the string, which leaves the buffer position
;; unknown. So we have to set the help-echo to a string rather than a function. But the
;; function may be useful in the future, so leaving it commented for now.
;; (defun ement-room--user-help-echo (window _object pos)
;; "Return user ID string for POS in WINDOW.
;; For use as a `help-echo' function on `ement-user' headings."
;; (let ((data (with-selected-window window
;; (ewoc-data (ewoc-locate ement-ewoc pos)))))
;; (cl-typecase data
;; (ement-event (ement-user-id (ement-event-sender data)))
;; (ement-user (ement-user-id data)))))
(defun ement-room--user-color (user)
"Return a color in which to display USER's messages."
(cl-labels ((relative-luminance
;; Copy of `modus-themes-wcag-formula', an elegant
;; implementation by Protesilaos Stavrou. Also see
;; <https://en.wikipedia.org/wiki/Relative_luminance> and
;; <https://www.w3.org/TR/WCAG20/#relativeluminancedef>.
(rgb) (cl-loop for k in '(0.2126 0.7152 0.0722)
for x in rgb
sum (* k (if (<= x 0.03928)
(/ x 12.92)
(expt (/ (+ x 0.055) 1.055) 2.4)))))
(contrast-ratio
;; Copy of `modus-themes-contrast'; see above.
(a b) (let ((ct (/ (+ (relative-luminance a) 0.05)
(+ (relative-luminance b) 0.05))))
(max ct (/ ct))))
(increase-contrast
(color against target toward)
(let ((gradient (cdr (color-gradient color toward 20)))
new-color)
(cl-loop do (setf new-color (pop gradient))
while new-color
until (>= (contrast-ratio new-color against) target)
;; Avoid infinite loop in case of weirdness
;; by returning color as a fallback.
finally return (or new-color color)))))
(let* ((id (ement-user-id user))
(id-hash (float (+ (abs (sxhash id)) ement-room-prism-color-adjustment)))
;; TODO: Wrap-around the value to get the color I want.
(ratio (/ id-hash (float most-positive-fixnum)))
(color-num (round (* (* 255 255 255) ratio)))
(color-rgb (list (/ (float (logand color-num 255)) 255)
(/ (float (lsh (logand color-num 65280) -8)) 255)
(/ (float (lsh (logand color-num 16711680) -16)) 255)))
(background-rgb (color-name-to-rgb (face-background 'default))))
(when (< (contrast-ratio color-rgb background-rgb) ement-room-prism-minimum-contrast)
(setf color-rgb (increase-contrast color-rgb background-rgb ement-room-prism-minimum-contrast
(color-name-to-rgb (face-foreground 'default)))))
(apply #'color-rgb-to-hex (append color-rgb (list 2))))))
;;;;; Compose buffer
;; Compose messages in a separate buffer, like `org-edit-special'.
(defvar-local ement-room-compose-buffer nil
"Non-nil in buffers that are composing a message to a room.")
(cl-defun ement-room-compose-message (room session &key body)
"Compose a message to ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room. With BODY, use it as the initial
message contents."
(interactive
(ement-with-room-and-session
(list ement-room ement-session)))
(let* ((compose-buffer (generate-new-buffer (format "*Ement compose: %s*" (ement--room-display-name ement-room))))
(send-message-filter ement-room-send-message-filter))
(with-current-buffer compose-buffer
(ement-room-init-compose-buffer room session)
(setf ement-room-send-message-filter send-message-filter)
;; TODO: Make mode configurable.
(when body
(insert body))
;; FIXME: Inexplicably, this doesn't do anything, so we comment it out for now.
;; (add-function :override (local 'org-mode)
;; ;; HACK: Since `org-mode' kills buffer-local variables we need, we add
;; ;; buffer-local advice to prevent that from happening in case a user enables it.
;; (lambda (&rest _ignore)
;; (message "Use `ement-room-compose-org' to activate Org in this buffer")))
;; NOTE: Surprisingly, we don't run this hook in `ement-room-init-compose-buffer',
;; because if a function in that hook calls the init function (like
;; `ement-room-compose-org' does), it makes `run-hooks' recursive. As long as this
;; is the only function that makes the compose buffer, and as long as none of the
;; hooks do anything that activating `org-mode' nullifies, this should be okay...
(run-hooks 'ement-room-compose-hook))
(pop-to-buffer compose-buffer)))
(defun ement-room-compose-from-minibuffer ()
"Edit the current message in a compose buffer.
To be called from a minibuffer opened from
`ement-room-read-string'."
(interactive)
(cl-assert (minibufferp)) (cl-assert ement-room) (cl-assert ement-session)
;; TODO: When requiring Emacs 27, use `letrec'.
;; HACK: I can't seem to find a better way to do this, to exit the minibuffer without exiting this command too.
(let* ((body (minibuffer-contents))
(compose-fn-symbol (gensym (format "ement-compose-%s" (or (ement-room-canonical-alias ement-room)
(ement-room-id ement-room)))))
(input-method current-input-method) ; Capture this value from the minibuffer.
(send-message-filter ement-room-send-message-filter)
(replying-to-event ement-room-replying-to-event)
(compose-fn (lambda ()
;; HACK: Since exiting the minibuffer restores the previous window configuration,
;; we have to do some magic to get the new compose buffer to appear.
;; TODO: Use letrec with Emacs 27.
(remove-hook 'minibuffer-exit-hook compose-fn-symbol)
;; FIXME: Probably need to unintern the symbol.
(ement-room-compose-message ement-room ement-session :body body)
;; FIXME: This doesn't propagate the send-message-filter to the minibuffer.
(setf ement-room-send-message-filter send-message-filter)
(setq-local ement-room-replying-to-event replying-to-event)
(when replying-to-event
(setq-local header-line-format
(concat header-line-format
(format " (Replying to message from %s)"
(ement--user-displayname-in
ement-room (ement-event-sender replying-to-event))))))
(let* ((compose-buffer (current-buffer))
(show-buffer-fn-symbol (gensym "ement-show-compose-buffer"))
(show-buffer-fn (lambda ()
(remove-hook 'window-configuration-change-hook show-buffer-fn-symbol)
;; FIXME: Probably need to unintern the symbol.
(pop-to-buffer compose-buffer)
(set-input-method input-method))))
(fset show-buffer-fn-symbol show-buffer-fn)
(add-hook 'window-configuration-change-hook show-buffer-fn-symbol)))))
(fset compose-fn-symbol compose-fn)
(add-hook 'minibuffer-exit-hook compose-fn-symbol)
;; Deactivate minibuffer's input method, otherwise subsequent
;; minibuffers will have it, too.
(deactivate-input-method)
(abort-recursive-edit)))
(defun ement-room-compose-send ()
"Prompt to send the current compose buffer's contents.
To be called from an `ement-room-compose' buffer."
(interactive)
(cl-assert ement-room-compose-buffer)
(cl-assert ement-room) (cl-assert ement-session)
;; Putting it in the kill ring seems like the best thing to do, to ensure
;; it doesn't get lost if the user exits the minibuffer before sending.
(kill-new (string-trim (buffer-string)))
(let ((room ement-room)
(session ement-session)
(input-method current-input-method)
(send-message-filter ement-room-send-message-filter)
(replying-to-event ement-room-replying-to-event))
(quit-restore-window nil 'kill)
(ement-view-room room session)
(let* ((prompt (format "Send message (%s): " (ement-room-display-name ement-room)))
(current-input-method input-method) ; Bind around read-string call.
(ement-room-send-message-filter send-message-filter)
(pos (when replying-to-event
(ewoc-location (ement-room--ewoc-last-matching ement-ewoc
(lambda (data)
(eq data replying-to-event))))))
(body (if replying-to-event
(ement-room-with-highlighted-event-at pos
(ement-room-read-string prompt (car kill-ring) nil nil 'inherit-input-method))
(ement-room-read-string prompt (car kill-ring) nil nil 'inherit-input-method)) ))
(ement-room-send-message ement-room ement-session :body body :replying-to-event replying-to-event))))
(defun ement-room-init-compose-buffer (room session)
"Eval BODY, setting up the current buffer as a compose buffer.
Sets ROOM and SESSION buffer-locally, binds `save-buffer' in
a copy of the local keymap, and sets `header-line-format'."
;; Using a macro for this seems awkward but necessary.
(setq-local ement-room room)
(setq-local ement-session session)
(setf ement-room-compose-buffer t)
(setq-local completion-at-point-functions
(append '(ement-room--complete-members-at-point ement-room--complete-rooms-at-point)
completion-at-point-functions))
;; FIXME: Compose with local map?
(use-local-map (if (current-local-map)
(copy-keymap (current-local-map))
(make-sparse-keymap)))
(local-set-key [remap save-buffer] #'ement-room-compose-send)
(setq header-line-format (substitute-command-keys
(format " Press \\[save-buffer] to send message to room (%s)"
(ement-room-display-name room)))))
;;;;; Widgets
(require 'widget)
(define-widget 'ement-room-membership 'item
"Widget for membership events."
;; FIXME: This makes it hard to add a timestamp according to the buffer's message format spec.
;; FIXME: The widget value inserts an extra space before the wrap prefix. There seems
;; to be no way to fix this while still using a widget for this, so maybe we shouldn't
;; use a widget after all. But it might be good to keep digging for a solution so that
;; widgets could be used for other things later...
:format "%{ %v %}"
:sample-face 'ement-room-membership
:value-create (lambda (widget)
(pcase-let* ((event (widget-value widget)))
(insert (ement-room-wrap-prefix
(ement-room--format-member-event event ement-room))))))
(defun ement-room--format-member-event (event room)
"Return formatted string for \"m.room.member\" EVENT in ROOM."
;; SPEC: Section 9.3.4: "m.room.member".
(pcase-let* (((cl-struct ement-event sender state-key
(content (map reason ('avatar_url new-avatar-url)
('membership new-membership) ('displayname new-displayname)))
(unsigned (map ('prev_content (map ('avatar_url old-avatar-url)
('membership prev-membership)
('displayname prev-displayname))))))
event)
(sender-name (ement--user-displayname-in ement-room sender)))
(cl-macrolet ((nes (var)
;; For "non-empty-string". Needed because the displayname can be
;; an empty string, but apparently is never null. (Note that the
;; argument should be a variable, never any other form, to avoid
;; multiple evaluation.)
`(when (and ,var (not (string-empty-p ,var)))
,var))
(sender-name-id-string
() `(propertize sender-name
'help-echo (ement-user-id sender)))
(new-displayname-sender-name-state-key-string
() `(propertize (or (nes new-displayname) (nes sender-name) (nes state-key))
'help-echo state-key))
(sender-name-state-key-string
() `(propertize sender-name
'help-echo state-key))
(prev-displayname-id-string
() `(propertize (or prev-displayname sender-name)
'help-echo (ement-user-id sender))))
(pcase-exhaustive new-membership
("invite"
(pcase prev-membership
((or "leave" '())
(format "%s invited %s"
(sender-name-id-string)
(new-displayname-sender-name-state-key-string)))
(_ (format "%s sent unrecognized invite event for %s"
(sender-name-id-string)
(new-displayname-sender-name-state-key-string)))))
("join"
(pcase prev-membership
("invite"
(format "%s accepted invitation to join"
(sender-name-state-key-string)))
("join"
(cond ((not (equal new-displayname prev-displayname))
(propertize (format "%s changed name to %s"
prev-displayname (or new-displayname (ement--user-displayname-in room sender)))
'help-echo state-key))
((not (equal new-avatar-url old-avatar-url))
(format "%s changed avatar"
(new-displayname-sender-name-state-key-string)))
(t (format "Unrecognized membership event for %s"
(sender-name-state-key-string)))))
("leave"
(format "%s rejoined"
(sender-name-state-key-string)))
(`nil
(format "%s joined"
(new-displayname-sender-name-state-key-string)))
(_ (format "%s sent unrecognized join event for %s"
(sender-name-id-string)
(new-displayname-sender-name-state-key-string)))))
("leave"
(pcase prev-membership
("invite"
(pcase state-key
((pred (equal (ement-user-id sender)))
(format "%s rejected invitation"
(sender-name-id-string)))
(_ (format "%s revoked %s's invitation"
(sender-name-id-string)
(new-displayname-sender-name-state-key-string)))))
("join"
(pcase state-key
((pred (equal (ement-user-id sender)))
(format "%s left%s"
(prev-displayname-id-string)
(if reason
(format " (%s)" reason)
"")))
(_ (format "%s kicked %s%s"
(sender-name-id-string)
(propertize (or prev-displayname state-key)
'help-echo state-key)
(if reason
(format " (%s)" reason)
"")))))
("ban"
(format "%s unbanned %s"
(sender-name-id-string)
state-key))
(_ (format "%s left%s"
(prev-displayname-id-string)
(if reason
(format " (%s)" reason)
"")))))
("ban"
(pcase prev-membership
((or "invite" "leave")
(format "%s banned %s%s"
(sender-name-id-string)
(propertize (or prev-displayname state-key)
'help-echo state-key)
(if reason
(format " (%s)" reason)
"")))
("join"
(format "%s kicked and banned %s%s"
(sender-name-id-string)
(propertize (or prev-displayname state-key)
'help-echo state-key)
(if reason
(format " (%s)" reason)
"")))
(_ (format "%s sent unrecognized ban event for %s"
(sender-name-id-string)
(propertize (or prev-displayname state-key)
'help-echo state-key)))))))))
;; NOTE: Widgets are only currently used for single membership events, not grouped ones.
(defun ement-room--format-membership-events (struct room)
"Return string for STRUCT in ROOM.
STRUCT should be an `ement-room-membership-events' struct."
(cl-labels ((event-user
(event) (propertize (if-let (user (gethash (ement-event-state-key event) ement-users))
(ement--user-displayname-in room user)
(ement-event-state-key event))
'help-echo (concat (ement-room--format-member-event event room)
" <" (ement-event-state-key event) ">")))
(old-membership (event) (map-nested-elt (ement-event-unsigned event) '(prev_content membership)))
(new-membership (event) (alist-get 'membership (ement-event-content event))))
(pcase-let* (((cl-struct ement-room-membership-events events) struct))
(pcase (length events)
(0 (warn "No events in `ement-room-membership-events' struct"))
(1 (ement-room--format-member-event (car events) room))
(_ (let* ((left-events (cl-remove-if-not (lambda (event)
(and (equal "leave" (new-membership event))
(not (member (old-membership event) '("ban" "invite")))))
events))
(join-events (cl-remove-if-not (lambda (event)
(and (equal "join" (new-membership event))
(not (equal "join" (old-membership event)))))
events))
(rejoin-events (cl-remove-if-not (lambda (event)
(and (equal "join" (new-membership event))
(equal "leave" (old-membership event))))
events))
(invite-events (cl-remove-if-not (lambda (event)
(equal "invite" (new-membership event)))
events))
(reject-events (cl-remove-if-not (lambda (event)
(and (equal "invite" (old-membership event))
(equal "leave" (new-membership event))))
events))
(ban-events (cl-remove-if-not (lambda (event)
(and (member (old-membership event) '("invite" "leave"))
(equal "ban" (new-membership event))))
events))
(unban-events (cl-remove-if-not (lambda (event)
(and (equal "ban" (old-membership event))
(equal "leave" (new-membership event))))
events))
(kick-and-ban-events (cl-remove-if-not (lambda (event)
(and (equal "join" (old-membership event))
(equal "ban" (new-membership event))))
events))
(rename-events (cl-remove-if-not (lambda (event)
(and (equal "join" (old-membership event))
(equal "join" (new-membership event))
(equal (alist-get 'avatar_url (ement-event-content event))
(map-nested-elt (ement-event-unsigned event)
'(prev_content avatar_url)))))
events))
(avatar-events (cl-remove-if-not (lambda (event)
(and (equal "join" (old-membership event))
(equal "join" (new-membership event))
(not (equal (alist-get 'avatar_url (ement-event-content event))
(map-nested-elt (ement-event-unsigned event)
'(prev_content avatar_url))))))
events))
join-and-leave-events rejoin-and-leave-events)
;; Remove apparent duplicates between join/rejoin events.
(setf join-events (cl-delete-if (lambda (event)
(cl-find (ement-event-state-key event) rejoin-events
:test #'equal :key #'ement-event-state-key))
join-events)
rejoin-events (cl-delete-if (lambda (event)
(cl-find (ement-event-state-key event) join-events
:test #'equal :key #'ement-event-state-key))
rejoin-events)
join-and-leave-events (cl-loop for join-event in join-events
for left-event = (cl-find (ement-event-state-key join-event) left-events
:test #'equal :key #'ement-event-state-key)
when left-event
collect left-event
and do (setf join-events (cl-delete (ement-event-state-key join-event) join-events
:test #'equal :key #'ement-event-state-key)
left-events (cl-delete (ement-event-state-key left-event) left-events
:test #'equal :key #'ement-event-state-key)))
rejoin-and-leave-events (cl-loop for rejoin-event in rejoin-events
for left-event = (cl-find (ement-event-state-key rejoin-event) left-events
:test #'equal :key #'ement-event-state-key)
when left-event
collect left-event
and do (setf rejoin-events (cl-delete
(ement-event-state-key rejoin-event) rejoin-events
:test #'equal :key #'ement-event-state-key)
left-events (cl-delete (ement-event-state-key left-event) left-events
:test #'equal :key #'ement-event-state-key))))
(format "Membership: %s."
(string-join (cl-loop for (type . events)
in (ement-alist "rejoined" rejoin-events
"joined" join-events
"left" left-events
"joined and left" join-and-leave-events
"rejoined and left" rejoin-and-leave-events
"invited" invite-events
"rejected invitation" reject-events
"banned" ban-events
"unbanned" unban-events
"kicked and banned" kick-and-ban-events
"changed name" rename-events
"changed avatar" avatar-events)
for users = (mapcar #'event-user
(cl-delete-duplicates
events :key #'ement-event-state-key))
for number = (length users)
when events
collect (format "%s %s (%s)" number
(propertize type 'face 'bold)
(string-join users ", ")))
"; "))))))))
;;;;; Images
;; Downloading and displaying images in messages, room/user avatars, etc.
(require 'image)
(defvar ement-room-image-keymap
(let ((map (make-sparse-keymap)))
(set-keymap-parent map image-map)
;; TODO: Make RET work for showing images too.
;; (define-key map (kbd "RET") #'ement-room-image-show)
(define-key map [mouse-1] #'ement-room-image-scale-mouse)
(define-key map [double-mouse-1] #'ement-room-image-show)
map)
"Keymap for images in room buffers.")
(defgroup ement-room-images nil
"Showing images in rooms."
:group 'ement-room)
(defcustom ement-room-images t
"Download and show images in messages, avatars, etc."
:type 'boolean
:set (lambda (option value)
(if (or (fboundp 'imagemagick-types)
(when (fboundp 'image-transforms-p)
(image-transforms-p)))
(set-default option value)
(set-default option nil)
(when (and value (display-images-p))
(display-warning 'ement "This Emacs was not built with ImageMagick support, nor does it support Cairo/XRender scaling, so images can't be displayed in Ement")))))
(defcustom ement-room-image-initial-height 0.2
"Limit images' initial display height.
If a number, it should be no larger than 1 (because Emacs can't
display images larger than the window body height)."
:type '(choice (const :tag "Use full window width" nil)
(number :tag "Limit to this multiple of the window body height")))
(defun ement-room-image-scale-mouse (event)
"Toggle scale of image at mouse EVENT.
Scale image to fit within the window's body. If image is already
fit to the window, reduce its max-height to 10% of the window's
height."
(interactive "e")
(pcase-let* ((`(,_type ,position ,_count) event)
(window (posn-window position))
(pos (event-start position)))
(with-selected-window window
(pcase-let* ((image (get-text-property pos 'display))
(window-width (window-body-width nil t))
(window-height (window-body-height nil t))
;; Image scaling commands set :max-height and friends to nil so use the
;; impossible dummy value -1. See <https://github.com/alphapapa/ement.el/issues/39>.
(new-height (if (= window-height (or (image-property image :max-height) -1))
(/ window-height 10)
window-height)))
(when (fboundp 'imagemagick-types)
;; Only do this when ImageMagick is supported.
;; FIXME: When requiring Emacs 27+, remove this (I guess?).
(setf (image-property image :type) 'imagemagick))
;; Set :scale to nil since image scaling commands might have changed it.
(setf (image-property image :scale) nil
(image-property image :max-width) window-width
(image-property image :max-height) new-height)))))
(defun ement-room-image-show (event)
"Show image at mouse EVENT in a new buffer."
(interactive "e")
(pcase-let* ((`(,_type ,position ,_count) event)
(window (posn-window position)))
(with-current-buffer (window-buffer window)
(pcase-let* ((pos (event-start position))
(image (copy-sequence (get-text-property pos 'display)))
(ement-event (ewoc-data (ewoc-locate ement-ewoc pos)))
((cl-struct ement-event id) ement-event)
(buffer-name (format "*Ement image: %s*" id))
(new-buffer (get-buffer-create buffer-name)))
(when (fboundp 'imagemagick-types)
;; Only do this when ImageMagick is supported.
;; FIXME: When requiring Emacs 27+, remove this (I guess?).
(setf (image-property image :type) 'imagemagick))
(setf (image-property image :scale) 1.0
(image-property image :max-width) nil
(image-property image :max-height) nil)
(with-current-buffer new-buffer
(erase-buffer)
(insert-image image))
(pop-to-buffer new-buffer '((display-buffer-pop-up-frame)))
(set-frame-parameter nil 'fullscreen 'maximized)))))
(defun ement-room--format-m.image (event)
"Return \"m.image\" EVENT formatted as a string.
When `ement-room-images' is non-nil, also download it and then
show it in the buffer."
(pcase-let* (((cl-struct ement-event content (local event-local)) event)
;; HACK: Get the room's buffer from the variable (the current buffer
;; will be a temp formatting buffer when this is called, but it still
;; inherits the `ement-room' variable from the room buffer, thankfully).
((cl-struct ement-room local) ement-room)
((map buffer) local)
;; TODO: Thumbnail support.
((map ('url mxc) info ;; ('thumbnail_url thumbnail-url)
) content)
((map thumbnail_info) info)
((map ('h _thumbnail-height) ('w _thumbnail-width)) thumbnail_info)
((map image) event-local)
(url (when mxc
(ement--mxc-to-url mxc ement-session)))
;; (thumbnail-url (ement--mxc-to-url thumbnail-url ement-session))
)
(if (and ement-room-images image)
;; Images enabled and image downloaded: create image and
;; return it in a string.
(condition-case err
(let ((image (create-image image nil 'data-p :ascent 'center))
(buffer-window (when buffer
(get-buffer-window buffer)))
max-height max-width)
;; Calculate max image display size.
(cond (ement-room-image-initial-height
;; Use configured value.
(setf max-height (truncate
;; Emacs doesn't like floats as the max-height.
(* (window-body-height buffer-window t)
ement-room-image-initial-height))
max-width (window-body-width buffer-window t)))
(buffer-window
;; Buffer displayed: use window size.
(setf max-height (window-body-height buffer-window t)
max-width (window-body-width buffer-window t)))
(t
;; Buffer not displayed: use frame size.
(setf max-height (frame-pixel-height)
max-width (frame-pixel-width))))
(when (fboundp 'imagemagick-types)
;; Only do this when ImageMagick is supported.
;; FIXME: When requiring Emacs 27+, remove this (I guess?).
(setf (image-property image :type) 'imagemagick))
(setf (image-property image :max-width) max-width
(image-property image :max-height) max-height
(image-property image :relief) 2
(image-property image :margin) 5
(image-property image :pointer) 'hand)
(concat "\n"
(ement-room-wrap-prefix " "
'display image
'keymap ement-room-image-keymap)))
(error (format "\n [error inserting image: %s]" (error-message-string err))))
;; Image not downloaded: insert URL as button, and download if enabled.
(prog1
(ement-room-wrap-prefix "[image]"
'action #'browse-url
'button t
'button-data url
'category t
'face 'button
'follow-link t
'help-echo url
'keymap button-map
'mouse-face 'highlight)
(when (and ement-room-images url)
;; Images enabled and URL present: download it.
(plz-run
(plz-queue ement-images-queue
'get url :as 'binary
:then (apply-partially #'ement-room--m.image-callback event ement-room)
:noquery t)))))))
(defun ement-room--m.image-callback (event room data)
"Add downloaded image from DATA to EVENT in ROOM.
Then invalidate EVENT's node to show the image."
(pcase-let* (((cl-struct ement-room (local (map buffer))) room))
(setf (map-elt (ement-event-local event) 'image) data)
(when (buffer-live-p buffer)
(with-current-buffer buffer
(if-let (node (ement-room--ewoc-last-matching ement-ewoc
(lambda (node-data)
(eq node-data event))))
(ewoc-invalidate ement-ewoc node)
;; This shouldn't happen, but very rarely, it can. I haven't figured out why
;; yet, so checking whether a node is found rather than blindly calling
;; `ewoc-invalidate' prevents an error from aborting event processing.
(display-warning 'ement-room--m.image-callback
(format "Event %S not found in room %S (a very rare, as-yet unexplained bug, which can be safely ignored; you may disconnect and reconnect if you wish, but it isn't strictly necessary)"
(ement-event-id event)
(ement-room-display-name room))))))))
(defun ement-room--format-m.file (event)
"Return \"m.file\" EVENT formatted as a string."
;; TODO: Insert thumbnail images when enabled.
(pcase-let* (((cl-struct ement-event
(content (map filename
('info (map mimetype size))
('url mxc-url))))
event)
(url (when mxc-url
(ement--mxc-to-url mxc-url ement-session)))
(human-size (file-size-human-readable size))
(string (format "[file: %s (%s) (%s)]" filename mimetype human-size)))
(concat (propertize string
'action #'browse-url
'button t
'button-data url
'category t
'face 'button
'follow-link t
'help-echo url
'keymap button-map
'mouse-face 'highlight)
(propertize " "
'display '(space :relative-height 1.5)))))
(defun ement-room--format-m.video (event)
"Return \"m.video\" EVENT formatted as a string."
;; TODO: Insert thumbnail images when enabled.
(pcase-let* (((cl-struct ement-event
(content (map body
('info (map mimetype size w h))
('url mxc-url))))
event)
(url (when mxc-url
(ement--mxc-to-url mxc-url ement-session)))
(human-size (file-size-human-readable size))
(string (format "[video: %s (%s) (%sx%s) (%s)]" body mimetype w h human-size)))
(concat (propertize string
'action #'browse-url
'button t
'button-data url
'category t
'face 'button
'follow-link t
'help-echo url
'keymap button-map
'mouse-face 'highlight)
(propertize " "
'display '(space :relative-height 1.5)))))
;;;;; Org format sending
;; Some of these declarations may need updating as Org changes.
(defvar org-export-with-toc)
(defvar org-export-with-broken-links)
(defvar org-export-with-section-numbers)
(defvar org-html-inline-images)
(declare-function org-element-property "org-element")
(declare-function org-export-data "ox")
(declare-function org-export-get-caption "ox")
(declare-function org-export-get-ordinal "ox")
(declare-function org-export-get-reference "ox")
(declare-function org-export-read-attribute "ox")
(declare-function org-html--has-caption-p "ox-html")
(declare-function org-html--textarea-block "ox-html")
(declare-function org-html--translate "ox-html")
(declare-function org-html-export-as-html "ox-html")
(declare-function org-html-format-code "ox-html")
(defun ement-room-compose-org ()
"Activate `org-mode' in current compose buffer.
Configures the buffer appropriately so that saving it will export
the Org buffer's contents."
(interactive)
(unless ement-room-compose-buffer
(user-error "This command should be run in a compose buffer. Use `ement-room-compose-message' first"))
;; Calling `org-mode' seems to wipe out local variables.
(let ((room ement-room)
(session ement-session))
(org-mode)
(ement-room-init-compose-buffer room session))
(setq-local ement-room-send-message-filter #'ement-room-send-org-filter))
(defun ement-room-send-org-filter (content room)
"Return event CONTENT for ROOM having processed its Org content.
The CONTENT's body is exported with
`org-html-export-as-html' (with some adjustments for
compatibility), and the result is added to the CONTENT as
\"formatted_body\"."
(require 'ox-html)
;; The CONTENT alist has string keys before being sent.
(pcase-let* ((body (alist-get "body" content nil nil #'equal))
(formatted-body
(save-window-excursion
(with-temp-buffer
(insert (ement--format-body-mentions body room
:template "[[https://matrix.to/#/%s][%s]]"))
(cl-letf (((symbol-function 'org-html-src-block)
(symbol-function 'ement-room--org-html-src-block)))
(let ((org-export-with-toc nil)
(org-export-with-broken-links t)
(org-export-with-section-numbers nil)
(org-html-inline-images nil))
(org-html-export-as-html nil nil nil 'body-only)))
(with-current-buffer "*Org HTML Export*"
(prog1 (string-trim (buffer-string))
(kill-buffer)))))))
(setf (alist-get "formatted_body" content nil nil #'equal) formatted-body
(alist-get "format" content nil nil #'equal) "org.matrix.custom.html")
content))
(defun ement-room--org-html-src-block (src-block _contents info)
"Transcode a SRC-BLOCK element from Org to HTML.
CONTENTS holds the contents of the item. INFO is a plist holding
contextual information.
This is a copy of `org-html-src-block' that uses Riot
Web-compatible HTML output, using HTML like:
<pre><code class=\"language-python\">..."
(if (org-export-read-attribute :attr_html src-block :textarea)
(org-html--textarea-block src-block)
(let ((lang (pcase (org-element-property :language src-block)
;; Riot's syntax coloring doesn't support "elisp", but "lisp" works.
("elisp" "lisp")
(else else)))
(code (org-html-format-code src-block info))
(label (let ((lbl (and (org-element-property :name src-block)
(org-export-get-reference src-block info))))
(if lbl (format " id=\"%s\"" lbl) ""))))
(if (not lang) (format "<pre class=\"example\"%s>\n%s</pre>" label code)
(format "<div class=\"org-src-container\">\n%s%s\n</div>"
;; Build caption.
(let ((caption (org-export-get-caption src-block)))
(if (not caption) ""
(let ((listing-number
(format
"<span class=\"listing-number\">%s </span>"
(format
(org-html--translate "Listing %d:" info)
(org-export-get-ordinal
src-block info nil #'org-html--has-caption-p)))))
(format "<label class=\"org-src-name\">%s%s</label>"
listing-number
(string-trim (org-export-data caption info))))))
;; Contents.
(format "<pre><code class=\"src language-%s\"%s>%s</code></pre>"
lang label code))))))
;;;;; Completion
;; Completing member and room names.
(defun ement-room--complete-members-at-point ()
"Complete member names and IDs at point.
Uses members in the current buffer's room. For use in
`completion-at-point-functions'."
(let ((beg (save-excursion
(when (re-search-backward (rx (or bol bos blank)) nil t)
(if (minibufferp)
(1+ (point))
(point)))))
(end (point))
(collection-fn (completion-table-dynamic
;; The manual seems to show the FUN ignoring any
;; arguments, but the `completion-table-dynamic' docstring
;; seems to say that it should use the argument.
(lambda (_ignore)
(ement-room--member-names-and-ids)))))
(when beg
(list beg end collection-fn :exclusive 'no))))
(defun ement-room--complete-rooms-at-point ()
"Complete room aliases and IDs at point.
For use in `completion-at-point-functions'."
(let ((beg (save-excursion
(when (re-search-backward (rx (or bol bos blank) (or "!" "#")) nil t)
(if (minibufferp)
(1+ (point))
(point)))))
(end (point))
(collection-fn (completion-table-dynamic
;; The manual seems to show the FUN ignoring any
;; arguments, but the `completion-table-dynamic' docstring
;; seems to say that it should use the argument.
(lambda (_ignore)
(ement-room--room-aliases-and-ids)))))
(when beg
(list beg end collection-fn :exclusive 'no))))
;; TODO: Use `cl-pushnew' in these two functions instead of `delete-dups'.
(defun ement-room--member-names-and-ids ()
"Return a list of member names and IDs seen in current room.
If room's `members' table is filled, use it; otherwise, fetch
members list and return already-seen members instead. For use in
`completion-at-point-functions'."
;; For now, we just collect a list of members from events we've seen.
;; TODO: In the future, we may maintain a per-room table of members, which
;; would be more suitable for completing names according to the spec.
(pcase-let* ((room (if (minibufferp)
(buffer-local-value
'ement-room (window-buffer (minibuffer-selected-window)))
ement-room))
(session (if (minibufferp)
(buffer-local-value
'ement-session (window-buffer (minibuffer-selected-window)))
ement-session))
((cl-struct ement-room members) room)
(members (if (alist-get 'fetched-members-p (ement-room-local room))
(hash-table-values members)
;; HACK: Members table empty: update list and use known events
;; for now.
(ement-singly (alist-get 'getting-members-p (ement-room-local room))
(ement--get-joined-members room session
:then (lambda (_) (setf (alist-get 'getting-members-p (ement-room-local room)) nil))
:else (lambda (_) (setf (alist-get 'getting-members-p (ement-room-local room)) nil))))
(mapcar #'ement-event-sender
(ement-room-timeline ement-room)))))
(delete-dups
(cl-loop for member in members
collect (ement-user-id member)
collect (ement--user-displayname-in room member)))))
(defun ement-room--room-aliases-and-ids ()
"Return a list of room names and aliases seen in current session.
For use in `completion-at-point-functions'."
(let* ((session (if (minibufferp)
(buffer-local-value
'ement-session (window-buffer (minibuffer-selected-window)))
ement-session)))
(delete-dups
(delq nil (cl-loop for room in (ement-session-rooms session)
collect (ement-room-id room)
collect (ement-room-canonical-alias room))))))
;;;;; Transient
(require 'transient)
(transient-define-prefix ement-room-transient ()
"Transient for Ement Room buffers."
[:pad-keys t
["Movement"
("TAB" "Next event" ement-room-goto-next)
("<backtab>" "Previous event" ement-room-goto-prev)
("SPC" "Scroll up and mark read" ement-room-scroll-up-mark-read)
("S-SPC" "Scroll down" ement-room-scroll-down-command)
("M-SPC" "Jump to fully-read marker" ement-room-goto-fully-read-marker)]
["Switching"
("M-g M-l" "List rooms" ement-room-list)
("M-g M-r" "Switch to other room" ement-view-room)
("M-g M-m" "Switch to mentions buffer" ement-notify-switch-to-mentions-buffer)
("M-g M-n" "Switch to notifications buffer" ement-notify-switch-to-notifications-buffer)
("q" "Quit window" quit-window)]]
[:pad-keys t
["Messages"
("c" "Composition format" ement-room-set-composition-format
:description (lambda ()
(concat "Composition format: "
(propertize (car (cl-rassoc ement-room-send-message-filter
(list (cons "Plain-text" nil)
(cons "Org-mode" 'ement-room-send-org-filter))
:test #'equal))
'face 'transient-value))))
("RET" "Write message" ement-room-send-message)
("S-RET" "Write reply" ement-room-write-reply)
("M-RET" "Compose message in buffer" ement-room-compose-message)
("<insert>" "Edit message" ement-room-edit-message)
("C-k" "Delete message" ement-room-delete-message)
("s r" "Send reaction" ement-room-send-reaction)
("s e" "Send emote" ement-room-send-emote)
("s f" "Send file" ement-room-send-file)
("s i" "Send image" ement-room-send-image)]
["Users"
("u RET" "Send direct message" ement-send-direct-message)
("u i" "Invite user" ement-invite-user)
("u I" "Ignore user" ement-ignore-user)]]
[:pad-keys t
["Room"
("M-s o" "Occur search in room" ement-room-occur)
("r d" "Describe room" ement-describe-room)
("r m" "List members" ement-list-members)
("r t" "Set topic" ement-room-set-topic)
("r f" "Set message format" ement-room-set-message-format)
("r N" "Override name" ement-room-override-name
:description (lambda ()
(format "Name override: %s"
(if-let* ((event (alist-get "org.matrix.msc3015.m.room.name.override"
(ement-room-account-data ement-room) nil nil #'equal))
(name (map-nested-elt event '(content name))))
(propertize name 'face 'transient-value)
(propertize "none" 'face 'transient-inactive-value)))))
("r n" "Set notification state" ement-room-set-notification-state
:description (lambda ()
(let ((state (ement-room-notification-state ement-room ement-session)))
(format "Notifications (%s|%s|%s|%s|%s)"
(propertize "default"
'face (pcase state
(`nil 'transient-value)
(_ 'transient-inactive-value)))
(propertize "all-loud"
'face (pcase state
('all-loud 'transient-value)
(_ 'transient-inactive-value)))
(propertize "all"
'face (pcase state
('all 'transient-value)
(_ 'transient-inactive-value)))
(propertize "mentions"
'face (pcase state
('mentions-and-keywords 'transient-value)
(_ 'transient-inactive-value)))
(propertize "none"
'face (pcase state
('none 'transient-value)
(_ 'transient-inactive-value)))))))
("r T" "Tag/untag room" ement-tag-room
:description (lambda ()
(format "Tag/untag room (%s|%s)"
(propertize "Fav"
'face (if (ement--room-tagged-p "m.favourite" ement-room)
'transient-value 'transient-inactive-value))
(propertize "Low-prio"
'face (if (ement--room-tagged-p "m.lowpriority" ement-room)
'transient-value 'transient-inactive-value)))))]
["Room membership"
("R c" "Create room" ement-create-room)
("R j" "Join room" ement-join-room)
("R l" "Leave room" ement-leave-room)
("R F" "Forget room" ement-forget-room)
("R n" "Set nick" ement-room-set-display-name
:description (lambda ()
(format "Set nick (%s)"
(propertize (ement--user-displayname-in
ement-room (gethash (ement-user-id (ement-session-user ement-session))
ement-users))
'face 'transient-value))))
("R s" "Toggle spaces" ement-room-toggle-space
:description (lambda ()
(format "Toggle spaces (%s)"
(if-let ((spaces (ement--room-spaces ement-room ement-session)))
(string-join
(mapcar (lambda (space)
(propertize (ement-room-display-name space)
'face 'transient-value))
spaces)
", ")
(propertize "none" 'face 'transient-inactive-value)))))]]
["Other"
("v" "View event" ement-room-view-event)
("g" "Sync new messages" ement-room-sync
:if (lambda ()
(interactive)
(or (not ement-auto-sync)
(not (map-elt ement-syncs ement-session)))))])
;;;; Footer
(provide 'ement-room)
;;; ement-room.el ends here
;;; ement-structs.el --- Ement structs -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
;;;; Debugging
;; NOTE: Uncomment this form and `emacs-lisp-byte-compile-and-load' the file to enable
;; `ement-debug' messages. This is commented out by default because, even though the
;; messages are only displayed when `warning-minimum-log-level' is `:debug' at runtime, if
;; that is so at expansion time, the expanded macro calls format the message and check the
;; log level at runtime, which is not zero-cost.
;; (eval-and-compile
;; (setq-local warning-minimum-log-level nil)
;; (setq-local warning-minimum-log-level :debug))
;;;; Requirements
(require 'cl-lib)
;;;; Structs
(cl-defstruct ement-user
id displayname account-data
(color nil :documentation "Color in which to display user's name.")
(message-color nil :documentation "Color in which to display user's messages.")
(username nil
;; NOTE: Not exactly according to spec, I guess, but useful for now.
:documentation "Username part of user's Matrix ID.")
(avatar-url nil :documentation "MXC URL to user's avatar.")
(avatar nil :documentation "One-space string with avatar image in display property."))
(cl-defstruct ement-event
id sender content origin-server-ts type unsigned state-key
receipts
;; The local slot is an alist used by the local client only.
local)
(cl-defstruct ement-server
name uri-prefix)
(cl-defstruct ement-session
user server token transaction-id rooms next-batch
device-id initial-device-display-name has-synced-p
account-data
;; Hash table of all seen events, keyed on event ID.
events)
(cl-defstruct ement-room
id display-name prev-batch
summary state timeline ephemeral account-data unread-notifications
latest-ts topic canonical-alias avatar status type invite-state
(members (make-hash-table :test #'equal) :documentation "Hash table mapping joined user IDs to user structs.")
;; The local slot is an alist used by the local client only.
local
(receipts (make-hash-table :test #'equal))
(displaynames (make-hash-table) :documentation "Hash table mapping users to their displayname in this room."))
;;;; Variables
;;;; Customization
;;;; Commands
;;;; Functions
;;;; Footer
(provide 'ement-structs)
;;; ement-structs.el ends here
;;; ement-tabulated-room-list.el --- Ement tabulated room list buffer -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library implements a room list buffer with `tabulated-list-mode'.
;; NOTE: It doesn't appear that there is a way to get the number of
;; members in a room other than by retrieving the list of members and
;; counting them. For a large room (e.g. the Spacemacs Gitter room or
;; #debian:matrix.org), that means thousands of users, none of the
;; details of which we care about. So it seems impractical to know
;; the number of members when using lazy-loading. So I guess we just
;; won't show the number of members.
;; TODO: (Or maybe there is, see m.joined_member_count).
;; NOTE: The tabulated-list API is awkward here. When the
;; `tabulated-list-format' is changed, we have to make the change in 4
;; or 5 other places, and if one forgets to, bugs with non-obvious
;; causes happen. I think library using EIEIO or structs would be
;; very helpful.
;;; Code:
;;;; Requirements
(require 'cl-lib)
(require 'tabulated-list)
(require 'ement)
;;;; Variables
(declare-function ement-notify-switch-to-mentions-buffer "ement-notify")
(declare-function ement-notify-switch-to-notifications-buffer "ement-notify")
(defvar ement-tabulated-room-list-mode-map
(let ((map (make-sparse-keymap)))
;; (define-key map (kbd "g") #'tabulated-list-revert)
;; (define-key map (kbd "q") #'bury-buffer)
(define-key map (kbd "SPC") #'ement-tabulated-room-list-next-unread)
(define-key map (kbd "M-g M-m") #'ement-notify-switch-to-mentions-buffer)
(define-key map (kbd "M-g M-n") #'ement-notify-switch-to-notifications-buffer)
;; (define-key map (kbd "S") #'tabulated-list-sort)
map))
(defvar ement-tabulated-room-list-timestamp-colors nil
"List of colors used for timestamps.
Set automatically when `ement-tabulated-room-list-mode' is activated.")
(defvar ement-sessions)
;;;; Customization
(defgroup ement-tabulated-room-list nil
"Options for the room list buffer."
:group 'ement)
(defcustom ement-tabulated-room-list-auto-update t
"Automatically update the room list buffer."
:type 'boolean)
(defcustom ement-tabulated-room-list-avatars (display-images-p)
"Show room avatars in the room list."
:type 'boolean)
(defcustom ement-tabulated-room-list-simplify-timestamps t
"Only show the largest unit of time in a timestamp.
For example, \"1h54m3s\" becomes \"1h\"."
:type 'boolean)
;;;;; Faces
(defface ement-tabulated-room-list-name
'((t (:inherit font-lock-function-name-face button)))
"Non-direct rooms.")
(defface ement-tabulated-room-list-direct
;; In case `font-lock-constant-face' is bold, we set the weight to normal, so it can be
;; made bold for unread rooms only.
'((t (:weight normal :inherit (font-lock-constant-face ement-tabulated-room-list-name))))
"Direct rooms.")
(defface ement-tabulated-room-list-invited
'((t (:inherit italic ement-tabulated-room-list-name)))
"Invited rooms.")
(defface ement-tabulated-room-list-left
'((t (:strike-through t :inherit ement-tabulated-room-list-name)))
"Left rooms.")
(defface ement-tabulated-room-list-unread
'((t (:inherit bold ement-tabulated-room-list-name)))
"Unread rooms.")
(defface ement-tabulated-room-list-favourite '((t (:inherit (font-lock-doc-face ement-tabulated-room-list-name))))
"Favourite rooms.")
(defface ement-tabulated-room-list-low-priority '((t (:inherit (font-lock-comment-face ement-tabulated-room-list-name))))
"Low-priority rooms.")
(defface ement-tabulated-room-list-recent
'((t (:inherit font-lock-warning-face)))
"Latest timestamp of recently updated rooms.
The foreground color is used to generate a gradient of colors
from recent to non-recent for rooms updated in the past 24
hours but at least one hour ago.")
(defface ement-tabulated-room-list-very-recent
'((t (:inherit error)))
"Latest timestamp of very recently updated rooms.
The foreground color is used to generate a gradient of colors
from recent to non-recent for rooms updated in the past hour.")
;;;; Bookmark support
;; Especially useful with Burly: <https://github.com/alphapapa/burly.el>
(require 'bookmark)
(defun ement-tabulated-room-list-bookmark-make-record ()
"Return a bookmark record for the `ement-tabulated-room-list' buffer."
(pcase-let* (((cl-struct ement-session user) ement-session)
((cl-struct ement-user (id session-id)) user))
;; MAYBE: Support bookmarking specific events in a room.
(list (concat "Ement room list (" session-id ")")
(cons 'session-id session-id)
(cons 'handler #'ement-tabulated-room-list-bookmark-handler))))
(defun ement-tabulated-room-list-bookmark-handler (bookmark)
"Show Ement room list buffer for BOOKMARK."
(pcase-let* (((map session-id) bookmark))
(unless (alist-get session-id ement-sessions nil nil #'equal)
;; MAYBE: Automatically connect.
(user-error "Session %s not connected: call `ement-connect' first" session-id))
(ement-tabulated-room-list)))
;;;; Commands
(defun ement-tabulated-room-list-next-unread ()
"Show next unread room."
(interactive)
(unless (button-at (point))
(call-interactively #'forward-button))
(unless (cl-loop with starting-line = (line-number-at-pos)
if (equal "U" (elt (tabulated-list-get-entry) 0))
do (progn
(goto-char (button-end (button-at (point))))
(push-button (1- (point)))
(cl-return t))
else do (call-interactively #'forward-button)
while (> (line-number-at-pos) starting-line))
;; No more unread rooms.
(message "No more unread rooms")))
;;;###autoload
(defun ement-tabulated-room-list (&rest _ignore)
"Show buffer listing joined rooms.
Calls `pop-to-buffer-same-window'. Interactively, with prefix,
call `pop-to-buffer'."
(interactive)
(with-current-buffer (get-buffer-create "*Ement Rooms*")
(ement-tabulated-room-list-mode)
(setq-local bookmark-make-record-function #'ement-tabulated-room-list-bookmark-make-record)
;; FIXME: There must be a better way to handle this.
(funcall (if current-prefix-arg
#'pop-to-buffer #'pop-to-buffer-same-window)
(current-buffer))))
(defun ement-tabulated-room-list--timestamp-colors ()
"Return a vector of generated latest-timestamp colors for rooms.
Used in `ement-tabulated-room-list' and `ement-room-list'."
(if (or (equal "unspecified-fg" (face-foreground 'default nil 'default))
(equal "unspecified-bg" (face-background 'default nil 'default)))
;; NOTE: On a TTY, the default face's foreground and background colors may be the
;; special values "unspecified-fg"/"unspecified-bg", in which case we can't generate
;; gradients, so we just return a vector of "unspecified-fg". See
;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=55623>.
(make-vector 134 "unspecified-fg")
(cl-coerce
(append (mapcar
;; One face per 10-minute period, from "recent" to 1-hour.
(lambda (rgb)
(pcase-let ((`(,r ,g ,b) rgb))
(color-rgb-to-hex r g b 2)))
(color-gradient (color-name-to-rgb (face-foreground 'ement-tabulated-room-list-very-recent
nil 'default))
(color-name-to-rgb (face-foreground 'ement-tabulated-room-list-recent
nil 'default))
6))
(mapcar
;; One face per hour, from "recent" to default.
(lambda (rgb)
(pcase-let ((`(,r ,g ,b) rgb))
(color-rgb-to-hex r g b 2)))
(color-gradient (color-name-to-rgb (face-foreground 'ement-tabulated-room-list-recent
nil 'default))
(color-name-to-rgb (face-foreground 'default nil 'default))
24))
(mapcar
;; One face per week for the last year (actually we
;; generate colors for the past two years' worth so
;; that the face for one-year-ago is halfway to
;; invisible, and we don't use colors past that point).
(lambda (rgb)
(pcase-let ((`(,r ,g ,b) rgb))
(color-rgb-to-hex r g b 2)))
(color-gradient (color-name-to-rgb (face-foreground 'default nil 'default))
(color-name-to-rgb (face-background 'default nil 'default))
104)))
'vector)))
(define-derived-mode ement-tabulated-room-list-mode tabulated-list-mode
"Ement-Tabulated-Room-List"
:group 'ement
(setf tabulated-list-format (vector
'("U" 1 t)
'(#("P" 0 1 (help-echo "Priority (favorite/low)")) 1 t)
'("B" 1 t)
;; '("U" 1 t)
'("d" 1 t) ; Direct
(list (propertize "🐱"
'help-echo "Avatar")
4 t) ; Avatar
'("Name" 25 t) '("Topic" 35 t)
(list "Latest"
(if ement-tabulated-room-list-simplify-timestamps
6 20)
#'ement-tabulated-room-list-latest<
:right-align t)
'("Members" 7 ement-tabulated-room-list-members<)
;; '("P" 1 t) '("Tags" 15 t)
'("Session" 15 t))
tabulated-list-sort-key '("Latest" . t)
ement-tabulated-room-list-timestamp-colors (ement-tabulated-room-list--timestamp-colors))
(add-hook 'tabulated-list-revert-hook #'ement-tabulated-room-list--set-entries nil 'local)
(tabulated-list-init-header)
(ement-tabulated-room-list--set-entries)
(tabulated-list-revert))
(defun ement-tabulated-room-list-action (event)
"Show buffer for room at EVENT or point."
(interactive "e")
(mouse-set-point event)
(pcase-let* ((room (tabulated-list-get-id))
(`[,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name ,_topic ,_latest ,_members ,user-id]
(tabulated-list-get-entry))
(session (alist-get user-id ement-sessions nil nil #'equal)))
(ement-view-room room session)))
;;;; Functions
;;;###autoload
(defun ement-tabulated-room-list-auto-update (_session)
"Automatically update the room list buffer.
Does so when variable `ement-tabulated-room-list-auto-update' is non-nil.
To be called in `ement-sync-callback-hook'."
(when (and ement-tabulated-room-list-auto-update
(buffer-live-p (get-buffer "*Ement Rooms*")))
(with-current-buffer (get-buffer "*Ement Rooms*")
(revert-buffer))))
(defun ement-tabulated-room-list--set-entries ()
"Set `tabulated-list-entries'."
;; Reset avatar size in case default font size has changed.
;; TODO: After implementing avatars.
;; (customize-set-variable 'ement-room-avatar-in-buffer-name-size ement-room-avatar-in-buffer-name-size)
;; NOTE: From Emacs docs:
;; This buffer-local variable specifies the entries displayed in the
;; Tabulated List buffer. Its value should be either a list, or a
;; function.
;;
;; If the value is a list, each list element corresponds to one entry,
;; and should have the form ‘(ID CONTENTS)’, where
;;
;; • ID is either ‘nil’, or a Lisp object that identifies the
;; entry. If the latter, the cursor stays on the same entry when
;; re-sorting entries. Comparison is done with ‘equal’.
;;
;; • CONTENTS is a vector with the same number of elements as
;; ‘tabulated-list-format’. Each vector element is either a
;; string, which is inserted into the buffer as-is, or a list
;; ‘(LABEL . PROPERTIES)’, which means to insert a text button by
;; calling ‘insert-text-button’ with LABEL and PROPERTIES as
;; arguments (*note Making Buttons::).
;;
;; There should be no newlines in any of these strings.
(let ((entries (cl-loop for (_id . session) in ement-sessions
append (mapcar (lambda (room)
(ement-tabulated-room-list--entry session room))
(ement-session-rooms session)))))
(setf tabulated-list-entries
;; Pre-sort by latest event so that, when the list is sorted by other columns,
;; the rooms will be secondarily sorted by latest event.
(cl-sort entries #'> :key (lambda (entry)
;; In case a room has no latest event (not sure if
;; this may obscure a bug, but this has happened, so
;; we need to handle it), we fall back to 0.
(or (ement-room-latest-ts (car entry)) 0))))))
(defun ement-tabulated-room-list--entry (session room)
"Return entry for ROOM in SESSION for `tabulated-list-entries'."
(pcase-let* (((cl-struct ement-room id canonical-alias display-name avatar topic latest-ts summary
(local (map buffer room-list-avatar)))
room)
((map ('m.joined_member_count member-count)) summary)
(e-alias (or canonical-alias
(setf (ement-room-canonical-alias room)
(ement--room-alias room))
id))
;; FIXME: Figure out how to track unread status cleanly.
(e-unread (if (and buffer (buffer-modified-p buffer))
(propertize "U" 'help-echo "Unread") ""))
(e-buffer (if buffer (propertize "B" 'help-echo "Room has buffer") ""))
(e-avatar (if (and ement-tabulated-room-list-avatars avatar)
(or room-list-avatar
(if-let* ((avatar-image (get-text-property 0 'display avatar))
(new-avatar-string (propertize " " 'display
(ement--resize-image avatar-image
nil (frame-char-height)))))
(progn
;; alist-get doesn't seem to return the new value when used with setf?
(setf (alist-get 'room-list-avatar (ement-room-local room))
new-avatar-string)
new-avatar-string)
;; If a room avatar image fails to download or decode
;; and ends up nil, we return the empty string.
(ement-debug "nil avatar for room: " (ement-room-display-name room) (ement-room-canonical-alias room))
""))
;; Room avatars disabled.
""))
;; We have to copy the list, otherwise using `setf' on it
;; later causes its value to be mutated for every entry.
(name-face (cl-copy-list '(:inherit (ement-tabulated-room-list-name))))
(e-name (list (propertize (or display-name
(ement--room-display-name room))
;; HACK: Apply face here, otherwise tabulated-list overrides it.
'face name-face
'help-echo e-alias)
'action #'ement-tabulated-room-list-action))
(e-topic (if topic
;; Remove newlines from topic. Yes, this can happen.
(replace-regexp-in-string "\n" "" topic t t)
""))
(formatted-timestamp (if latest-ts
(ement--human-format-duration (- (time-convert nil 'integer) (/ latest-ts 1000))
t)
""))
(latest-face (when latest-ts
(let* ((difference-seconds (- (float-time) (/ latest-ts 1000)) )
(n (cl-typecase difference-seconds
((number 0 3599) ;; 1 hour to 1 day: 24 1-hour periods.
(truncate (/ difference-seconds 600)))
((number 3600 86400) ;; 1 day
(+ 6 (truncate (/ difference-seconds 3600))))
(otherwise ;; Difference in weeks.
(min (/ (length ement-tabulated-room-list-timestamp-colors) 2)
(+ 24 (truncate (/ difference-seconds 86400 7))))))))
(list :foreground (elt ement-tabulated-room-list-timestamp-colors n)))))
(e-latest (or (when formatted-timestamp
(propertize formatted-timestamp
'value latest-ts
'face latest-face))
;; Invited rooms don't have a latest-ts.
""))
(e-session (propertize (ement-user-id (ement-session-user session))
'value session))
;; ((e-tags favorite-p low-priority-p) (ement-tabulated-room-list--tags room))
(e-direct-p (if (ement--room-direct-p room session)
(propertize "d" 'help-echo "Direct room")
""))
(e-priority (cond ((ement--room-favourite-p room) "F")
((ement--room-low-priority-p room) "l")
(" ")))
(e-members (if member-count (number-to-string member-count) "")))
(when ement-tabulated-room-list-simplify-timestamps
(setf e-latest (replace-regexp-in-string
(rx bos (1+ digit) (1+ alpha) (group (1+ (1+ digit) (1+ alpha))))
"" e-latest t t 1)))
;; Add face modifiers.
(when (and buffer (buffer-modified-p buffer))
;; For some reason, `push' doesn't work with `map-elt'.
(setf (map-elt name-face :inherit)
(cons 'ement-tabulated-room-list-unread (map-elt name-face :inherit))))
(when (ement--room-direct-p room session)
(setf (map-elt name-face :inherit)
(cons 'ement-tabulated-room-list-direct (map-elt name-face :inherit))))
(when (ement--room-favourite-p room)
(push 'ement-tabulated-room-list-favourite (map-elt name-face :inherit)))
(when (ement--room-low-priority-p room)
(push 'ement-tabulated-room-list-low-priority (map-elt name-face :inherit)))
(pcase (ement-room-type room)
('invite
(setf e-topic (concat (propertize "[invited]"
'face 'ement-tabulated-room-list-invited)
" " e-topic)
(map-elt name-face :inherit) (cons 'ement-tabulated-room-list-invited
(map-elt name-face :inherit))))
('leave
(setf e-topic (concat (propertize "[left]"
'face 'ement-tabulated-room-list-left)
" " e-topic)
(map-elt name-face :inherit) (cons (map-elt name-face :inherit)
'ement-tabulated-room-list-left))))
(list room (vector e-unread e-priority e-buffer e-direct-p
e-avatar e-name e-topic e-latest e-members
;; e-tags
e-session
;; e-avatar
))))
;; TODO: Define sorters with a macro? This gets repetitive and hard to update.
(defun ement-tabulated-room-list-members< (a b)
"Return non-nil if entry A has fewer members than room B.
A and B should be entries from `tabulated-list-mode'."
(pcase-let* ((`(,_room [,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name-for-list ,_topic ,_latest ,a-members ,_session]) a)
(`(,_room [,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name-for-list ,_topic ,_latest ,b-members ,_session]) b))
(when (and a-members b-members)
;; Invited rooms may have no member count (I think).
(< (string-to-number a-members) (string-to-number b-members)))))
(defun ement-tabulated-room-list-latest< (a b)
"Return non-nil if entry A has fewer members than room B.
A and B should be entries from `tabulated-list-mode'."
(pcase-let* ((`(,_room-a [,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name-for-list ,_topic ,a-latest ,_a-members ,_session]) a)
(`(,_room-b [,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name-for-list ,_topic ,b-latest ,_b-members ,_session]) b)
(a-latest (get-text-property 0 'value a-latest))
(b-latest (get-text-property 0 'value b-latest)))
(cond ((and a-latest b-latest)
(< a-latest b-latest))
(b-latest
;; Invited rooms have no latest timestamp, and we want to sort them first.
nil)
(t t))))
;;;; Footer
(provide 'ement-tabulated-room-list)
;;; ement-tabulated-room-list.el ends here
;;; ement.el --- Matrix client -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; URL: https://github.com/alphapapa/ement.el
;; Version: 0.9.2
;; Package-Requires: ((emacs "27.1") (map "2.1") (persist "0.5") (plz "0.2") (taxy "0.10") (taxy-magit-section "0.12.1") (svg-lib "0.2.5") (transient "0.3.7"))
;; Keywords: comm
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Another Matrix client! This one is written from scratch and is
;; intended to be more "Emacsy," more suitable for MELPA, etc. Also
;; it has a shorter, perhaps catchier name, that is a mildly clever
;; play on the name of the official Matrix client and the Emacs Lisp
;; filename extension (oops, I explained the joke), which makes for
;; much shorter symbol names.
;; This file implements the core client library. Functions that may be called in multiple
;; files belong in `ement-lib'.
;;; Code:
;;;; Debugging
;; NOTE: Uncomment this form and `emacs-lisp-byte-compile-and-load' the file to enable
;; `ement-debug' messages. This is commented out by default because, even though the
;; messages are only displayed when `warning-minimum-log-level' is `:debug' at runtime, if
;; that is so at expansion time, the expanded macro calls format the message and check the
;; log level at runtime, which is not zero-cost.
;; (eval-and-compile
;; (require 'warnings)
;; (setq-local warning-minimum-log-level nil)
;; (setq-local warning-minimum-log-level :debug))
;;;; Requirements
;; Built in.
(require 'cl-lib)
(require 'dns)
(require 'files)
(require 'map)
;; This package.
(require 'ement-lib)
(require 'ement-room)
(require 'ement-notify)
;;;; Variables
(defvar ement-sessions nil
"Alist of active `ement-session' sessions, keyed by MXID.")
(defvar ement-syncs nil
"Alist of outstanding sync processes for each session.")
(defvar ement-users (make-hash-table :test #'equal)
;; NOTE: When changing the ement-user struct, it's necessary to
;; reset this table to clear old-type structs.
"Hash table storing user structs keyed on user ID.")
(defvar ement-progress-reporter nil
"Used to report progress while processing sync events.")
(defvar ement-progress-value nil
"Used to report progress while processing sync events.")
(defvar ement-sync-callback-hook
'(ement--update-room-buffers ement--auto-sync ement-tabulated-room-list-auto-update
ement-room-list-auto-update)
"Hook run after `ement--sync-callback'.
Hooks are called with one argument, the session that was
synced.")
(defvar ement-event-hook
'(ement-notify ement--process-event ement--put-event)
"Hook called for events.
Each function is called with three arguments: the event, the
room, and the session. This hook isn't intended to be modified
by users; ones who do so should know what they're doing.")
(defvar ement-default-sync-filter
'((room (state (lazy_load_members . t))
(timeline (lazy_load_members . t))))
"Default filter for sync requests.")
(defvar ement-images-queue (make-plz-queue :limit 5)
"`plz' HTTP request queue for image requests.")
(defvar ement-read-receipt-idle-timer nil
"Idle timer used to update read receipts.")
;; From other files.
(defvar ement-room-avatar-max-width)
(defvar ement-room-avatar-max-height)
;;;; Customization
(defgroup ement nil
"Options for Ement, the Matrix client."
:group 'comm)
(defcustom ement-save-sessions nil
"Save session to disk.
Writes the session file when Emacs is killed."
:type 'boolean
:set (lambda (option value)
(set-default option value)
(if value
(add-hook 'kill-emacs-hook #'ement--kill-emacs-hook)
(remove-hook 'kill-emacs-hook #'ement--kill-emacs-hook))))
(defcustom ement-sessions-file "~/.cache/ement.el"
;; FIXME: Expand correct XDG cache directory (new in Emacs 27).
"Save username and access token to this file."
:type 'file)
(defcustom ement-auto-sync t
"Automatically sync again after syncing."
:type 'boolean)
(defcustom ement-after-initial-sync-hook
'(ement-room-list--after-initial-sync ement-view-initial-rooms ement--link-children ement--run-idle-timer)
"Hook run after initial sync.
Run with one argument, the session synced."
:type 'hook)
(defcustom ement-initial-sync-timeout 40
"Timeout in seconds for initial sync requests.
For accounts in many rooms, the Matrix server may take some time
to prepare the initial sync response, and increasing this timeout
might be necessary."
:type 'integer)
(defcustom ement-auto-view-rooms nil
"Rooms to view after initial sync.
Alist mapping user IDs to a list of room aliases/IDs to open buffers for."
:type '(alist :key-type (string :tag "Local user ID")
:value-type (repeat (string :tag "Room alias/ID"))))
(defcustom ement-disconnect-hook '(ement-kill-buffers ement--stop-idle-timer)
;; FIXME: Put private functions in a private hook.
"Functions called when disconnecting.
That is, when calling command `ement-disconnect'. Functions are
called with no arguments."
:type 'hook)
(defcustom ement-view-room-display-buffer-action '(display-buffer-same-window)
"Display buffer action to use when opening room buffers.
See function `display-buffer' and info node `(elisp) Buffer
Display Action Functions'."
:type 'function)
(defcustom ement-auto-view-room-display-buffer-action '(display-buffer-no-window)
"Display buffer action to use when automatically opening room buffers.
That is, rooms listed in `ement-auto-view-rooms', which see. See
function `display-buffer' and info node `(elisp) Buffer Display
Action Functions'."
:type 'function)
(defcustom ement-interrupted-sync-hook '(ement-interrupted-sync-warning)
"Functions to call when syncing of a session is interrupted.
Only called when `ement-auto-sync' is non-nil. Functions are
called with one argument, the session whose sync was interrupted.
This hook allows the user to customize how sync interruptions are
handled (e.g. how to be notified)."
:type 'hook
:options '(ement-interrupted-sync-message ement-interrupted-sync-warning))
;;;; Commands
;;;###autoload
(cl-defun ement-connect (&key user-id password uri-prefix session)
"Connect to Matrix with USER-ID and PASSWORD, or using SESSION.
Interactively, with prefix, ignore a saved session and log in
again; otherwise, use a saved session if `ement-save-sessions' is
enabled and a saved session is available, or prompt to log in if
not enabled or available.
If USERID or PASSWORD are not specified, the user will be
prompted for them.
If URI-PREFIX is specified, it should be the prefix of the
server's API URI, including protocol, hostname, and optionally
the port, e.g.
\"https://matrix-client.matrix.org\"
\"http://localhost:8080\""
(interactive (if current-prefix-arg
;; Force new session.
(list :user-id (read-string "User ID: ")
:password (read-passwd "Password: "))
;; Use known session.
(unless ement-sessions
;; Read sessions from disk.
(condition-case err
(setf ement-sessions (ement--read-sessions))
(error (display-warning 'ement (format "Unable to read session data from disk (%s). Prompting to log in again."
(error-message-string err))))))
(cl-case (length ement-sessions)
(0 (list :user-id (read-string "User ID: ")
:password (read-passwd "Password: ")))
(1 (list :session (cdar ement-sessions)))
(otherwise (list :session (ement-complete-session))))))
(cl-labels ((new-session
() (unless (string-match (rx bos "@" (group (1+ (not (any ":")))) ; Username
":" (group (optional (1+ (not (any blank)))))) ; Server name
user-id)
(user-error "Invalid user ID format: use @USERNAME:SERVER"))
(let* ((username (match-string 1 user-id))
(server-name (match-string 2 user-id))
(uri-prefix (or uri-prefix (ement--hostname-uri server-name)))
(user (make-ement-user :id user-id :username username))
(server (make-ement-server :name server-name :uri-prefix uri-prefix))
(transaction-id (ement--initial-transaction-id))
(initial-device-display-name (format "Ement.el: %s@%s"
;; Just to be extra careful:
(or user-login-name "[unknown user-login-name]")
(or (system-name) "[unknown system-name]")))
(device-id (secure-hash 'sha256 initial-device-display-name)))
(make-ement-session :user user :server server :transaction-id transaction-id
:device-id device-id :initial-device-display-name initial-device-display-name
:events (make-hash-table :test #'equal))))
(password-login
() (pcase-let* (((cl-struct ement-session user device-id initial-device-display-name) session)
((cl-struct ement-user id) user)
(data (ement-alist "type" "m.login.password"
"identifier"
(ement-alist "type" "m.id.user"
"user" id)
"password" password
"device_id" device-id
"initial_device_display_name" initial-device-display-name)))
;; TODO: Clear password in callback (if we decide to hold on to it for retrying login timeouts).
(ement-api session "login" :method 'post :data (json-encode data)
:then (apply-partially #'ement--login-callback session))))
(flows-callback
(data) (if (cl-loop for flow across (map-elt data 'flows)
thereis (equal (map-elt flow 'type) "m.login.password"))
(progn
(message "Ement: Logging in with password...")
(password-login))
(error "Matrix server doesn't support m.login.password login flow. Supported flows: %s"
(cl-loop for flow in (map-elt data 'flows)
collect (map-elt flow 'type))))))
(if session
;; Start syncing given session.
(let ((user-id (ement-user-id (ement-session-user session))))
;; HACK: If session is already in ement-sessions, this replaces it. I think that's okay...
(setf (alist-get user-id ement-sessions nil nil #'equal) session)
(ement--sync session :timeout ement-initial-sync-timeout))
;; Start password login flow. Prompt for user ID and password
;; if not given (i.e. if not called interactively.)
(unless user-id
(setf user-id (read-string "User ID: ")))
(unless password
(setf password (read-passwd (format "Password for %s: " user-id))))
(setf session (new-session))
(when (ement-api session "login" :then #'flows-callback)
(message "Ement: Checking server's login flows...")))))
(defun ement-disconnect (sessions)
"Disconnect from SESSIONS.
Interactively, with prefix, disconnect from all sessions. If
`ement-auto-sync' is enabled, stop syncing, and clear the session
data. When enabled, write the session to disk. Any existing
room buffers are left alive and can be read, but other commands
in them won't work."
(interactive (list (if current-prefix-arg
(mapcar #'cdr ement-sessions)
(list (ement-complete-session)))))
(when ement-save-sessions
;; Write sessions before we remove them from the variable.
(ement--write-sessions ement-sessions))
(dolist (session sessions)
(let ((user-id (ement-user-id (ement-session-user session))))
(when-let ((process (map-elt ement-syncs session)))
(ignore-errors
(delete-process process)))
;; NOTE: I'd like to use `map-elt' here, but not until
;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=47368> is fixed, I guess.
(setf (alist-get session ement-syncs nil nil #'equal) nil
(alist-get user-id ement-sessions nil 'remove #'equal) nil)))
(unless ement-sessions
;; HACK: If no sessions remain, clear the users table. It might be best
;; to store a per-session users table, but this is probably good enough.
(clrhash ement-users))
(run-hooks 'ement-disconnect-hook)
(message "Ement: Disconnected (%s)"
(string-join (cl-loop for session in sessions
collect (ement-user-id (ement-session-user session)))
", ")))
(defun ement-kill-buffers ()
"Kill all Ement buffers.
Useful in, e.g. `ement-disconnect-hook', which see."
(interactive)
(dolist (buffer (buffer-list))
(when (string-prefix-p "ement-" (symbol-name (buffer-local-value 'major-mode buffer)))
(kill-buffer buffer))))
(defun ement--login-callback (session data)
"Record DATA from logging in to SESSION and do initial sync."
(pcase-let* (((cl-struct ement-session (user (cl-struct ement-user (id user-id)))) session)
((map ('access_token token) ('device_id device-id)) data))
(setf (ement-session-token session) token
(ement-session-device-id session) device-id
(alist-get user-id ement-sessions nil nil #'equal) session)
(ement--sync session :timeout ement-initial-sync-timeout)))
;;;; Functions
(defun ement-interrupted-sync-warning (session)
"Display a warning that syncing of SESSION was interrupted."
(display-warning
'ement
(format
(substitute-command-keys
"\\<ement-room-mode-map>Syncing of session <%s> was interrupted. Use command `ement-room-sync' in a room buffer to retry.")
(ement-user-id (ement-session-user session)))
:error))
(defun ement-interrupted-sync-message (session)
"Display a message that syncing of SESSION was interrupted."
(message
(substitute-command-keys
"\\<ement-room-mode-map>Syncing of session <%s> was interrupted. Use command `ement-room-sync' in a room buffer to retry.")
(ement-user-id (ement-session-user session))))
(defun ement--run-idle-timer (&rest _ignore)
"Run idle timer that updates read receipts.
To be called from `ement-after-initial-sync-hook'. Timer is
stored in `ement-read-receipt-idle-timer'."
(setf ement-read-receipt-idle-timer (run-with-idle-timer 3 t #'ement-room-read-receipt-idle-timer)))
(defun ement--stop-idle-timer (&rest _ignore)
"Stop idle timer stored in `ement-read-receipt-idle-timer'.
To be called from `ement-disconnect-hook'."
(when (timerp ement-read-receipt-idle-timer)
(cancel-timer ement-read-receipt-idle-timer)
(setf ement-read-receipt-idle-timer nil)))
(defun ement-view-initial-rooms (session)
"View rooms for SESSION configured in `ement-auto-view-rooms'."
(when-let (rooms (alist-get (ement-user-id (ement-session-user session))
ement-auto-view-rooms nil nil #'equal))
(dolist (alias/id rooms)
(when-let (room (cl-find-if (lambda (room)
(or (equal alias/id (ement-room-canonical-alias room))
(equal alias/id (ement-room-id room))))
(ement-session-rooms session)))
(let ((ement-view-room-display-buffer-action ement-auto-view-room-display-buffer-action))
(ement-view-room room session))))))
(defun ement--initial-transaction-id ()
"Return an initial transaction ID for a new session."
;; We generate a somewhat-random initial transaction ID to avoid potential conflicts in
;; case, e.g. using Pantalaimon causes a transaction ID conflict. See
;; <https://github.com/alphapapa/ement.el/issues/36>.
(cl-parse-integer
(secure-hash 'sha256 (prin1-to-string (list (current-time) (system-name))))
:end 8 :radix 16))
(defsubst ement--sync-messages-p (session)
"Return non-nil if sync-related messages should be shown for SESSION."
;; For now, this seems like the best way.
(or (not (ement-session-has-synced-p session))
(not ement-auto-sync)))
(defun ement--hostname-uri (hostname)
"Return the \".well-known\" URI for server HOSTNAME.
If no URI is found, prompt the user for the hostname."
;; FIXME: When fail-prompting, a URI should be returned, not just a hostname.
;; SPEC: <https://matrix.org/docs/spec/client_server/r0.6.1#id178> ("4.1 Well-known URI")
(cl-labels ((fail-prompt
() (let ((input (read-string "Auto-discovery of server's well-known URI failed. Input server hostname, or leave blank to use server name: ")))
(pcase input
("" hostname)
(_ input))))
(parse (string)
(if-let ((object (ignore-errors (json-read-from-string string))))
;; Return extracted value.
(map-nested-elt object '(m.homeserver base_url))
;; Parsing error: FAIL_PROMPT.
(fail-prompt))))
(condition-case err
(let ((response (plz 'get (concat "https://" hostname "/.well-known/matrix/client")
:as 'response :then 'sync)))
(if (plz-response-p response)
(pcase (plz-response-status response)
(200 (parse (plz-response-body response)))
(404 (fail-prompt))
(_ (warn "Ement: `plz' request for .well-known URI returned unexpected code: %s"
(plz-response-status response))
(fail-prompt)))
(warn "Ement: `plz' request for .well-known URI did not return a `plz' response")
(fail-prompt)))
(error (warn "Ement: `plz' request for .well-known URI signaled an error: %S" err)
(fail-prompt)))))
(cl-defun ement--sync (session &key force quiet
(timeout 40) ;; Give the server an extra 10 seconds.
(filter ement-default-sync-filter))
"Send sync request for SESSION.
If SESSION has a `next-batch' token, it's used. If FORCE, first
delete any outstanding sync processes. If QUIET, don't show a
message about syncing this time. Cancel request after TIMEOUT
seconds.
FILTER may be an alist representing a raw event filter (i.e. not
a filter ID). When unspecified, the value of
`ement-default-sync-filter' is used. The filter is encoded with
`json-encode'. To use no filter, specify FILTER as nil."
;; SPEC: <https://matrix.org/docs/spec/client_server/r0.6.1#id257>.
;; TODO: Filtering: <https://matrix.org/docs/spec/client_server/r0.6.1#filtering>.
;; TODO: Use a filter ID for default filter.
;; TODO: Optionally, automatically sync again when HTTP request fails.
;; TODO: Ensure that the process in (map-elt ement-syncs session) is live.
(when (map-elt ement-syncs session)
(if force
(condition-case err
(delete-process (map-elt ement-syncs session))
;; Ensure the only error is the expected one from deleting the process.
(ement-api-error (cl-assert (equal "curl process killed" (plz-error-message (cl-third err))))
(message "Ement: Forcing new sync")))
(user-error "Ement: Already syncing this session")))
(pcase-let* (((cl-struct ement-session next-batch) session)
(params (remove
nil (list (list "full_state" (if next-batch "false" "true"))
(when filter
;; TODO: Document filter arg.
(list "filter" (json-encode filter)))
(when next-batch
(list "since" next-batch))
(when next-batch
(list "timeout" "30000")))))
(sync-start-time (time-to-seconds))
;; FIXME: Auto-sync again in error handler.
(process (ement-api session "sync" :params params
:timeout timeout
:then (apply-partially #'ement--sync-callback session)
:else (lambda (plz-error)
(setf (map-elt ement-syncs session) nil)
;; TODO: plz probably needs nicer error handling.
;; Ideally we would use `condition-case', but since the
;; error is signaled in `plz--sentinel'...
(pcase-let (((cl-struct plz-error curl-error response) plz-error)
(reason))
(cond ((when response
(pcase (plz-response-status response)
((or 429 502) (setf reason "failed")))))
((pcase curl-error
(`(28 . ,_) (setf reason "timed out")))))
(if reason
(if (not ement-auto-sync)
(run-hook-with-args 'ement-interrupted-sync-hook session)
(message "Ement: Sync %s (%s). Syncing again..."
reason (ement-user-id (ement-session-user session)))
;; Set QUIET to allow the just-printed message to remain visible.
(ement--sync session :timeout timeout :quiet t))
;; Unrecognized errors:
(pcase curl-error
(`(,code . ,message)
(signal 'ement-api-error (list (format "Ement: Network error: %s: %s" code message)
plz-error)))
(_ (signal 'ement-api-error (list "Ement: Unrecognized network error" plz-error)))))))
:json-read-fn (lambda ()
"Print a message, then call `json-read'."
(when (ement--sync-messages-p session)
(message "Ement: Response arrived after %.2f seconds. Reading %s JSON response..."
(- (time-to-seconds) sync-start-time)
(file-size-human-readable (buffer-size))))
(let ((start-time (time-to-seconds)))
(prog1 (json-read)
(when (ement--sync-messages-p session)
(message "Ement: Reading JSON took %.2f seconds"
(- (time-to-seconds) start-time)))))))))
(when process
(setf (map-elt ement-syncs session) process)
(when (and (not quiet) (ement--sync-messages-p session))
(message "Ement: Sync request sent, waiting for response...")))))
(defun ement--sync-callback (session data)
"Process sync DATA for SESSION.
Runs `ement-sync-callback-hook' with SESSION."
;; Remove the sync first. We already have the data from it, and the
;; process has exited, so it's safe to run another one.
(setf (map-elt ement-syncs session) nil)
(pcase-let* (((map rooms ('next_batch next-batch) ('account_data (map ('events account-data-events))))
data)
((map ('join joined-rooms) ('invite invited-rooms) ('leave left-rooms)) rooms)
(num-events (+
;; HACK: In `ement--push-joined-room-events', we do something
;; with each event 3 times, so we multiply this by 3.
;; FIXME: That calculation doesn't seem to be quite right, because
;; the progress reporter never seems to hit 100% before it's done.
(* 3 (cl-loop for (_id . room) in joined-rooms
sum (length (map-nested-elt room '(state events)))
sum (length (map-nested-elt room '(timeline events)))))
(cl-loop for (_id . room) in invited-rooms
sum (length (map-nested-elt room '(invite_state events)))))))
;; Append account data events.
;; TODO: Since only one event of each type is allowed in account data (the spec
;; doesn't seem to make this clear, but see
;; <https://github.com/matrix-org/matrix-js-sdk/blob/d0b964837f2820940bd93e718a2450b5f528bffc/src/store/memory.ts#L292>),
;; we should store account-data events in a hash table or alist rather than just a
;; list of events.
(cl-callf2 append (cl-coerce account-data-events 'list) (ement-session-account-data session))
;; Process invited and joined rooms.
(ement-with-progress-reporter (:when (ement--sync-messages-p session)
:reporter ("Ement: Reading events..." 0 num-events))
;; Left rooms.
(mapc (apply-partially #'ement--push-left-room-events session) left-rooms)
;; Invited rooms.
(mapc (apply-partially #'ement--push-invite-room-events session) invited-rooms)
;; Joined rooms.
(mapc (apply-partially #'ement--push-joined-room-events session) joined-rooms))
;; TODO: Process "left" rooms (remove room structs, etc).
;; NOTE: We update the next-batch token before updating any room buffers. This means
;; that any errors in updating room buffers (like for unexpected event formats that
;; expose a bug) could cause events to not appear in the buffer, but the user could
;; still dismiss the error and start syncing again, and the client could remain
;; usable. Updating the token after doing everything would be preferable in some
;; ways, but it would mean that an event that exposes a bug would be processed again
;; on every sync, causing the same error each time. It would seem preferable to
;; maintain at least some usability rather than to keep repeating a broken behavior.
(setf (ement-session-next-batch session) next-batch)
;; Run hooks which update buffers, etc.
(run-hook-with-args 'ement-sync-callback-hook session)
;; Show sync message if appropriate, and run after-initial-sync-hook.
(when (ement--sync-messages-p session)
(message (concat "Ement: Sync done."
(unless (ement-session-has-synced-p session)
(run-hook-with-args 'ement-after-initial-sync-hook session)
;; Show tip after initial sync.
(setf (ement-session-has-synced-p session) t)
" Use commands `ement-list-rooms' or `ement-view-room' to view a room."))))))
(defun ement--push-invite-room-events (session invited-room)
"Push events for INVITED-ROOM into that room in SESSION."
;; TODO: Make ement-session-rooms a hash-table.
(ement--push-joined-room-events session invited-room 'invite))
(defun ement--auto-sync (session)
"If `ement-auto-sync' is non-nil, sync SESSION again."
(when ement-auto-sync
(ement--sync session)))
(defun ement--update-room-buffers (session)
"Insert new events into SESSION's rooms which have buffers.
To be called in `ement-sync-callback-hook'."
;; TODO: Move this to ement-room.el, probably.
;; For now, we primitively iterate over the buffer list to find ones
;; whose mode is `ement-room-mode'.
(let* ((buffers (cl-loop for room in (ement-session-rooms session)
for buffer = (map-elt (ement-room-local room) 'buffer)
when (buffer-live-p buffer)
collect buffer)))
(dolist (buffer buffers)
(with-current-buffer buffer
(save-window-excursion
;; NOTE: When the buffer has a window, it must be the selected one
;; while calling event-insertion functions. I don't know if this is
;; due to a bug in EWOC or if I just misunderstand something, but
;; without doing this, events may be inserted at the wrong place.
(when-let ((buffer-window (get-buffer-window buffer)))
(select-window buffer-window))
(cl-assert ement-room)
(when (ement-room-ephemeral ement-room)
;; Ephemeral events.
(ement-room--process-events (ement-room-ephemeral ement-room))
(setf (ement-room-ephemeral ement-room) nil))
(when-let ((new-events (alist-get 'new-events (ement-room-local ement-room))))
;; HACK: Process these events in reverse order, so that later events (like reactions)
;; which refer to earlier events can find them. (Not sure if still necessary.)
(ement-room--process-events (reverse new-events))
(setf (alist-get 'new-events (ement-room-local ement-room)) nil))
(when-let ((new-events (alist-get 'new-account-data-events (ement-room-local ement-room))))
;; Account data events. Do this last so, e.g. read markers can refer to message events we've seen.
(ement-room--process-events new-events)
(setf (alist-get 'new-account-data-events (ement-room-local ement-room)) nil)))))))
(cl-defun ement--push-joined-room-events (session joined-room &optional (status 'join))
"Push events for JOINED-ROOM into that room in SESSION.
Also used for left rooms, in which case STATUS should be set to
`leave'."
(pcase-let* ((`(,id . ,event-types) joined-room)
(id (symbol-name id)) ; Really important that the ID is a STRING!
;; TODO: Make ement-session-rooms a hash-table.
(room (or (cl-find-if (lambda (room)
(equal id (ement-room-id room)))
(ement-session-rooms session))
(car (push (make-ement-room :id id) (ement-session-rooms session)))))
((map summary state ephemeral timeline
('invite_state (map ('events invite-state-events)))
('account_data (map ('events account-data-events)))
('unread_notifications unread-notifications))
event-types)
(latest-timestamp))
(setf (ement-room-status room) status
(ement-room-unread-notifications room) unread-notifications)
;; NOTE: The idea is that, assuming that events in the sync reponse are in
;; chronological order, we push them to the lists in the room slots in that order,
;; leaving the head of each list as the most recent event of that type. That means
;; that, e.g. the room state events may be searched in order to find, e.g. the most
;; recent room name event. However, chronological order is not guaranteed, e.g. after
;; loading older messages (the "retro" function; this behavior is in development).
;; MAYBE: Use queue.el to store the events in a DLL, so they could
;; be accessed from either end. Could be useful.
;; Push the StrippedState events to the room's invite-state. (These events have no
;; timestamp data.) We also run the event hook, because for invited rooms, the
;; invite-state events include room name, topic, etc.
(cl-loop for event across-ref invite-state-events do
(setf event (ement--make-event event))
(push event (ement-room-invite-state room))
(run-hook-with-args 'ement-event-hook event room session))
;; Save room summary.
(dolist (parameter '(m.heroes m.joined_member_count m.invited_member_count))
(when (alist-get parameter summary)
;; These fields are only included when they change.
(setf (alist-get parameter (ement-room-summary room)) (alist-get parameter summary))))
;; Update account data. According to the spec, only one of each event type is
;; supposed to be present in a room's account data, so we store them as an alist keyed
;; on their type. (NOTE: We don't currently make them into event structs, but maybe
;; we should in the future.)
(cl-loop for event across account-data-events
for type = (alist-get 'type event)
do (setf (alist-get type (ement-room-account-data room) nil nil #'equal) event))
;; But we also need to track just the new events so we can process those in a room
;; buffer (and for some reason, we do make them into structs here, but I don't
;; remember why). FIXME: Unify this.
(cl-callf2 append (mapcar #'ement--make-event account-data-events)
(alist-get 'new-account-data-events (ement-room-local room)))
;; Save state and timeline events.
(cl-macrolet ((push-events
(type accessor)
;; Push new events of TYPE to room's slot of ACCESSOR, and return the latest timestamp pushed.
`(let ((ts 0))
;; NOTE: We replace each event in the vector with the
;; struct, which is used when calling hooks later.
(cl-loop for event across-ref (alist-get 'events ,type)
do (setf event (ement--make-event event))
do (push event (,accessor room))
(when (ement--sync-messages-p session)
(ement-progress-update))
(when (> (ement-event-origin-server-ts event) ts)
(setf ts (ement-event-origin-server-ts event))))
;; One would think that one should use `maximizing' here, but, completely
;; inexplicably, it sometimes returns nil, even when every single value it's comparing
;; is a number. It's absolutely bizarre, but I have to do the equivalent manually.
ts)))
;; FIXME: This is a bit convoluted and hacky now. Refactor it.
(setf latest-timestamp
(max (push-events state ement-room-state)
(push-events timeline ement-room-timeline)))
;; NOTE: We also append the new events to the new-events list in the room's local
;; slot, which is used by `ement--update-room-buffers' to insert only new events.
;; FIXME: Does this also need to be done for invite-state events?
(cl-callf2 append (cl-coerce (alist-get 'events timeline) 'list)
(alist-get 'new-events (ement-room-local room)))
;; Update room's latest-timestamp slot.
(when (> latest-timestamp (or (ement-room-latest-ts room) 0))
(setf (ement-room-latest-ts room) latest-timestamp))
(unless (ement-session-has-synced-p session)
;; Only set this token on initial sync, otherwise it would
;; overwrite earlier tokens from loading earlier messages.
(setf (ement-room-prev-batch room) (alist-get 'prev_batch timeline))))
;; Run event hook for state and timeline events.
(cl-loop for event across (alist-get 'events state)
do (run-hook-with-args 'ement-event-hook event room session)
(when (ement--sync-messages-p session)
(ement-progress-update)))
(cl-loop for event across (alist-get 'events timeline)
do (run-hook-with-args 'ement-event-hook event room session)
(when (ement--sync-messages-p session)
(ement-progress-update)))
;; Ephemeral events (do this after state and timeline hooks, so those events will be
;; in the hash tables).
(cl-loop for event across (alist-get 'events ephemeral)
for event-struct = (ement--make-event event)
do (push event-struct (ement-room-ephemeral room))
(ement--process-event event-struct room session))
(when (ement-session-has-synced-p session)
;; NOTE: We don't fill gaps in "limited" requests on initial
;; sync, only in subsequent syncs, e.g. after the system has
;; slept and awakened.
;; NOTE: When not limited, the read value is `:json-false', so
;; we must explicitly compare to t.
(when (eq t (alist-get 'limited timeline))
;; Timeline was limited: start filling gap. We start the
;; gap-filling, retrieving up to the session's current
;; next-batch token (this function is not called when retrieving
;; older messages, so the session's next-batch token is only
;; evaluated once, when this chain begins, and then that token
;; is passed to repeated calls to `ement-room-retro-to-token'
;; until the gap is filled).
(ement-room-retro-to-token room session (alist-get 'prev_batch timeline)
(ement-session-next-batch session))))))
(defun ement--push-left-room-events (session left-room)
"Push events for LEFT-ROOM into that room in SESSION."
(ement--push-joined-room-events session left-room 'leave))
(defun ement--make-event (event)
"Return `ement-event' struct for raw EVENT list.
Adds sender to `ement-users' when necessary."
(pcase-let* (((map content type unsigned redacts
('event_id id) ('origin_server_ts ts)
('sender sender-id) ('state_key state-key))
event)
(sender (or (gethash sender-id ement-users)
(puthash sender-id (make-ement-user :id sender-id)
ement-users))))
;; MAYBE: Handle other keys in the event, such as "room_id" in "invite" events.
(make-ement-event :id id :sender sender :type type :content content :state-key state-key
:origin-server-ts ts :unsigned unsigned
;; Since very few events will be redactions and have this key, we
;; record it in the local slot alist rather than as another slot on
;; the struct.
:local (when redacts
(ement-alist 'redacts redacts)))))
(defun ement--put-event (event _room session)
"Put EVENT on SESSION's events table."
(puthash (ement-event-id event) event (ement-session-events session)))
;; FIXME: These functions probably need to compare timestamps to
;; ensure that older events that are inserted at the head of the
;; events lists aren't used instead of newer ones.
;; TODO: These two functions should be folded into event handlers.
;;;;; Reading/writing sessions
(defun ement--read-sessions ()
"Return saved sessions alist read from disk.
Returns nil if unable to read `ement-sessions-file'."
(cl-labels ((plist-to-session
(plist) (pcase-let* (((map (:user user-data) (:server server-data)
(:token token) (:transaction-id transaction-id))
plist)
(user (apply #'make-ement-user user-data))
(server (apply #'make-ement-server server-data))
(session (make-ement-session :user user :server server
:token token :transaction-id transaction-id)))
(setf (ement-session-events session) (make-hash-table :test #'equal))
session)))
(when (file-exists-p ement-sessions-file)
(pcase-let* ((read-circle t)
(sessions (with-temp-buffer
(insert-file-contents ement-sessions-file)
(read (current-buffer)))))
(prog1
(cl-loop for (id . plist) in sessions
collect (cons id (plist-to-session plist)))
(message "Ement: Read sessions."))))))
(defun ement--write-sessions (sessions-alist)
"Write SESSIONS-ALIST to disk."
;; We only record the slots we need. We record them as a plist
;; so that changes to the struct definition don't matter.
;; NOTE: If we ever persist more session data (like room data, so we
;; could avoid doing an initial sync next time), we should limit the
;; amount of session data saved (e.g. room history could grow
;; forever on-disk, which probably isn't what we want).
;; NOTE: This writes all current sessions, even if there are multiple active ones and only one
;; is being disconnected. That's probably okay, but it might be something to keep in mind.
(cl-labels ((session-plist
(session) (pcase-let* (((cl-struct ement-session user server token transaction-id) session)
((cl-struct ement-user (id user-id) username) user)
((cl-struct ement-server (name server-name) uri-prefix) server))
(list :user (list :id user-id
:username username)
:server (list :name server-name
:uri-prefix uri-prefix)
:token token
:transaction-id transaction-id))))
(message "Ement: Writing sessions...")
(with-temp-file ement-sessions-file
(pcase-let* ((print-level nil)
(print-length nil)
;; Very important to use `print-circle', although it doesn't
;; solve everything. Writing/reading Lisp data can be tricky...
(print-circle t)
(sessions-alist-plist (cl-loop for (id . session) in sessions-alist
collect (cons id (session-plist session)))))
(prin1 sessions-alist-plist (current-buffer))))
;; Ensure permissions are safe.
(chmod ement-sessions-file #o600)))
(defun ement--kill-emacs-hook ()
"Function to be added to `kill-emacs-hook'.
Writes Ement session to disk when enabled."
(ignore-errors
;; To avoid interfering with Emacs' exit, We must be careful that
;; this function handles errors, so just ignore any.
(when (and ement-save-sessions
ement-sessions)
(ement--write-sessions ement-sessions))))
;;;;; Event handlers
(defvar ement-event-handlers nil
"Alist mapping event types to functions which process an event of each type.
Each function is called with three arguments: the event, the
room, and the session. These handlers are run regardless of
whether a room has a live buffer.")
(defun ement--process-event (event room session)
"Process EVENT for ROOM in SESSION.
Uses handlers defined in `ement-event-handlers'. If no handler
is defined for EVENT's type, does nothing and returns nil. Any
errors signaled during processing are demoted in order to prevent
unexpected errors from arresting event processing and syncing."
(when-let ((handler (alist-get (ement-event-type event) ement-event-handlers nil nil #'equal)))
;; We demote any errors that happen while processing events, because it's possible for
;; events to be malformed in unexpected ways, and that could cause an error, which
;; would stop processing of other events and prevent further syncing. See,
;; e.g. <https://github.com/alphapapa/ement.el/pull/61>.
(with-demoted-errors "Ement (ement--process-event): Error processing event: %S"
(funcall handler event room session))))
(defmacro ement-defevent (type &rest body)
"Define an event handling function for events of TYPE, a string.
Around the BODY, the variable `event' is bound to the event being
processed, `room' to the room struct in which the event occurred,
and `session' to the session. Adds function to
`ement-event-handlers', which see."
(declare (indent defun))
`(setf (alist-get ,type ement-event-handlers nil nil #'string=)
(lambda (event room session)
,(concat "`ement-' handler function for " type " events.")
,@body)))
;; I love how Lisp macros make it so easy and concise to define these
;; event handlers!
(ement-defevent "m.room.avatar"
(when ement-room-avatars
;; If room avatars are disabled, we don't download avatars at all. This
;; means that, if a user has them disabled and then reenables them, they will
;; likely need to reconnect to cause them to be displayed in most rooms.
(if-let ((url (alist-get 'url (ement-event-content event))))
(plz-run
(plz-queue ement-images-queue
'get (ement--mxc-to-url url session) :as 'binary :noquery t
:then (lambda (data)
(when ement-room-avatars
;; MAYBE: Store the raw image data instead of using create-image here.
(let ((image (create-image data nil 'data-p
:ascent 'center
:max-width ement-room-avatar-max-width
:max-height ement-room-avatar-max-height)))
(if (not image)
(progn
(display-warning 'ement (format "Room avatar seems unreadable: ROOM-ID:%S AVATAR-URL:%S"
(ement-room-id room) (ement--mxc-to-url url session)))
(setf (ement-room-avatar room) nil
(alist-get 'room-list-avatar (ement-room-local room)) nil))
(when (fboundp 'imagemagick-types)
;; Only do this when ImageMagick is supported.
;; FIXME: When requiring Emacs 27+, remove this (I guess?).
(setf (image-property image :type) 'imagemagick))
;; We set the room-avatar slot to a propertized string that
;; displays as the image. This seems the most convenient thing to
;; do. We also unset the cached room-list-avatar so it can be
;; remade.
(setf (ement-room-avatar room) (propertize " " 'display image)
(alist-get 'room-list-avatar (ement-room-local room)) nil)))))))
;; Unset avatar.
(setf (ement-room-avatar room) nil
(alist-get 'room-list-avatar (ement-room-local room)) nil))))
(ement-defevent "m.room.create"
(ignore session)
(pcase-let* (((cl-struct ement-event (content (map type))) event))
(when type
(setf (ement-room-type room) type))))
(ement-defevent "m.room.member"
"Put/update member on `ement-users' and room's members table."
(ignore session)
(pcase-let* (((cl-struct ement-room members) room)
((cl-struct ement-event state-key
(content (map displayname membership
('avatar_url avatar-url))))
event)
(user (or (gethash state-key ement-users)
(puthash state-key
(make-ement-user :id state-key :avatar-url avatar-url
;; NOTE: The spec doesn't seem to say whether the
;; displayname in the member event applies only to the
;; room or is for the user generally, so we'll save it
;; in the struct anyway.
:displayname displayname)
ement-users))))
(pcase membership
("join"
(puthash state-key user members)
(puthash user displayname (ement-room-displaynames room)))
(_ (remhash state-key members)
(remhash user (ement-room-displaynames room))))))
(ement-defevent "m.room.name"
(ignore session)
(pcase-let* (((cl-struct ement-event (content (map name))) event))
(when name
;; Recalculate room name and cache in slot.
(setf (ement-room-display-name room) (ement--room-display-name room)))))
(ement-defevent "m.room.topic"
(ignore session)
(pcase-let* (((cl-struct ement-event (content (map topic))) event))
(when topic
(setf (ement-room-topic room) topic))))
(ement-defevent "m.receipt"
(ignore session)
(pcase-let (((cl-struct ement-event content) event)
((cl-struct ement-room (receipts room-receipts)) room))
(cl-loop for (event-id . receipts) in content
do (cl-loop for (user-id . receipt) in (alist-get 'm.read receipts)
;; Users may not have been "seen" yet, so although we'd
;; prefer to key on the user struct, we key on the user ID.
;; Same for events, unfortunately.
;; NOTE: The JSON map keys are converted to symbols by `json-read'.
;; MAYBE: (Should we keep them that way? It would use less memory, I guess.)
do (puthash (symbol-name user-id)
(cons (symbol-name event-id) (alist-get 'ts receipt))
room-receipts)))))
(ement-defevent "m.space.child"
;; SPEC: v1.2/11.35.
(pcase-let* ((space-room room)
((cl-struct ement-session rooms) session)
((cl-struct ement-room (id parent-room-id)) space-room)
((cl-struct ement-event (state-key child-room-id) (content (map via))) event)
(child-room (cl-find child-room-id rooms :key #'ement-room-id :test #'equal)))
(if via
;; Child being declared: add it.
(progn
(cl-pushnew child-room-id (alist-get 'children (ement-room-local space-room)) :test #'equal)
(when child-room
;; The user is also in the child room: link the parent space-room in it.
;; FIXME: On initial sync, if the child room hasn't been processed yet, this will fail.
(cl-pushnew parent-room-id (alist-get 'parents (ement-room-local child-room)) :test #'equal)))
;; Child being disowned: remove it.
(setf (alist-get 'children (ement-room-local space-room))
(delete child-room-id (alist-get 'children (ement-room-local space-room))))
(when child-room
;; The user is also in the child room: unlink the parent space-room in it.
(setf (alist-get 'parents (ement-room-local child-room))
(delete parent-room-id (alist-get 'parents (ement-room-local child-room))))))))
(ement-defevent "m.room.canonical_alias"
(ignore session)
(pcase-let (((cl-struct ement-event (content (map alias))) event))
(setf (ement-room-canonical-alias room) alias)))
(defun ement--link-children (session)
"Link child rooms in SESSION.
To be called after initial sync."
;; On initial sync, when processing m.space.child events, the child rooms may not have
;; been processed yet, so we link them again here.
(pcase-let (((cl-struct ement-session rooms) session))
(dolist (room rooms)
(pcase-let (((cl-struct ement-room (id parent-id) (local (map children))) room))
(when children
(dolist (child-id children)
(when-let ((child-room (cl-find child-id rooms :key #'ement-room-id :test #'equal)))
(cl-pushnew parent-id (alist-get 'parents (ement-room-local child-room)) :test #'equal))))))))
;;;; Footer
(provide 'ement)
;;; ement.el ends here
This is dockAviIV.info, produced by makeinfo version 6.7 from
ement.texi.
INFO-DIR-SECTION Emacs
START-INFO-DIR-ENTRY
* Ement: (ement). Matrix client for Emacs.
END-INFO-DIR-ENTRY
File: dockAviIV.info, Node: Top, Next: Installation, Up: (dir)
Ement.el
********
https://elpa.gnu.org/packages/ement.svg
(https://elpa.gnu.org/packages/ement.html)
Ement.el is a Matrix client for Emacs. It aims to be simple, fast,
featureful, and reliable.
Feel free to join us in the chat room:
https://img.shields.io/matrix/ement.el:matrix.org.svg?label=%23ement.el:matrix.org
(https://matrix.to/#/#ement.el:matrix.org)
* Menu:
* Installation::
* Usage::
* Rationale::
* Changelog::
* Development::
* License::
— The Detailed Node Listing —
Installation
* GNU ELPA::
* GNU Guix::
* Debian::
* Git master::
* Manual::
Usage
* Bindings::
* Tips::
* Encrypted room support through Pantalaimon::
Bindings
* Room buffers::
* Room list buffer::
* Directory buffers::
* Mentions/notifications buffers::
Tips
* Displaying symbols and emojis::
Changelog
* 0.9.2: 092.
* 0.9.1: 091.
* 0.9: 09.
* 0.8.3: 083.
* 0.8.2: 082.
* 0.8.1: 081.
* 0.8: 08.
* 0.7: 07.
* 0.6: 06.
* 0.5.2: 052.
* 0.5.1: 051.
* 0.5: 05.
* 0.4.1: 041.
* 0.4: 04.
* 0.3.1: 031.
* 0.3: 03.
* 0.2.1: 021.
* 0.2: 02.
* 0.1.4: 014.
* 0.1.3: 013.
* 0.1.2: 012.
* 0.1.1: 011.
* 0.1: 01.
Development
* Copyright Assignment::
* Matrix spec in Org format::
File: dockAviIV.info, Node: Installation, Next: Usage, Prev: Top, Up: Top
1 Installation
**************
* Menu:
* GNU ELPA::
* GNU Guix::
* Debian::
* Git master::
* Manual::
File: dockAviIV.info, Node: GNU ELPA, Next: GNU Guix, Up: Installation
1.1 GNU ELPA
============
Ement.el is published in GNU ELPA (http://elpa.gnu.org/), so it may be
installed in Emacs with the command ‘M-x package-install RET ement RET’.
This is the recommended way to install Ement.el, as it will install the
current stable release.
File: dockAviIV.info, Node: GNU Guix, Next: Debian, Prev: GNU ELPA, Up: Installation
1.2 GNU Guix
============
Ement.el is also available in GNU Guix (https://guix.gnu.org/) as
‘emacs-ement’.
File: dockAviIV.info, Node: Debian, Next: Git master, Prev: GNU Guix, Up: Installation
1.3 Debian
==========
Ement.el is also available in Debian as elpa-ement
(https://packages.debian.org/elpa-ement).
File: dockAviIV.info, Node: Git master, Next: Manual, Prev: Debian, Up: Installation
1.4 Git master
==============
The ‘master’ branch of the Git repository is intended to be usable at
all times; only minor bugs are expected to be found in it before a new
stable release is made. To install from this, it is recommended to use
quelpa-use-package (https://github.com/quelpa/quelpa-use-package), like
this:
;; Install and load `quelpa-use-package'.
(package-install 'quelpa-use-package)
(require 'quelpa-use-package)
;; Install Ement.
(use-package ement
:quelpa (ement :fetcher github :repo "alphapapa/ement.el"))
One might also use systems like Straight
(https://github.com/radian-software/straight.el) (which is also used by
DOOM (https://github.com/doomemacs/doomemacs)) to install from Git, but
the author cannot offer support for them.
File: dockAviIV.info, Node: Manual, Prev: Git master, Up: Installation
1.5 Manual
==========
Ement.el is intended to be installed with Emacs’s package system, which
will ensure that the required autoloads are generated, etc. If you
choose to install it manually, you’re on your own.
File: dockAviIV.info, Node: Usage, Next: Rationale, Prev: Installation, Up: Top
2 Usage
*******
• • •
1. Call command ‘ement-connect’ to connect. Multiple sessions are
supported, so you may call the command again to connect to another
account.
2. Wait for initial sync to complete (which can take a few
moments–initial sync JSON requests can be large).
3. Use these commands (room-related commands may be called with
universal prefix to prompt for the room):
• ‘ement-list-rooms’ to view the list of joined rooms.
• ‘ement-view-room’ to view a room’s buffer, selected with
completion.
• ‘ement-create-room’ to create a new room.
• ‘ement-create-space’ to create a space.
• ‘ement-invite-user’ to invite a user to a room.
• ‘ement-join-room’ to join a room.
• ‘ement-leave-room’ to leave a room.
• ‘ement-forget-room’ to forget a room.
• ‘ement-tag-room’ to toggle a tag on a room (including
favorite/low-priority status).
• ‘ement-list-members’ to list members in a room.
• ‘ement-send-direct-message’ to send a direct message to a user
(in an existing direct room, or creating a new one
automatically).
• ‘ement-room-edit-message’ to edit a message at point.
• ‘ement-room-send-file’ to send a file.
• ‘ement-room-send-image’ to send an image.
• ‘ement-room-set-topic’ to set a room’s topic.
• ‘ement-room-occur’ to search in a room’s known events.
• ‘ement-room-override-name’ to override a room’s display name.
• ‘ement-ignore-user’ to ignore a user (or with interactive
prefix, un-ignore).
• ‘ement-room-set-message-format’ to set a room’s message format
buffer-locally.
• ‘ement-room-toggle-space’ to toggle a room’s membership in a
space (a way to group rooms in Matrix).
• ‘ement-directory’ to view a room directory.
• ‘ement-directory-search’ to search a room directory.
4. Use these special buffers to see events from multiple rooms (you
can also reply to messages from these buffers!):
• See all new events that mention you in the ‘*Ement Mentions*’
buffer.
• See all new events in rooms that have open buffers in the
‘*Ement Notifications*’ buffer.
* Menu:
* Bindings::
* Tips::
* Encrypted room support through Pantalaimon::
File: dockAviIV.info, Node: Bindings, Next: Tips, Up: Usage
2.1 Bindings
============
These bindings are common to all of the following buffer types:
• Switch to a room buffer: ‘M-g M-r’
• Switch to the room list buffer: ‘M-g M-l’
• Switch to the mentions buffer: ‘M-g M-m’
• Switch to the notifications buffer: ‘M-g M-n’
* Menu:
* Room buffers::
* Room list buffer::
* Directory buffers::
* Mentions/notifications buffers::
File: dockAviIV.info, Node: Room buffers, Next: Room list buffer, Up: Bindings
2.1.1 Room buffers
------------------
• Show command menu: ‘?’
*Movement*
• Next event: ‘TAB’
• Previous event: ‘<backtab>’
• Scroll up and mark read: ‘SPC’
• Scroll down: ‘S-SPC’
• Jump to fully-read marker: ‘M-SPC’
• Load older messages: at top of buffer, scroll contents up (i.e.
‘S-SPC’, ‘M-v’ or ‘mwheel-scroll’)
*Switching*
• List rooms: ‘M-g M-l’
• Switch to other room: ‘M-g M-r’
• Switch to mentions buffer: ‘M-g M-m’
• Switch to notifications buffer: ‘M-g M-n’
• Quit window: ‘q’
*Messages*
• Write message: ‘RET’
• Write reply to event at point (when region is active, only quote
marked text) : ‘S-RET’
• Compose message in buffer: ‘M-RET’ (while writing in minibuffer:
‘C-c ')’ (Use command ‘ement-room-compose-org’ to activate Org mode
in the compose buffer.)
• Edit message: ‘<insert>’
• Delete message: ‘C-k’
• Send reaction to event at point, or send same reaction at point: ‘s
r’
• Send emote: ‘s e’
• Send file: ‘s f’
• Send image: ‘s i’
• View event source: ‘v’
• Complete members and rooms at point: ‘C-M-i’ (standard
‘completion-at-point’ command). (Type an ‘@’ prefix for a member
mention, a ‘#’ prefix for a room alias, or a ‘!’ prefix for a room
ID.)
*Images*
• Toggle scale of image (between fit-to-window and thumbnail):
‘mouse-1’
• Show image in new buffer at full size: ‘double-mouse-1’
*Users*
• Send direct message: ‘u RET’
• Invite user: ‘u i’
• Ignore user: ‘u I’
*Room*
• Occur search in room: ‘M-s o’
• List members: ‘r m’
• Set topic: ‘r t’
• Set message format: ‘r f’
• Set notification rules: ‘r n’
• Override display name: ‘r N’
• Tag/untag room: ‘r T’
*Room membership*
• Create room: ‘R c’
• Join room: ‘R j’
• Leave room: ‘R l’
• Forget room: ‘R F’
• Toggle room’s spaces: ‘R s’
*Other*
• Sync new messages (not necessary if auto sync is enabled; with
prefix to force new sync): ‘g’
File: dockAviIV.info, Node: Room list buffer, Next: Directory buffers, Prev: Room buffers, Up: Bindings
2.1.2 Room list buffer
----------------------
• Show buffer of room at point: ‘RET’
• Show buffer of next unread room: ‘SPC’
• Move between room names: ‘TAB’ / ‘<backtab>’
• Kill room’s buffer: ‘k’
• Toggle room’s membership in a space: ‘s’
File: dockAviIV.info, Node: Directory buffers, Next: Mentions/notifications buffers, Prev: Room list buffer, Up: Bindings
2.1.3 Directory buffers
-----------------------
• View/join a room: ‘RET’ / ‘mouse-1’
• Load next batch of rooms: ‘+’
File: dockAviIV.info, Node: Mentions/notifications buffers, Prev: Directory buffers, Up: Bindings
2.1.4 Mentions/notifications buffers
------------------------------------
• Move between events: ‘TAB’ / ‘<backtab>’
• Go to event at point in its room buffer: ‘RET’
• Write reply to event at point (shows the event in its room while
writing) : ‘S-RET’
File: dockAviIV.info, Node: Tips, Next: Encrypted room support through Pantalaimon, Prev: Bindings, Up: Usage
2.2 Tips
========
• Desktop notifications are enabled by default for events that
mention the local user. They can also be shown for all events in
rooms with open buffers.
• Send messages in Org mode format by customizing the option
‘ement-room-send-message-filter’ (which enables Org format by
default), or by calling ‘ement-room-compose-org’ in a compose
buffer (which enables it for a single message). Then Org-formatted
messages are automatically converted and sent as HTML-formatted
messages (with the Org syntax as the plain-text fallback). You can
send syntax such as:
• Bold, italic, underline, strikethrough
• Links
• Tables
• Source blocks (including results with ‘:exports both’)
• Footnotes (okay, that might be pushing it, but you can!)
• And, generally, anything that Org can export to HTML
• Starting in the room list buffer, by pressing ‘SPC’ repeatedly, you
can cycle through and read all rooms with unread buffers. (If a
room doesn’t have a buffer, it will not be included.)
• Room buffers and the room-list buffer can be bookmarked in Emacs,
i.e. using ‘C-x r m’. This is especially useful with Burly
(https://github.com/alphapapa/burly.el): you can arrange an Emacs
frame with several room buffers displayed at once, use
‘burly-bookmark-windows’ to bookmark the layout, and then you can
restore that layout and all of the room buffers by opening the
bookmark, rather than having to manually arrange them every time
you start Emacs or change the window configuration.
• Images and other files can be uploaded to rooms using
drag-and-drop.
• Mention members by typing a ‘@’ followed by their displayname or
Matrix ID. (Members’ names and rooms’ aliases/IDs may be completed
with ‘completion-at-point’ commands.)
• You can customize settings in the ‘ement’ group.
• *Note:* ‘setq’ should not be used for certain options, because
it will not call the associated setter function. Users who
have an aversion to the customization system may experience
problems.
* Menu:
* Displaying symbols and emojis::
File: dockAviIV.info, Node: Displaying symbols and emojis, Up: Tips
2.2.1 Displaying symbols and emojis
-----------------------------------
Emacs may not display certain symbols and emojis well by default. Based
on this question and answer
(https://emacs.stackexchange.com/questions/62049/override-the-default-font-for-emoji-characters),
you may find that the simplest way to fix this is to install an
appropriate font, like Noto Emoji
(https://www.google.com/get/noto/#emoji-zsye), and then use this Elisp
code:
(setf use-default-font-for-symbols nil)
(set-fontset-font t 'unicode "Noto Emoji" nil 'append)
File: dockAviIV.info, Node: Encrypted room support through Pantalaimon, Prev: Tips, Up: Usage
2.3 Encrypted room support through Pantalaimon
==============================================
Ement.el doesn’t support encrypted rooms natively, but it can be used
transparently with the E2EE-aware reverse proxy daemon Pantalaimon
(https://github.com/matrix-org/pantalaimon/). After configuring it
according to its documentation, call ‘ement-connect’ with the
appropriate hostname and port, like:
(ement-connect :uri-prefix "http://localhost:8009")
File: dockAviIV.info, Node: Rationale, Next: Changelog, Prev: Usage, Up: Top
3 Rationale
***********
Why write a new Emacs Matrix client when there is already
matrix-client.el (https://github.com/alphapapa/matrix-client.el), by the
same author, no less? A few reasons:
• ‘matrix-client’ uses an older version of the Matrix spec, r0.3.0,
with a few elements of r0.4.0 grafted in. Bringing it up to date
with the current version of the spec, r0.6.1, would be more work
than to begin with the current version. Ement.el targets r0.6.1
from the beginning.
• ‘matrix-client’ does not use Matrix’s lazy-loading feature (which
was added to the specification later), so initial sync requests can
take a long time for the server to process and can be large
(sometimes tens of megabytes of JSON for the client to process!).
Ement.el uses lazy-loading, which significantly improves
performance.
• ‘matrix-client’ automatically makes buffers for every room a user
has joined, even if the user doesn’t currently want to watch a
room. Ement.el opens room buffers on-demand, improving performance
by not having to insert events into buffers for rooms the user
isn’t watching.
• ‘matrix-client’ was developed without the intention of publishing
it to, e.g. MELPA or ELPA. It has several dependencies, and its
code does not always install or compile cleanly due to
macro-expansion issues (apparently depending on the user’s Emacs
config). Ement.el is designed to have minimal dependencies outside
of Emacs (currently only one, ‘plz’, which could be imported into
the project), and every file is linted and compiles cleanly using
makem.sh (https://github.com/alphapapa/makem.sh).
• ‘matrix-client’ uses EIEIO, probably unnecessarily, since few, if
any, of the benefits of EIEIO are realized in it. Ement.el uses
structs instead.
• ‘matrix-client’ uses bespoke code for inserting messages into
buffers, which works pretty well, but has a few minor bugs which
are difficult to track down. Ement.el uses Emacs’s built-in (and
perhaps little-known) ‘ewoc’ library, which makes it much simpler
and more reliable to insert and update messages in buffers, and
enables the development of advanced UI features more easily.
• ‘matrix-client’ was, to a certain extent, designed to imitate other
messaging apps. The result is, at least when used with the
‘matrix-client-frame’ command, fairly pleasing to use, but isn’t
especially "Emacsy." Ement.el is intended to better fit into
Emacs’s paradigms.
• ‘matrix-client’’s long name makes for long symbol names, which
makes for tedious, verbose code. ‘ement’ is easy to type and makes
for concise, readable code.
• The author has learned much since writing ‘matrix-client’ and hopes
to write simpler, more readable, more maintainable code in
Ement.el. It’s hoped that this will enable others to contribute
more easily.
Note that, while ‘matrix-client’ remains usable, and probably will
for some time to come, Ement.el has now surpassed it in every way. The
only reason to choose ‘matrix-client’ instead is if one is using an
older version of Emacs that isn’t supported by Ement.el.
File: dockAviIV.info, Node: Changelog, Next: Development, Prev: Rationale, Up: Top
4 Changelog
***********
* Menu:
* 0.9.2: 092.
* 0.9.1: 091.
* 0.9: 09.
* 0.8.3: 083.
* 0.8.2: 082.
* 0.8.1: 081.
* 0.8: 08.
* 0.7: 07.
* 0.6: 06.
* 0.5.2: 052.
* 0.5.1: 051.
* 0.5: 05.
* 0.4.1: 041.
* 0.4: 04.
* 0.3.1: 031.
* 0.3: 03.
* 0.2.1: 021.
* 0.2: 02.
* 0.1.4: 014.
* 0.1.3: 013.
* 0.1.2: 012.
* 0.1.1: 011.
* 0.1: 01.
File: dockAviIV.info, Node: 092, Next: 091, Up: Changelog
4.1 0.9.2
=========
*Fixes*
• Restore position in room list when refreshing.
• Completion in minibuffer.
File: dockAviIV.info, Node: 091, Next: 09, Prev: 092, Up: Changelog
4.2 0.9.1
=========
*Fixes*
• Error in ‘ement-room-list’ command upon initial sync.
File: dockAviIV.info, Node: 09, Next: 083, Prev: 091, Up: Changelog
4.3 0.9
=======
*Additions*
• Option ‘ement-room-timestamp-header-align’ controls how timestamp
headers are aligned in room buffers.
• Option ‘ement-room-view-hook’ runs functions when ‘ement-room-view’
is called. (By default, it refreshes the room list buffer.)
• In the room list, middle-clicking a room which has a buffer closes
its buffer.
• Basic support for video events. (Thanks to Arto Jantunen
(https://github.com/viiru-).)
*Changes*
• Using new option ‘ement-room-timestamp-header-align’, timestamp
headers default to right-aligned. (With default settings, this
keeps them near message timestamps and makes for a cleaner
appearance.)
*Fixes*
• Recognition of certain MXID or displayname forms in outgoing
messages when linkifying (aka "pilling") them.
• Unreadable room avatar images no longer cause errors. (Fixes #147
(https://github.com/alphapapa/ement.el/issues/147). Thanks to
@jgarte (https://github.com/jgarte) for reporting.)
• Don’t error in ‘ement-room-list’ when no rooms are joined. (Fixes
#123 (https://github.com/alphapapa/ement.el/issues/123). Thanks to
@Kabouik (https://github.com/Kabouik) and Omar Antolín Camarena
(https://github.com/oantolin) for reporting.)
• Enable member/room completion in compose buffers. (Fixes #115
(https://github.com/alphapapa/ement.el/issues/115). Thanks to
Thanks to Justus Piater (https://github.com/piater) and Caleb Chase
(https://github.com/chasecaleb) for reporting.)
File: dockAviIV.info, Node: 083, Next: 082, Prev: 09, Up: Changelog
4.4 0.8.3
=========
*Fixes*
• Avoid use of ‘pcase’’s ‘(map :KEYWORD)’ form. (This can cause a
broken installation on older versions of Emacs that have an older
version of the ‘map’ library loaded, such as Emacs 27.2 included in
Debian 11. Since there’s no way to force Emacs to actually load
the version of ‘map’ required by this package before installing it
(which would naturally happen upon restarting Emacs), we can only
avoid using such forms while these versions of Emacs are widely
used.)
File: dockAviIV.info, Node: 082, Next: 081, Prev: 083, Up: Changelog
4.5 0.8.2
=========
*Fixes*
• Deduplicate grouped membership events.
File: dockAviIV.info, Node: 081, Next: 08, Prev: 082, Up: Changelog
4.6 0.8.1
=========
Added missing changelog entry (of course).
File: dockAviIV.info, Node: 08, Next: 07, Prev: 081, Up: Changelog
4.7 0.8
=======
*Additions*
• Command ‘ement-create-space’ creates a new space.
• Command ‘ement-room-toggle-space’ toggles a room’s membership in a
space (a way to group rooms in Matrix).
• Visibility of sections in the room list is saved across sessions.
• Command ‘ement-room-list-kill-buffer’ kills a room’s buffer from
the room list.
• Set ‘device_id’ and ‘initial_device_display_name’ upon login (e.g.
‘Ement.el: username@hostname’). (#134
(https://github.com/alphapapa/ement.el/issues/134). Thanks to Arto
Jantunen (https://github.com/viiru-) for reporting.)
*Changes*
• Room-related commands may be called interactively with a universal
prefix to prompt for the room/session (allowing to send events or
change settings in rooms other than the current one).
• Command ‘ement-room-list’ reuses an existing window showing the
room list when possible. (#131
(https://github.com/alphapapa/ement.el/issues/131). Thanks to Jeff
Bowman (https://github.com/jeffbowman) for suggesting.)
• Command ‘ement-tag-room’ toggles tags (rather than adding by
default and removing when called with a prefix).
• Default room grouping now groups "spaced" rooms separately.
*Fixes*
• Message format filter works properly when writing replies.
• Improve insertion of sender name headers when using the "Elemental"
message format.
• Prompts in commands ‘ement-leave-room’ and ‘ement-forget-room’.
File: dockAviIV.info, Node: 07, Next: 06, Prev: 08, Up: Changelog
4.8 0.7
=======
*Additions*
• Command ‘ement-room-override-name’ sets a local override for a
room’s display name. (Especially helpful for 1:1 rooms and bridged
rooms. See MSC3015
(https://github.com/matrix-org/matrix-spec-proposals/pull/3015#issuecomment-1451017296).)
*Changes*
• Improve display of room tombstones (displayed at top and bottom of
buffer, and new room ID is linked to join).
• Use descriptive prompts in ‘ement-leave-room’ and
‘ement-forget-room’ commands.
*Fixes*
• Command ‘ement-view-space’ when called from a room buffer. (Thanks
to Richard Brežák (https://github.com/MagicRB) for reporting.)
• Don’t call ‘display-buffer’ when reverting room list buffer.
(Fixes #121 (https://github.com/alphapapa/ement.el/issues/121).
Thanks to mekeor (https://github.com/mekeor) for reporting.)
• Retry sync for network timeouts. (Accidentally broken in v0.6.)
*Internal*
• Function ‘ement-put-account-data’ accepts ‘:room’ argument to put
on a room’s account data.
File: dockAviIV.info, Node: 06, Next: 052, Prev: 07, Up: Changelog
4.9 0.6
=======
*Additions*
• Command ‘ement-view-space’ to view a space’s rooms in a directory
buffer.
*Changes*
• Improve ‘ement-describe-room’ command (formatting, bindings).
*Fixes*
• Retry sync for HTTP 502 "Bad Gateway" errors.
• Formatting of unban events.
• Update password authentication according to newer Matrix spec.
(Fixes compatibility with Conduit servers. #66
(https://github.com/alphapapa/ement.el/issues/66). Thanks to
Travis Peacock (https://github.com/tpeacock19), Arto Jantunen
(https://github.com/viiru-), and Stephen D
(https://github.com/scd31).)
• Image scaling issues. (Thanks to Visuwesh
(https://github.com/vizs).)
File: dockAviIV.info, Node: 052, Next: 051, Prev: 06, Up: Changelog
4.10 0.5.2
==========
*Fixes*
• Apply ‘ement-initial-sync-timeout’ properly (important for when the
homeserver is slow to respond).
File: dockAviIV.info, Node: 051, Next: 05, Prev: 052, Up: Changelog
4.11 0.5.1
==========
*Fixes*
• Autoload ‘ement-directory’ commands.
• Faces in ‘ement-directory’ listings.
File: dockAviIV.info, Node: 05, Next: 041, Prev: 051, Up: Changelog
4.12 0.5
========
*Additions*
• Present "joined-and-left" and "rejoined-and-left" membership event
pairs as such.
• Process and show rooms’ canonical alias events.
*Changes*
• The taxy.el (https://github.com/alphapapa/taxy.el)-based room list,
with programmable, smart grouping, is now the default
‘ement-room-list’. (The old, ‘tabulated-list-mode’-based room list
is available as ‘ement-tabulated-room-list’.)
• When selecting a room to view with completion, don’t offer spaces.
• When selecting a room with completion, empty aliases and topics are
omitted instead of being displayed as nil.
*Fixes*
• Use of send-message filter when replying.
• Replies may be written in compose buffers.
File: dockAviIV.info, Node: 041, Next: 04, Prev: 05, Up: Changelog
4.13 0.4.1
==========
*Fixes*
• Don’t show "curl process interrupted" message when updating a read
marker’s position again.
File: dockAviIV.info, Node: 04, Next: 031, Prev: 041, Up: Changelog
4.14 0.4
========
*Additions*
• Option ‘ement-room-unread-only-counts-notifications’, now enabled
by default, causes rooms’ unread status to be determined only by
their notification counts (which are set by the server and depend
on rooms’ notification settings).
• Command ‘ement-room-set-notification-state’ sets a room’s
notification state (imitating Element’s user-friendly presets).
• Room buffers’ Transient menus show the room’s notification state
(imitating Element’s user-friendly presets).
• Command ‘ement-set-display-name’ sets the user’s global
displayname.
• Command ‘ement-room-set-display-name’ sets the user’s displayname
in a room (which is also now displayed in the room’s Transient
menu).
• Column ‘Notifications’ in the ‘ement-taxy-room-list’ buffer shows
rooms’ notification state.
• Option ‘ement-interrupted-sync-hook’ allows customization of how
sync interruptions are handled. (Now, by default, a warning is
displayed instead of merely a message.)
*Changes*
• When a room’s read receipt is updated, the room’s buffer is also
marked as unmodified. (In concert with the new option, this makes
rooms’ unread status more intuitive.)
*Fixes*
• Binding of command ‘ement-forget-room’ in room buffers.
• Highlighting of ‘@room’ mentions.
File: dockAviIV.info, Node: 031, Next: 03, Prev: 04, Up: Changelog
4.15 0.3.1
==========
*Fixes*
• Room unread status (when the last event in a room is sent by the
local user, the room is considered read).
File: dockAviIV.info, Node: 03, Next: 021, Prev: 031, Up: Changelog
4.16 0.3
========
*Additions*
• Command ‘ement-directory’ shows a server’s room directory.
• Command ‘ement-directory-search’ searches a server’s room
directory.
• Command ‘ement-directory-next’ fetches the next batch of rooms in a
directory.
• Command ‘ement-leave-room’ accepts a ‘FORCE-P’ argument
(interactively, with prefix) to leave a room without prompting.
• Command ‘ement-forget-room’ accepts a ‘FORCE-P’ argument
(interactively, with prefix) to also leave the room, and to forget
it without prompting.
• Option ‘ement-notify-mark-frame-urgent-predicates’ marks the frame
as urgent when (by default) a message mentions the local user or
"@room" and the message’s room has an open buffer.
*Changes*
• Minor improvements to date/time headers.
*Fixes*
• Command ‘ement-describe-room’ for rooms without topics.
• Improve insertion of old messages around existing timestamp
headers.
• Reduce D-Bus notification system check timeout to 2 seconds (from
the default of 25).
• Compatibility with Emacs 27.
File: dockAviIV.info, Node: 021, Next: 02, Prev: 03, Up: Changelog
4.17 0.2.1
==========
*Fixes*
• Info manual export filename.
File: dockAviIV.info, Node: 02, Next: 014, Prev: 021, Up: Changelog
4.18 0.2
========
*Changes*
• Read receipts are re-enabled. (They’re now implemented with a
global idle timer rather than ‘window-scroll-functions’, which
sometimes caused a strange race condition that could cause Emacs to
become unresponsive or crash.)
• When determining whether a room is considered unread, non-message
events like membership changes, reactions, etc. are ignored. This
fixes a bug that caused certain rooms that had no message events
(like some bridged rooms) to appear as unread when they shouldn’t
have. But it’s unclear whether this is always preferable (e.g.
one might want a member leaving a room to cause it to be marked
unread), so this is classified as a change rather than simply a
fix, and more improvements may be made to this in the future.
(Fixes #97 (https://github.com/alphapapa/ement.el/issues/97).
Thanks to Julien Roy (https://github.com/MrRoy) for reporting and
testing.)
• The ‘ement-taxy-room-list’ view no longer automatically refreshes
the list if the region is active in the buffer. (This allows the
user to operate on multiple rooms without the contents of the
buffer changing before completing the process.)
*Fixes*
• Links to only rooms (as opposed to links to events in rooms) may be
activated to join them.
• Read receipts mark the last completely visible event (rather than
one that’s only partially displayed).
• Prevent error when a room avatar image fails to load.
File: dockAviIV.info, Node: 014, Next: 013, Prev: 02, Up: Changelog
4.19 0.1.4
==========
*Fixed*
• Info manual directory headers.
File: dockAviIV.info, Node: 013, Next: 012, Prev: 014, Up: Changelog
4.20 0.1.3
==========
*Fixed*
• Temporarily disable sending of read receipts due to an unusual bug
that could cause Emacs to become unresponsive. (The feature will
be re-enabled in a future release.)
File: dockAviIV.info, Node: 012, Next: 011, Prev: 013, Up: Changelog
4.21 0.1.2
==========
*Fixed*
• Function ‘ement-room-sync’ correctly updates room-list buffers.
(Thanks to Visuwesh (https://github.com/vizs).)
• Only send D-Bus notifications when supported. (Fixes #83
(https://github.com/alphapapa/ement.el/issues/83). Thanks to
Tassilo Horn (https://github.com/tsdh).)
File: dockAviIV.info, Node: 011, Next: 01, Prev: 012, Up: Changelog
4.22 0.1.1
==========
*Fixed*
• Function ‘ement-room-scroll-up-mark-read’ selects the correct room
window.
• Option ‘ement-room-list-avatars’ defaults to what function
‘display-images-p’ returns.
File: dockAviIV.info, Node: 01, Prev: 011, Up: Changelog
4.23 0.1
========
After almost two years of development, the first tagged release.
Submitted to GNU ELPA.
File: dockAviIV.info, Node: Development, Next: License, Prev: Changelog, Up: Top
5 Development
*************
Bug reports, feature requests, suggestions — _oh my_!
* Menu:
* Copyright Assignment::
* Matrix spec in Org format::
File: dockAviIV.info, Node: Copyright Assignment, Next: Matrix spec in Org format, Up: Development
5.1 Copyright Assignment
========================
Ement.el is published in GNU ELPA and is considered part of GNU Emacs.
Therefore, cumulative contributions of more than 15 lines of code
require that the author assign copyright of such contributions to the
FSF. Authors who are interested in doing so may contact assign@gnu.org
<assign@gnu.org> to request the appropriate form.
File: dockAviIV.info, Node: Matrix spec in Org format, Prev: Copyright Assignment, Up: Development
5.2 Matrix spec in Org format
=============================
An Org-formatted version of the Matrix spec is available in the
meta/spec (https://github.com/alphapapa/ement.el/tree/meta/spec) branch.
File: dockAviIV.info, Node: License, Prev: Development, Up: Top
6 License
*********
GPLv3
Tag Table:
Node: Top188
Node: Installation1434
Node: GNU ELPA1619
Node: GNU Guix1968
Node: Debian2173
Node: Git master2384
Node: Manual3275
Node: Usage3571
Node: Bindings6207
Node: Room buffers6679
Node: Room list buffer9084
Node: Directory buffers9492
Node: Mentions/notifications buffers9763
Node: Tips10157
Node: Displaying symbols and emojis12583
Node: Encrypted room support through Pantalaimon13210
Node: Rationale13774
Node: Changelog17205
Node: 09217625
Node: 09117806
Node: 0917976
Node: 08319652
Node: 08220288
Node: 08120441
Node: 0820581
Node: 0722215
Node: 0623399
Node: 05224204
Node: 05124427
Node: 0524630
Node: 04125482
Node: 0425696
Node: 03127218
Node: 0327443
Node: 02128677
Node: 0228819
Node: 01430458
Node: 01330603
Node: 01230898
Node: 01131312
Node: 0131617
Node: Development31788
Node: Copyright Assignment32027
Node: Matrix spec in Org format32513
Node: License32817
End Tag Table
Local Variables:
coding: utf-8
End:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xml:space="preserve"
id="svg4768"
viewBox="0.171 0.201 512 512"
height="48"
width="48"
version="1.0"
inkscape:version="0.48.4 r9939"
sodipodi:docname="logo.svg"
inkscape:export-filename="/home/me/src/emacs/ement.el/images/logo64.png"
inkscape:export-xdpi="120"
inkscape:export-ydpi="120"><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1173"
id="namedview72"
showgrid="false"
inkscape:zoom="6.9532167"
inkscape:cx="63.22113"
inkscape:cy="9.8428958"
inkscape:window-x="1920"
inkscape:window-y="6"
inkscape:window-maximized="1"
inkscape:current-layer="svg4768" /><metadata
id="metadata70"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><!-- Gnu Emacs Icon
Copyright (C) 2008-2017 Free Software Foundation, Inc.
Author: Nicolas Petton <nicolas@petton.fr>
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 <http://www.gnu.org/licenses/>.
--><!-- Created with Inkscape (http://www.inkscape.org/) --><defs
id="defs4770"><linearGradient
id="linearGradient3961"><stop
id="stop3963"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" /><stop
id="stop3965"
offset="1"
style="stop-color:#0dbd8b;stop-opacity:1;" /></linearGradient><linearGradient
id="linearGradient3874"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1;"
id="stop3876" /><stop
id="stop3882"
style="stop-color:#000000;stop-opacity:1;"
offset="1" /></linearGradient><linearGradient
id="linearGradient3866"><stop
style="stop-color:#00ff18;stop-opacity:1;"
offset="0"
id="stop3868" /><stop
style="stop-color:#000000;stop-opacity:1;"
offset="1"
id="stop3870" /></linearGradient><linearGradient
id="linearGradient3817"><stop
id="stop3819"
style="stop-color:#00ff0a;stop-opacity:1;"
offset="0" /><stop
id="stop3823"
style="stop-color:#000000;stop-opacity:0.99215686;"
offset="1" /></linearGradient><linearGradient
id="linearGradient4292"><stop
id="stop4294"
offset="0"
style="stop-color:#411f5d;stop-opacity:1" /><stop
id="stop4296"
offset="1"
style="stop-color:#5b2a85;stop-opacity:1" /></linearGradient><linearGradient
id="linearGradient4284"><stop
offset="0"
style="stop-color:#8381c5;stop-opacity:1"
id="stop4286" /><stop
id="stop4290"
style="stop-color:#7e55b3;stop-opacity:0.99607843"
offset="0.56639391" /><stop
offset="1"
style="stop-color:#a52ecb;stop-opacity:0.99215686"
id="stop4288" /></linearGradient><linearGradient
id="linearGradient4898"><stop
id="stop4278"
style="stop-color:#bab8db;stop-opacity:1"
offset="0" /><stop
id="stop4280"
style="stop-color:#5955a9;stop-opacity:0.99159664"
offset="1" /></linearGradient><linearGradient
id="linearGradient3294"><stop
offset="0"
style="stop-color:#6376e6;stop-opacity:1"
id="stop3296" /><stop
offset="0.50094414"
style="stop-color:#222989;stop-opacity:1"
id="stop3302" /><stop
offset="1"
style="stop-color:#00003d;stop-opacity:1"
id="stop3298" /></linearGradient><linearGradient
id="linearGradient3284"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1"
id="stop3286" /><stop
offset="0.84845906"
style="stop-color:#000000;stop-opacity:0.49803922"
id="stop3292" /><stop
offset="1"
style="stop-color:#000000;stop-opacity:0"
id="stop3288" /></linearGradient><linearGradient
id="linearGradient3274"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1"
id="stop3276" /><stop
offset="1"
style="stop-color:#000000;stop-opacity:0"
id="stop3278" /></linearGradient><linearGradient
id="linearGradient3262"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1"
id="stop3264" /><stop
offset="1"
style="stop-color:#000000;stop-opacity:0"
id="stop3266" /></linearGradient><linearGradient
id="linearGradient3242"><stop
offset="0"
style="stop-color:#282828;stop-opacity:1"
id="stop3244" /><stop
offset="0.39253417"
style="stop-color:#808080;stop-opacity:1"
id="stop3252" /><stop
offset="1"
style="stop-color:#d9d9d9;stop-opacity:1"
id="stop3246" /></linearGradient><linearGradient
id="linearGradient3202"><stop
offset="0"
style="stop-color:#2b2b2b;stop-opacity:1"
id="stop3204" /><stop
offset="0.5"
style="stop-color:#828383;stop-opacity:1"
id="stop3250" /><stop
offset="1"
style="stop-color:#dadbdb;stop-opacity:1"
id="stop3206" /></linearGradient><linearGradient
id="linearGradient4966"><stop
offset="0"
style="stop-color:#b6b3d8;stop-opacity:1"
id="stop4968" /><stop
offset="1"
style="stop-color:#b6b3d8;stop-opacity:0"
id="stop4970" /></linearGradient><linearGradient
id="linearGradient4938"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1"
id="stop4940" /><stop
offset="1"
style="stop-color:#000000;stop-opacity:0"
id="stop4942" /></linearGradient><linearGradient
id="linearGradient4282"><stop
offset="0"
style="stop-color:#bab8db;stop-opacity:1"
id="stop4900" /><stop
offset="1"
style="stop-color:#5955a9;stop-opacity:0.99159664"
id="stop4902" /></linearGradient><linearGradient
id="linearGradient4876"><stop
offset="0"
style="stop-color:#d3d2e8;stop-opacity:1"
id="stop4878" /><stop
offset="1"
style="stop-color:#5955a9;stop-opacity:0.99159664"
id="stop4880" /></linearGradient><radialGradient
gradientTransform="matrix(0.6817439,0,0,0.5905355,-3.8523706,-28.935273)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient4898"
id="radialGradient4892"
fy="-108.96888"
fx="20.951529"
r="266.76535"
cy="-108.96888"
cx="20.951529" /><radialGradient
gradientTransform="matrix(1,0,0,0.1854103,0,383.88493)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient4938"
id="radialGradient4944"
fy="471.26172"
fx="233.8876"
r="170.49393"
cy="471.26172"
cx="233.8876" /><radialGradient
gradientTransform="matrix(1,0,0,0.9121621,0,32.654948)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient4966"
id="radialGradient4972"
fy="371.76376"
fx="299.70135"
r="76.696358"
cy="371.76376"
cx="299.70135" /><radialGradient
gradientTransform="matrix(0.414705,0.3300575,-0.5059004,0.6356454,346.95314,49.479585)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3202"
id="radialGradient3210"
fy="390.45248"
fx="289.44067"
r="17.67668"
cy="390.45248"
cx="289.44067" /><radialGradient
gradientTransform="matrix(0.414705,0.3300575,-0.5059004,0.6356454,448.41009,-65.398074)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3202"
id="radialGradient3238"
fy="382.14804"
fx="283.50717"
r="17.67668"
cy="382.14804"
cx="283.50717" /><radialGradient
gradientTransform="matrix(-6.5565014e-2,-5.9721765e-2,1.6871024,-1.8521705,171.90774,540.51473)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3242"
id="radialGradient3248"
fy="181.18982"
fx="418.45551"
r="63.068935"
cy="181.18982"
cx="418.45551" /><radialGradient
gradientTransform="matrix(0.4055116,-3.3440123e-2,0.1034174,4.3988695,177.23251,-1191.6649)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3262"
id="radialGradient3268"
fy="357.33591"
fx="354.51709"
r="33.712105"
cy="357.33591"
cx="354.51709" /><radialGradient
gradientTransform="matrix(-0.1339874,-0.1146812,0.3079048,-0.3597394,444.23592,395.03849)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3274"
id="radialGradient3280"
fy="223.55537"
fx="510.58469"
r="132.28336"
cy="223.55537"
cx="510.58469" /><radialGradient
gradientTransform="matrix(-1.2497569,1.3798305,-9.6289463e-2,-7.2974479e-2,674.3826,-70.590682)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3284"
id="radialGradient3290"
fy="-158.17821"
fx="284.4671"
r="110.2972"
cy="-158.17821"
cx="284.4671" /><radialGradient
gradientTransform="matrix(-0.1008165,-8.0872321e-2,1.0745309,-1.3395252,13.843287,784.79288)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3294"
id="radialGradient3300"
fy="356.62274"
fx="425.51019"
r="143.34167"
cy="356.62274"
cx="425.51019" /><filter
height="1.088351"
y="-0.044175496"
width="1.0892536"
x="-0.044626798"
id="filter4350"
style="color-interpolation-filters:sRGB"><feGaussianBlur
id="feGaussianBlur4352"
stdDeviation="8.7848425" /></filter><linearGradient
y2="300.73987"
x2="236.61363"
y1="-161.8512"
x1="-122.20192"
spreadMethod="pad"
gradientTransform="matrix(0.87385837,0,0,0.82818057,246.00762,250.28138)"
gradientUnits="userSpaceOnUse"
id="linearGradient4245"
xlink:href="#linearGradient3817" /><linearGradient
y2="66.018341"
x2="173.94518"
y1="396.6066"
x1="447.80933"
gradientTransform="matrix(0.98684959,0,0,0.98684959,3.0344187,2.5250397)"
gradientUnits="userSpaceOnUse"
id="linearGradient4247"
xlink:href="#linearGradient4292" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3961"
id="radialGradient3959"
cx="1136.9111"
cy="38.175797"
fx="1136.9111"
fy="38.175797"
r="233.11514"
gradientTransform="matrix(1,0,0,1.010216,-880.74005,217.63519)"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3961"
id="radialGradient3973"
cx="255.12297"
cy="256.89456"
fx="255.12297"
fy="256.89456"
r="239.78181"
gradientTransform="matrix(1,0,0,1.009932,0,-2.5514676)"
gradientUnits="userSpaceOnUse" /></defs><rect
style="fill:none;display:none"
id="rect4772"
y="0.20100001"
x="0.171"
height="512"
width="512" /><g
style="display:none"
id="g4788"><g
style="display:inline"
id="g4790" /></g><g
style="display:none"
id="g4806"><g
style="display:inline"
id="g4808"><path
style="fill:#050505;display:none"
id="path4810"
d="M 349.098,256.651 C 348.833,256.397 386.735,284.256 388.519,281.663 C 394.881,272.411 470.565,188.526 473.303,165.427 C 473.545,163.424 472.787,161.331 472.787,161.331 C 472.787,161.331 471.597,161.187 466.462,157.017 C 463.77,154.825 460.979,152.436 460.979,152.436 C 444.925,153.434 403.094,193.995 349.917,256.004" /></g></g><path
d="m 488.23812,256.89456 c 0,130.06121 -104.3692,235.49665 -233.1151,235.49665 -128.7459,0 -233.115201,-105.43544 -233.115201,-235.49665 0,-130.06123 104.369301,-235.49666 233.115201,-235.49666 128.7459,0 233.1151,105.43543 233.1151,235.49666 z"
id="path4235"
style="fill:url(#radialGradient3973);fill-opacity:1.0;stroke:#000000;stroke-width:13.33333301999999954;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
inkscape:connector-curvature="0" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.64471179;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -862.34934,-104.26336 c 0,-16.20664 13.138,-29.34464 29.34471,-29.34464 108.04422,0 195.63103,87.586807 195.63103,195.631095 0,16.206636 -13.138,29.344642 -29.34464,29.344642 -16.20664,0 -29.34464,-13.138006 -29.34464,-29.344642 0,-75.631014 -61.3108,-136.941754 -136.94175,-136.941754 -16.20671,0 -29.34471,-13.138067 -29.34471,-29.344701 z"
id="path4" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.64471179;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -725.40758,326.12504 c 0,16.20664 -13.13801,29.34464 -29.34464,29.34464 -108.04429,0 -195.63112,-87.58681 -195.63112,-195.63103 0,-16.20664 13.138,-29.34471 29.3447,-29.34471 16.2066,0 29.3447,13.13807 29.3447,29.34471 0,75.63095 61.3107,136.94175 136.94172,136.94175 16.20663,0 29.34464,13.138 29.34464,29.34464 z"
id="path6" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.64471179;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -1009.0726,179.40172 c -16.2067,0 -29.3447,-13.13801 -29.3447,-29.34464 0,-108.044293 87.58676,-195.631101 195.6311,-195.631101 16.20663,0 29.34464,13.138002 29.34464,29.344639 0,16.20663681 -13.13801,29.344702 -29.34464,29.344702 -75.63104,0 -136.94174,61.310741 -136.94174,136.94176 0,16.20663 -13.1381,29.34464 -29.34466,29.34464 z"
id="path8" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.64471179;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -578.68426,42.459959 c 16.20664,0 29.34464,13.138002 29.34464,29.344639 0,108.044292 -87.58681,195.631102 -195.63103,195.631102 -16.20664,0 -29.34471,-13.138 -29.34471,-29.34464 0,-16.20664 13.13807,-29.34464 29.34471,-29.34464 75.63095,0 136.94175,-61.3108 136.94175,-136.941822 0,-16.206637 13.138,-29.344639 29.34464,-29.344639 z"
id="path10" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.23561016;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -258.96103,188.38006 c -5.92272,0 -10.72401,-4.80129 -10.72401,-10.72404 0,-39.48479 32.00863,-71.49343 71.49346,-71.49343 5.92272,0 10.72401,4.80129 10.72401,10.72401 0,5.92273 -4.80129,10.72401 -10.72401,10.72401 -27.63938,0 -50.04542,22.40606 -50.04542,50.04541 0,5.92275 -4.80131,10.72404 -10.72403,10.72404 z"
id="path4-3" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.23561016;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -187.33996,241.78363 c 0,5.92274 -4.80129,10.72402 -10.72401,10.72402 -39.48482,0 -71.49346,-32.00861 -71.49346,-71.49346 0,-5.92272 4.80129,-10.72401 10.72401,-10.72401 5.92273,0 10.72403,4.80129 10.72403,10.72401 0,27.63939 22.40604,50.04541 50.04542,50.04541 5.92272,0 10.72401,4.80133 10.72401,10.72402 z"
id="path8-7" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.23561016;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -258.74453,313.27152 c -5.92272,0 -10.72401,-4.80129 -10.72401,-10.72401 0,-39.48483 32.00863,-71.49347 71.49344,-71.49347 5.92272,0 10.72403,4.80129 10.72403,10.72401 0,5.92273 -4.80131,10.72402 -10.72403,10.72402 -27.63937,0 -50.04542,22.40605 -50.04542,50.04544 0,5.92272 -4.80129,10.72401 -10.72401,10.72401 z"
id="path10-5" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.23561016;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -187.12346,367.23573 c 0,5.92272 -4.80128,10.724 -10.72401,10.724 -39.48482,0 -71.49346,-32.00863 -71.49346,-71.49343 0,-5.92272 4.80129,-10.72403 10.72403,-10.72403 5.92271,0 10.72404,4.80131 10.72404,10.72403 0,27.63936 22.40601,50.04542 50.04539,50.04542 5.92273,0 10.72401,4.80128 10.72401,10.72401 z"
id="path6-6" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.29136932;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m 629.26349,134.49819 c -5.17912,5.17913 -13.57608,5.17911 -18.75524,-3e-5 -34.52746,-34.527469 -34.52746,-90.507338 4e-5,-125.0348349 5.17912,-5.1791201 13.57609,-5.1791215 18.75521,2.1e-6 5.17912,5.1791198 5.1791,13.5760858 -10e-6,18.7551968 -24.16926,24.16926 -24.16922,63.355176 0,87.524396 5.17914,5.17914 5.17912,13.57614 0,18.75527 z"
id="path4-3-3" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.41281462;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m 740.87146,587.17776 c -10.37726,0 -18.78964,-8.41245 -18.78964,-18.78967 0,-69.18171 56.08261,-125.26432 125.26429,-125.26432 10.37724,0 18.78967,8.41239 18.78967,18.78963 0,10.37726 -8.41243,18.78964 -18.78967,18.78964 -48.42717,0 -87.68502,39.25785 -87.68502,87.68505 0,10.37722 -8.41238,18.78967 -18.78963,18.78967 z"
id="path10-5-6" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.29136932;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m 630.7519,243.88245 c -5.17914,5.17911 -13.5761,5.17911 -18.75523,0 -34.52748,-34.52751 -34.5275,-90.50737 0,-125.03487 5.17912,-5.1791 13.57607,-5.17908 18.75518,3e-5 5.17917,5.17907 5.17918,13.57606 5e-5,18.75519 -24.16926,24.16926 -24.16924,63.35515 2e-5,87.5244 5.17912,5.17912 5.17907,13.57615 -2e-5,18.75518 z"
id="path8-7-5" /><g
id="g3921"
transform="matrix(1.2623093,0,0,1.2623093,-22.620675,-167.67864)"><path
id="path4-9"
d="m -567.88395,525.90207 c 0,-8.09524 6.56246,-14.6577 14.65773,-14.6577 53.96826,0 97.71801,43.74975 97.71801,97.71804 0,8.09524 -6.56246,14.65769 -14.65769,14.65769 -8.09525,0 -14.6577,-6.56245 -14.6577,-14.65769 0,-37.7778 -30.62484,-68.40262 -68.40262,-68.40262 -8.09527,0 -14.65773,-6.56248 -14.65773,-14.65772 z"
clip-rule="evenodd"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.32203454;stroke-opacity:1" /><path
id="path6-1"
d="m -499.48133,740.88175 c 0,8.09524 -6.56245,14.65768 -14.65769,14.65768 -53.96829,0 -97.71805,-43.74974 -97.71805,-97.718 0,-8.09524 6.56246,-14.65772 14.65772,-14.65772 8.09523,0 14.65773,6.56248 14.65773,14.65772 0,37.77778 30.62479,68.40262 68.4026,68.40262 8.09524,0 14.65769,6.56245 14.65769,14.6577 z"
clip-rule="evenodd"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.32203454;stroke-opacity:1" /><path
id="path8-2"
d="m -641.17244,667.59321 c -8.09527,0 -14.65773,-6.56245 -14.65773,-14.65769 0,-53.96829 43.74973,-97.71804 97.71804,-97.71804 8.09524,0 14.6577,6.56246 14.6577,14.6577 0,8.09524 -6.56246,14.65772 -14.6577,14.65772 -37.77782,0 -68.40261,30.62481 -68.40261,68.40262 0,8.09524 -6.5625,14.65769 -14.6577,14.65769 z"
clip-rule="evenodd"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.32203454;stroke-opacity:1" /><path
id="path10-7"
d="m -426.19279,599.19059 c 8.09524,0 14.65769,6.56246 14.65769,14.65769 0,53.9683 -43.74975,97.71805 -97.71801,97.71805 -8.09524,0 -14.65772,-6.56246 -14.65772,-14.65769 0,-8.09525 6.56248,-14.6577 14.65772,-14.6577 37.77778,0 68.40262,-30.62484 68.40262,-68.40266 0,-8.09523 6.56245,-14.65769 14.6577,-14.65769 z"
clip-rule="evenodd"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.32203454;stroke-opacity:1" /></g><path
d="m 168.87017,369.7941 c 0,0 13.23506,0.93627 30.26137,-0.56431 6.89523,-0.60768 33.07453,-3.17912 52.64715,-7.47152 0,0 23.86373,-5.10715 36.63057,-9.81197 13.35843,-4.92285 20.62761,-9.10098 23.89954,-15.02127 -0.14269,-1.21302 1.00739,-5.51461 -5.15294,-8.09846 -15.74964,-6.60595 -34.01548,-5.41108 -70.15906,-6.1774 -40.0818,-1.37718 -53.41542,-8.08627 -60.51817,-13.48977 -6.81108,-5.48163 -3.38613,-20.64701 25.79772,-34.00541 14.70074,-7.11354 72.32932,-20.24098 72.32932,-20.24098 -19.40827,-9.59329 -55.59858,-26.458 -63.03787,-30.09979 -6.52468,-3.19401 -16.96616,-8.00329 -19.2295,-13.82188 -2.56614,-5.58603 6.06036,-10.39793 10.87905,-11.77591 15.51905,-4.47641 37.42722,-7.25864 57.3662,-7.571 10.0224,-0.15701 11.64921,-0.80185 11.64921,-0.80185 13.82901,-2.29394 22.93273,-11.75538 19.13973,-26.73949 -3.40511,-15.29487 -21.36391,-24.28197 -38.43005,-21.17074 -16.07116,2.92988 -54.80683,14.18154 -54.80683,14.18154 47.88016,-0.4144 55.89402,0.38474 59.47311,5.38881 2.11368,2.95526 -0.96045,7.00739 -13.7299,9.09291 -13.90189,2.27049 -42.80009,5.00476 -42.80009,5.00476 -27.72258,1.6464 -47.25033,1.75659 -53.10719,14.15679 -3.82632,8.10119 4.08038,15.26323 7.5459,19.74633 14.64462,16.28629 35.79785,25.06993 49.41383,31.53826 5.12311,2.43375 20.15489,7.02978 20.15489,7.02978 -44.17265,-2.42953 -76.03716,11.13432 -94.72864,26.75121 -21.14069,19.55411 -11.78868,42.86201 31.52274,57.21332 25.58149,8.47645 38.26825,12.46292 76.42687,9.02676 22.47583,-1.21144 26.01893,-0.49052 26.24301,1.35373 0.31548,2.59652 -24.96418,9.04641 -31.86578,11.03716 -17.55777,5.06447 -63.5838,15.29078 -63.81419,15.34039 z"
id="path4237"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-opacity:1;stroke-width:2.13333333000000014;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.29136932;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m 741.14237,231.14899 c 5.17911,5.17911 5.17911,13.5761 0,18.75521 -34.5275,34.52749 -90.50738,34.5275 -125.03484,5e-5 -5.17911,-5.17912 -5.17915,-13.57613 -10e-6,-18.75527 5.17911,-5.17911 13.57615,-5.1791 18.75525,1e-5 24.16922,24.16921 63.35516,24.16927 87.5244,2e-5 5.17913,-5.17912 13.57606,-5.17915 18.7552,-2e-5 z"
id="path6-6-2" /><text
xml:space="preserve"
style="font-size:237.56724547999996844px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#0dbd8b;font-family:Hack;-inkscape-font-specification:Hack;stroke-opacity:1;stroke-width:2.13333333000000014;stroke-miterlimit:4;stroke-dasharray:none"
x="295.67422"
y="342.85031"
id="text3927"
sodipodi:linespacing="125%"
transform="scale(0.97330289,1.0274294)"><tspan
sodipodi:role="line"
id="tspan3929"
x="295.67422"
y="342.85031"
style="font-size:237.56724547999996844px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;font-family:Hack;-inkscape-font-specification:Hack;stroke:#0dbd8b;stroke-opacity:1;stroke-width:2.13333333000000014;stroke-miterlimit:4;stroke-dasharray:none">l</tspan></text>
<text
xml:space="preserve"
style="font-size:138.19949341000000231px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#0dbd8b;font-family:Hack;-inkscape-font-specification:Hack;stroke-opacity:1;stroke-width:2.13333333000000014;stroke-miterlimit:4;stroke-dasharray:none"
x="76.662376"
y="367.39389"
id="text3931"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3933"
x="76.662376"
y="367.39389">.</tspan></text>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xml:space="preserve"
id="svg4768"
viewBox="0.171 0.201 512 512"
height="48"
width="48"
version="1.0"
inkscape:version="0.48.4 r9939"
sodipodi:docname="logo2.svg"
inkscape:export-filename="/home/me/src/emacs/ement.el/images/logo-128px.png"
inkscape:export-xdpi="240"
inkscape:export-ydpi="240"><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1173"
id="namedview72"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="-8.3796227"
inkscape:cy="-20.646658"
inkscape:window-x="1920"
inkscape:window-y="6"
inkscape:window-maximized="1"
inkscape:current-layer="svg4768" /><metadata
id="metadata70"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><!-- Gnu Emacs Icon
Copyright (C) 2008-2017 Free Software Foundation, Inc.
Author: Nicolas Petton <nicolas@petton.fr>
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 <http://www.gnu.org/licenses/>.
--><!-- Created with Inkscape (http://www.inkscape.org/) --><defs
id="defs4770"><linearGradient
id="linearGradient3961"><stop
id="stop3963"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" /><stop
id="stop3965"
offset="1"
style="stop-color:#0dbd8b;stop-opacity:1;" /></linearGradient><linearGradient
id="linearGradient3874"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1;"
id="stop3876" /><stop
id="stop3882"
style="stop-color:#000000;stop-opacity:1;"
offset="1" /></linearGradient><linearGradient
id="linearGradient3866"><stop
style="stop-color:#00ff18;stop-opacity:1;"
offset="0"
id="stop3868" /><stop
style="stop-color:#000000;stop-opacity:1;"
offset="1"
id="stop3870" /></linearGradient><linearGradient
id="linearGradient3817"><stop
id="stop3819"
style="stop-color:#00ff0a;stop-opacity:1;"
offset="0" /><stop
id="stop3823"
style="stop-color:#000000;stop-opacity:0.99215686;"
offset="1" /></linearGradient><linearGradient
id="linearGradient4292"><stop
id="stop4294"
offset="0"
style="stop-color:#411f5d;stop-opacity:1" /><stop
id="stop4296"
offset="1"
style="stop-color:#5b2a85;stop-opacity:1" /></linearGradient><linearGradient
id="linearGradient4284"><stop
offset="0"
style="stop-color:#8381c5;stop-opacity:1"
id="stop4286" /><stop
id="stop4290"
style="stop-color:#7e55b3;stop-opacity:0.99607843"
offset="0.56639391" /><stop
offset="1"
style="stop-color:#a52ecb;stop-opacity:0.99215686"
id="stop4288" /></linearGradient><linearGradient
id="linearGradient4898"><stop
id="stop4278"
style="stop-color:#bab8db;stop-opacity:1"
offset="0" /><stop
id="stop4280"
style="stop-color:#5955a9;stop-opacity:0.99159664"
offset="1" /></linearGradient><linearGradient
id="linearGradient3294"><stop
offset="0"
style="stop-color:#6376e6;stop-opacity:1"
id="stop3296" /><stop
offset="0.50094414"
style="stop-color:#222989;stop-opacity:1"
id="stop3302" /><stop
offset="1"
style="stop-color:#00003d;stop-opacity:1"
id="stop3298" /></linearGradient><linearGradient
id="linearGradient3284"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1"
id="stop3286" /><stop
offset="0.84845906"
style="stop-color:#000000;stop-opacity:0.49803922"
id="stop3292" /><stop
offset="1"
style="stop-color:#000000;stop-opacity:0"
id="stop3288" /></linearGradient><linearGradient
id="linearGradient3274"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1"
id="stop3276" /><stop
offset="1"
style="stop-color:#000000;stop-opacity:0"
id="stop3278" /></linearGradient><linearGradient
id="linearGradient3262"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1"
id="stop3264" /><stop
offset="1"
style="stop-color:#000000;stop-opacity:0"
id="stop3266" /></linearGradient><linearGradient
id="linearGradient3242"><stop
offset="0"
style="stop-color:#282828;stop-opacity:1"
id="stop3244" /><stop
offset="0.39253417"
style="stop-color:#808080;stop-opacity:1"
id="stop3252" /><stop
offset="1"
style="stop-color:#d9d9d9;stop-opacity:1"
id="stop3246" /></linearGradient><linearGradient
id="linearGradient3202"><stop
offset="0"
style="stop-color:#2b2b2b;stop-opacity:1"
id="stop3204" /><stop
offset="0.5"
style="stop-color:#828383;stop-opacity:1"
id="stop3250" /><stop
offset="1"
style="stop-color:#dadbdb;stop-opacity:1"
id="stop3206" /></linearGradient><linearGradient
id="linearGradient4966"><stop
offset="0"
style="stop-color:#b6b3d8;stop-opacity:1"
id="stop4968" /><stop
offset="1"
style="stop-color:#b6b3d8;stop-opacity:0"
id="stop4970" /></linearGradient><linearGradient
id="linearGradient4938"><stop
offset="0"
style="stop-color:#000000;stop-opacity:1"
id="stop4940" /><stop
offset="1"
style="stop-color:#000000;stop-opacity:0"
id="stop4942" /></linearGradient><linearGradient
id="linearGradient4282"><stop
offset="0"
style="stop-color:#bab8db;stop-opacity:1"
id="stop4900" /><stop
offset="1"
style="stop-color:#5955a9;stop-opacity:0.99159664"
id="stop4902" /></linearGradient><linearGradient
id="linearGradient4876"><stop
offset="0"
style="stop-color:#d3d2e8;stop-opacity:1"
id="stop4878" /><stop
offset="1"
style="stop-color:#5955a9;stop-opacity:0.99159664"
id="stop4880" /></linearGradient><radialGradient
gradientTransform="matrix(0.6817439,0,0,0.5905355,-3.8523706,-28.935273)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient4898"
id="radialGradient4892"
fy="-108.96888"
fx="20.951529"
r="266.76535"
cy="-108.96888"
cx="20.951529" /><radialGradient
gradientTransform="matrix(1,0,0,0.1854103,0,383.88493)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient4938"
id="radialGradient4944"
fy="471.26172"
fx="233.8876"
r="170.49393"
cy="471.26172"
cx="233.8876" /><radialGradient
gradientTransform="matrix(1,0,0,0.9121621,0,32.654948)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient4966"
id="radialGradient4972"
fy="371.76376"
fx="299.70135"
r="76.696358"
cy="371.76376"
cx="299.70135" /><radialGradient
gradientTransform="matrix(0.414705,0.3300575,-0.5059004,0.6356454,346.95314,49.479585)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3202"
id="radialGradient3210"
fy="390.45248"
fx="289.44067"
r="17.67668"
cy="390.45248"
cx="289.44067" /><radialGradient
gradientTransform="matrix(0.414705,0.3300575,-0.5059004,0.6356454,448.41009,-65.398074)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3202"
id="radialGradient3238"
fy="382.14804"
fx="283.50717"
r="17.67668"
cy="382.14804"
cx="283.50717" /><radialGradient
gradientTransform="matrix(-6.5565014e-2,-5.9721765e-2,1.6871024,-1.8521705,171.90774,540.51473)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3242"
id="radialGradient3248"
fy="181.18982"
fx="418.45551"
r="63.068935"
cy="181.18982"
cx="418.45551" /><radialGradient
gradientTransform="matrix(0.4055116,-3.3440123e-2,0.1034174,4.3988695,177.23251,-1191.6649)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3262"
id="radialGradient3268"
fy="357.33591"
fx="354.51709"
r="33.712105"
cy="357.33591"
cx="354.51709" /><radialGradient
gradientTransform="matrix(-0.1339874,-0.1146812,0.3079048,-0.3597394,444.23592,395.03849)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3274"
id="radialGradient3280"
fy="223.55537"
fx="510.58469"
r="132.28336"
cy="223.55537"
cx="510.58469" /><radialGradient
gradientTransform="matrix(-1.2497569,1.3798305,-9.6289463e-2,-7.2974479e-2,674.3826,-70.590682)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3284"
id="radialGradient3290"
fy="-158.17821"
fx="284.4671"
r="110.2972"
cy="-158.17821"
cx="284.4671" /><radialGradient
gradientTransform="matrix(-0.1008165,-8.0872321e-2,1.0745309,-1.3395252,13.843287,784.79288)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3294"
id="radialGradient3300"
fy="356.62274"
fx="425.51019"
r="143.34167"
cy="356.62274"
cx="425.51019" /><filter
height="1.088351"
y="-0.044175496"
width="1.0892536"
x="-0.044626798"
id="filter4350"
style="color-interpolation-filters:sRGB"><feGaussianBlur
id="feGaussianBlur4352"
stdDeviation="8.7848425" /></filter><linearGradient
y2="300.73987"
x2="236.61363"
y1="-161.8512"
x1="-122.20192"
spreadMethod="pad"
gradientTransform="matrix(0.87385837,0,0,0.82818057,246.00762,250.28138)"
gradientUnits="userSpaceOnUse"
id="linearGradient4245"
xlink:href="#linearGradient3817" /><linearGradient
y2="66.018341"
x2="173.94518"
y1="396.6066"
x1="447.80933"
gradientTransform="matrix(0.98684959,0,0,0.98684959,3.0344187,2.5250397)"
gradientUnits="userSpaceOnUse"
id="linearGradient4247"
xlink:href="#linearGradient4292" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3961"
id="radialGradient3959"
cx="1136.9111"
cy="38.175797"
fx="1136.9111"
fy="38.175797"
r="233.11514"
gradientTransform="matrix(1,0,0,1.010216,-880.74005,217.63519)"
gradientUnits="userSpaceOnUse" /><filter
inkscape:collect="always"
id="filter3894"><feGaussianBlur
inkscape:collect="always"
stdDeviation="7.6798908"
id="feGaussianBlur3896" /></filter><filter
color-interpolation-filters="sRGB"
inkscape:collect="always"
id="filter3894-3"><feGaussianBlur
inkscape:collect="always"
stdDeviation="7.6798908"
id="feGaussianBlur3896-1" /></filter></defs><rect
style="fill:none;display:none"
id="rect4772"
y="0.20100001"
x="0.171"
height="512"
width="512" /><g
style="display:none"
id="g4788"><g
style="display:inline"
id="g4790" /></g><g
style="display:none"
id="g4806"><g
style="display:inline"
id="g4808"><path
style="fill:#050505;display:none"
id="path4810"
d="M 349.098,256.651 C 348.833,256.397 386.735,284.256 388.519,281.663 C 394.881,272.411 470.565,188.526 473.303,165.427 C 473.545,163.424 472.787,161.331 472.787,161.331 C 472.787,161.331 471.597,161.187 466.462,157.017 C 463.77,154.825 460.979,152.436 460.979,152.436 C 444.925,153.434 403.094,193.995 349.917,256.004" /></g></g><path
d="m 438.46612,112.92247 c 80.3259,102.29207 63.35739,249.67467 -37.9002,329.18824 C 299.30833,521.62427 152.10573,503.15872 71.779825,400.86665 -8.5460891,298.57456 8.4225064,151.19191 109.6801,71.678342 210.93769,-7.8352228 358.14021,10.630379 438.46612,112.92247 z"
id="path4235"
style="fill:#0dbd8b;fill-opacity:1;stroke:#000000;stroke-width:13.33333302;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
inkscape:connector-curvature="0" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.64471179;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -862.34934,-104.26336 c 0,-16.20664 13.138,-29.34464 29.34471,-29.34464 108.04422,0 195.63103,87.586807 195.63103,195.631095 0,16.206636 -13.138,29.344642 -29.34464,29.344642 -16.20664,0 -29.34464,-13.138006 -29.34464,-29.344642 0,-75.631014 -61.3108,-136.941754 -136.94175,-136.941754 -16.20671,0 -29.34471,-13.138067 -29.34471,-29.344701 z"
id="path4" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.64471179;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -725.40758,326.12504 c 0,16.20664 -13.13801,29.34464 -29.34464,29.34464 -108.04429,0 -195.63112,-87.58681 -195.63112,-195.63103 0,-16.20664 13.138,-29.34471 29.3447,-29.34471 16.2066,0 29.3447,13.13807 29.3447,29.34471 0,75.63095 61.3107,136.94175 136.94172,136.94175 16.20663,0 29.34464,13.138 29.34464,29.34464 z"
id="path6" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.64471179;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -1009.0726,179.40172 c -16.2067,0 -29.3447,-13.13801 -29.3447,-29.34464 0,-108.044293 87.58676,-195.631101 195.6311,-195.631101 16.20663,0 29.34464,13.138002 29.34464,29.344639 0,16.20663681 -13.13801,29.344702 -29.34464,29.344702 -75.63104,0 -136.94174,61.310741 -136.94174,136.94176 0,16.20663 -13.1381,29.34464 -29.34466,29.34464 z"
id="path8" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.64471179;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -578.68426,42.459959 c 16.20664,0 29.34464,13.138002 29.34464,29.344639 0,108.044292 -87.58681,195.631102 -195.63103,195.631102 -16.20664,0 -29.34471,-13.138 -29.34471,-29.34464 0,-16.20664 13.13807,-29.34464 29.34471,-29.34464 75.63095,0 136.94175,-61.3108 136.94175,-136.941822 0,-16.206637 13.138,-29.344639 29.34464,-29.344639 z"
id="path10" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.23561016;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -258.96103,188.38006 c -5.92272,0 -10.72401,-4.80129 -10.72401,-10.72404 0,-39.48479 32.00863,-71.49343 71.49346,-71.49343 5.92272,0 10.72401,4.80129 10.72401,10.72401 0,5.92273 -4.80129,10.72401 -10.72401,10.72401 -27.63938,0 -50.04542,22.40606 -50.04542,50.04541 0,5.92275 -4.80131,10.72404 -10.72403,10.72404 z"
id="path4-3" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.23561016;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -187.33996,241.78363 c 0,5.92274 -4.80129,10.72402 -10.72401,10.72402 -39.48482,0 -71.49346,-32.00861 -71.49346,-71.49346 0,-5.92272 4.80129,-10.72401 10.72401,-10.72401 5.92273,0 10.72403,4.80129 10.72403,10.72401 0,27.63939 22.40604,50.04541 50.04542,50.04541 5.92272,0 10.72401,4.80133 10.72401,10.72402 z"
id="path8-7" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.23561016;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -258.74453,313.27152 c -5.92272,0 -10.72401,-4.80129 -10.72401,-10.72401 0,-39.48483 32.00863,-71.49347 71.49344,-71.49347 5.92272,0 10.72403,4.80129 10.72403,10.72401 0,5.92273 -4.80131,10.72402 -10.72403,10.72402 -27.63937,0 -50.04542,22.40605 -50.04542,50.04544 0,5.92272 -4.80129,10.72401 -10.72401,10.72401 z"
id="path10-5" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.23561016;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m -187.12346,367.23573 c 0,5.92272 -4.80128,10.724 -10.72401,10.724 -39.48482,0 -71.49346,-32.00863 -71.49346,-71.49343 0,-5.92272 4.80129,-10.72403 10.72403,-10.72403 5.92271,0 10.72404,4.80131 10.72404,10.72403 0,27.63936 22.40601,50.04542 50.04539,50.04542 5.92273,0 10.72401,4.80128 10.72401,10.72401 z"
id="path6-6" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.29136932;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m 629.26349,134.49819 c -5.17912,5.17913 -13.57608,5.17911 -18.75524,-3e-5 -34.52746,-34.527469 -34.52746,-90.507338 4e-5,-125.0348349 5.17912,-5.1791201 13.57609,-5.1791215 18.75521,2.1e-6 5.17912,5.1791198 5.1791,13.5760858 -10e-6,18.7551968 -24.16926,24.16926 -24.16922,63.355176 0,87.524396 5.17914,5.17914 5.17912,13.57614 0,18.75527 z"
id="path4-3-3" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.41281462;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m 740.87146,587.17776 c -10.37726,0 -18.78964,-8.41245 -18.78964,-18.78967 0,-69.18171 56.08261,-125.26432 125.26429,-125.26432 10.37724,0 18.78967,8.41239 18.78967,18.78963 0,10.37726 -8.41243,18.78964 -18.78967,18.78964 -48.42717,0 -87.68502,39.25785 -87.68502,87.68505 0,10.37722 -8.41238,18.78967 -18.78963,18.78967 z"
id="path10-5-6" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.29136932;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m 630.7519,243.88245 c -5.17914,5.17911 -13.5761,5.17911 -18.75523,0 -34.52748,-34.52751 -34.5275,-90.50737 0,-125.03487 5.17912,-5.1791 13.57607,-5.17908 18.75518,3e-5 5.17917,5.17907 5.17918,13.57606 5e-5,18.75519 -24.16926,24.16926 -24.16924,63.35515 2e-5,87.5244 5.17912,5.17912 5.17907,13.57615 -2e-5,18.75518 z"
id="path8-7-5" /><g
id="g3921"
transform="matrix(1.2623093,0,0,1.2623093,-22.620675,-167.67864)"><path
id="path4-9"
d="m -567.88395,525.90207 c 0,-8.09524 6.56246,-14.6577 14.65773,-14.6577 53.96826,0 97.71801,43.74975 97.71801,97.71804 0,8.09524 -6.56246,14.65769 -14.65769,14.65769 -8.09525,0 -14.6577,-6.56245 -14.6577,-14.65769 0,-37.7778 -30.62484,-68.40262 -68.40262,-68.40262 -8.09527,0 -14.65773,-6.56248 -14.65773,-14.65772 z"
clip-rule="evenodd"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.32203454;stroke-opacity:1" /><path
id="path6-1"
d="m -499.48133,740.88175 c 0,8.09524 -6.56245,14.65768 -14.65769,14.65768 -53.96829,0 -97.71805,-43.74974 -97.71805,-97.718 0,-8.09524 6.56246,-14.65772 14.65772,-14.65772 8.09523,0 14.65773,6.56248 14.65773,14.65772 0,37.77778 30.62479,68.40262 68.4026,68.40262 8.09524,0 14.65769,6.56245 14.65769,14.6577 z"
clip-rule="evenodd"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.32203454;stroke-opacity:1" /><path
id="path8-2"
d="m -641.17244,667.59321 c -8.09527,0 -14.65773,-6.56245 -14.65773,-14.65769 0,-53.96829 43.74973,-97.71804 97.71804,-97.71804 8.09524,0 14.6577,6.56246 14.6577,14.6577 0,8.09524 -6.56246,14.65772 -14.6577,14.65772 -37.77782,0 -68.40261,30.62481 -68.40261,68.40262 0,8.09524 -6.5625,14.65769 -14.6577,14.65769 z"
clip-rule="evenodd"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.32203454;stroke-opacity:1" /><path
id="path10-7"
d="m -426.19279,599.19059 c 8.09524,0 14.65769,6.56246 14.65769,14.65769 0,53.9683 -43.74975,97.71805 -97.71801,97.71805 -8.09524,0 -14.65772,-6.56246 -14.65772,-14.65769 0,-8.09525 6.56248,-14.6577 14.65772,-14.6577 37.77778,0 68.40262,-30.62484 68.40262,-68.40266 0,-8.09523 6.56245,-14.65769 14.6577,-14.65769 z"
clip-rule="evenodd"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.32203454;stroke-opacity:1" /></g><path
d="m 132.03775,284.66555 c 0,0 8.69652,10.02068 21.79702,20.99902 5.30536,4.44597 25.63519,21.13925 42.51032,31.94399 0,0 20.48551,13.26291 32.83983,18.96361 12.92683,5.96487 21.0213,8.15057 27.52118,6.27789 0.75681,-0.95865 4.61173,-3.1871 2.08279,-9.37015 -6.46553,-15.80783 -20.22633,-27.87882 -45.24183,-53.97806 -27.3683,-29.31592 -32.05255,-43.48826 -33.2541,-52.33151 -0.94007,-8.69226 12.20528,-16.994 42.2872,-5.80372 15.42502,5.36496 65.45708,36.83202 65.45708,36.83202 -6.94024,-20.50721 -20.6055,-58.02276 -23.29074,-65.85827 -2.35514,-6.87215 -6.3377,-17.65607 -3.82376,-23.37085 2.13538,-5.76446 11.63777,-3.06713 16.01947,-0.63418 14.13893,7.80833 31.59768,21.33241 45.91754,35.21052 7.19793,6.97589 8.80422,7.67024 8.80422,7.67024 11.40065,8.15653 24.5282,7.90359 32.44151,-5.37384 8.40733,-13.22288 2.06338,-32.27651 -12.20417,-42.14412 -13.43576,-9.29229 -48.78214,-28.72642 -48.78214,-28.72642 34.14941,33.56336 39.25098,39.7951 38.24337,45.86431 -0.59509,3.58428 -5.6341,4.27584 -16.13816,-3.27885 -11.43561,-8.22465 -33.80314,-26.72533 -33.80314,-26.72533 -20.767,-18.43865 -34.65312,-32.16894 -47.56281,-27.5421 -8.43403,3.02279 -7.90747,13.678 -8.62701,19.29852 -1.16084,21.87146 7.58578,43.04002 12.63994,57.24178 1.90166,5.3435 9.28085,19.22246 9.28085,19.22246 -29.51684,-32.95272 -61.63954,-45.89324 -85.89923,-48.0673 -28.77557,-1.12189 -38.64387,21.97216 -18.16598,62.74587 12.09509,24.0826 18.24712,35.87235 47.65908,60.42484 16.74943,15.03619 18.74503,18.05128 17.59938,19.51384 -1.61292,2.05912 -24.04912,-11.25556 -30.33696,-14.72806 -15.99635,-8.83409 -55.77276,-34.14832 -55.97075,-34.27615 z"
id="path4237-0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:2.13333344;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3894)"
inkscape:connector-curvature="0"
transform="matrix(-1.3538609,0,0,-1.3538609,693.74978,1158.395)" /><path
d="m 132.03775,284.66555 c 0,0 8.69652,10.02068 21.79702,20.99902 5.30536,4.44597 25.63519,21.13925 42.51032,31.94399 0,0 20.48551,13.26291 32.83983,18.96361 12.92683,5.96487 21.0213,8.15057 27.52118,6.27789 0.75681,-0.95865 4.61173,-3.1871 2.08279,-9.37015 -6.46553,-15.80783 -20.22633,-27.87882 -45.24183,-53.97806 -27.3683,-29.31592 -32.05255,-43.48826 -33.2541,-52.33151 -0.94007,-8.69226 12.20528,-16.994 42.2872,-5.80372 15.42502,5.36496 65.45708,36.83202 65.45708,36.83202 -6.94024,-20.50721 -20.6055,-58.02276 -23.29074,-65.85827 -2.35514,-6.87215 -6.3377,-17.65607 -3.82376,-23.37085 2.13538,-5.76446 11.63777,-3.06713 16.01947,-0.63418 14.13893,7.80833 31.59768,21.33241 45.91754,35.21052 7.19793,6.97589 8.80422,7.67024 8.80422,7.67024 11.40065,8.15653 24.5282,7.90359 32.44151,-5.37384 8.40733,-13.22288 2.06338,-32.27651 -12.20417,-42.14412 -13.43576,-9.29229 -48.78214,-28.72642 -48.78214,-28.72642 34.14941,33.56336 39.25098,39.7951 38.24337,45.86431 -0.59509,3.58428 -5.6341,4.27584 -16.13816,-3.27885 -11.43561,-8.22465 -33.80314,-26.72533 -33.80314,-26.72533 -20.767,-18.43865 -34.65312,-32.16894 -47.56281,-27.5421 -8.43403,3.02279 -7.90747,13.678 -8.62701,19.29852 -1.16084,21.87146 7.58578,43.04002 12.63994,57.24178 1.90166,5.3435 9.28085,19.22246 9.28085,19.22246 -29.51684,-32.95272 -61.63954,-45.89324 -85.89923,-48.0673 -28.77557,-1.12189 -38.64387,21.97216 -18.16598,62.74587 12.09509,24.0826 18.24712,35.87235 47.65908,60.42484 16.74943,15.03619 18.74503,18.05128 17.59938,19.51384 -1.61292,2.05912 -24.04912,-11.25556 -30.33696,-14.72806 -15.99635,-8.83409 -55.77276,-34.14832 -55.97075,-34.27615 z"
id="path4237-0-2"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:2.13333344;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3894-3)"
inkscape:connector-curvature="0"
transform="matrix(1.3538609,0,0,1.3538609,-52.89981,465.82444)" /><path
d="m -269.87161,757.91183 c 0,0 13.23506,0.93627 30.26137,-0.56431 6.89523,-0.60768 33.07453,-3.17912 52.64715,-7.47152 0,0 23.86373,-5.10715 36.63057,-9.81197 13.35843,-4.92285 20.62761,-9.10098 23.89954,-15.02127 -0.14269,-1.21302 1.00739,-5.51461 -5.15294,-8.09846 -15.74964,-6.60595 -34.01548,-5.41108 -70.15906,-6.1774 -40.0818,-1.37718 -53.41542,-8.08627 -60.51817,-13.48977 -6.81108,-5.48163 -3.38613,-20.64701 25.79772,-34.00541 14.70074,-7.11354 72.32932,-20.24098 72.32932,-20.24098 -19.40827,-9.59329 -55.59858,-26.458 -63.03787,-30.09979 -6.52468,-3.19401 -16.96616,-8.00329 -19.2295,-13.82188 -2.56614,-5.58603 6.06036,-10.39793 10.87905,-11.77591 15.51905,-4.47641 37.42722,-7.25864 57.3662,-7.571 10.0224,-0.15701 11.64921,-0.80185 11.64921,-0.80185 13.82901,-2.29394 22.93273,-11.75538 19.13973,-26.73949 -3.40511,-15.29487 -21.36391,-24.28197 -38.43005,-21.17074 -16.07116,2.92988 -54.80683,14.18154 -54.80683,14.18154 47.88016,-0.4144 55.89402,0.38474 59.47311,5.38881 2.11368,2.95526 -0.96045,7.00739 -13.7299,9.09291 -13.90189,2.27049 -42.80009,5.00476 -42.80009,5.00476 -27.72258,1.6464 -47.25033,1.75659 -53.10719,14.15679 -3.82632,8.10119 4.08038,15.26323 7.5459,19.74633 14.64462,16.28629 35.79785,25.06993 49.41383,31.53826 5.12311,2.43375 20.15489,7.02978 20.15489,7.02978 -44.17265,-2.42953 -76.03716,11.13432 -94.72864,26.75121 -21.14069,19.55411 -11.78868,42.86201 31.52274,57.21332 25.58149,8.47645 38.26825,12.46292 76.42687,9.02676 22.47583,-1.21144 26.01893,-0.49052 26.24301,1.35373 0.31548,2.59652 -24.96418,9.04641 -31.86578,11.03716 -17.55777,5.06447 -63.5838,15.29078 -63.81419,15.34039 z"
id="path4237"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:2.13333344;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
inkscape:connector-curvature="0" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:0.29136932;stroke-opacity:1"
inkscape:connector-curvature="0"
clip-rule="evenodd"
d="m 741.14237,231.14899 c 5.17911,5.17911 5.17911,13.5761 0,18.75521 -34.5275,34.52749 -90.50738,34.5275 -125.03484,5e-5 -5.17911,-5.17912 -5.17915,-13.57613 -10e-6,-18.75527 5.17911,-5.17911 13.57615,-5.1791 18.75525,1e-5 24.16922,24.16921 63.35516,24.16927 87.5244,2e-5 5.17913,-5.17912 13.57606,-5.17915 18.7552,-2e-5 z"
id="path6-6-2" /><text
xml:space="preserve"
style="font-size:237.56724548px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#0dbd8b;stroke-width:2.13333344;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;font-family:Hack;-inkscape-font-specification:Hack"
x="1116.8435"
y="417.50568"
id="text3927"
sodipodi:linespacing="125%"
transform="scale(0.97330289,1.0274294)"><tspan
sodipodi:role="line"
id="tspan3929"
x="1116.8435"
y="417.50568"
style="font-size:237.56724548px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;stroke:#0dbd8b;stroke-width:2.13333344;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;font-family:Hack;-inkscape-font-specification:Hack">l</tspan></text>
<text
xml:space="preserve"
style="font-size:138.19949341px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#0dbd8b;stroke-width:2.13333344;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;font-family:Hack;-inkscape-font-specification:Hack"
x="-359.01129"
y="525.40228"
id="text3931"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3933"
x="-359.01129"
y="525.40228">.</tspan></text>
<path
d="m 434.66331,265.76624 c 0,0 -5.87624,-16.97489 -16.80346,-37.37302 -4.42522,-8.26075 -21.55552,-39.48393 -37.31315,-61.56659 0,0 -19.05507,-26.99135 -31.70512,-40.38178 -13.23615,-14.01104 -22.30529,-20.83761 -31.41651,-21.76094 -1.43417,0.82308 -7.40128,1.68002 -7.34029,10.72393 0.15581,23.12199 11.36194,45.22591 29.63861,90.62981 19.61161,50.63174 18.35361,70.8008 15.40574,82.51822 -3.2003,11.39589 -23.90241,15.20102 -56.06034,-14.02326 -16.67759,-14.51651 -63.68239,-79.27534 -63.68239,-79.27534 -1.61653,29.26616 -3.35632,83.29359 -3.93191,94.49265 -0.50478,9.82218 -0.93646,25.37992 -6.97594,31.29347 -5.58886,6.1668 -16.16918,-2.01234 -20.4485,-7.27809 -13.82996,-16.93838 -28.95018,-42.73217 -39.94787,-67.38856 -5.52802,-12.39366 -7.19635,-14.07581 -7.19635,-14.07581 -10.21371,-15.99561 -26.83628,-22.29501 -43.47232,-9.60048 -17.229294,12.37696 -18.862353,39.5161 -5.90882,59.10686 12.19836,18.44856 46.81573,60.68509 46.81573,60.68509 -25.9912,-59.38689 -29.26021,-69.78878 -24.93481,-76.90697 2.55449,-4.20376 9.23472,-2.5327 18.62521,12.25475 10.2233,16.09882 29.00297,50.6202 29.00297,50.6202 16.79982,33.63665 27.327,57.88869 45.8806,58.58235 12.12126,0.45314 16.83063,-13.20078 20.56787,-19.9004 12.4834,-26.89688 12.16354,-57.90459 12.97156,-78.29709 0.30404,-7.67281 -1.9721,-28.83169 -1.9721,-28.83169 20.47806,56.28447 54.31805,88.73667 83.70504,103.69706 35.59166,15.91462 59.63247,-8.12927 54.4544,-69.68472 -3.05843,-36.35714 -4.84572,-54.27229 -29.42634,-99.9488 -13.46673,-27.33625 -14.45442,-32.13071 -12.27766,-33.39096 3.06463,-1.7743 24.54469,26.26536 30.69511,33.79814 15.64679,19.16356 52.86658,71.02164 53.05093,71.28206 z"
id="path4237-0-1"
style="fill:#351d52;fill-opacity:0.45098039;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:2.88823676;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
inkscape:connector-curvature="0" /><path
d="m 76.374471,245.63739 c 0,0 5.87624,16.97489 16.803461,37.37302 4.425216,8.26075 21.555518,39.48394 37.313168,61.56662 0,0 19.05505,26.99133 31.70509,40.38174 13.23617,14.01101 22.30526,20.83764 31.41647,21.76092 1.43424,-0.82309 7.40129,-1.68002 7.34026,-10.72394 -0.15576,-23.12194 -11.36187,-45.22583 -29.63856,-90.62974 -19.61159,-50.63172 -18.35358,-70.8008 -15.40573,-82.51823 3.2003,-11.39589 23.90241,-15.20102 56.06034,14.02327 16.67758,14.51651 63.68232,79.27531 63.68232,79.27531 1.61655,-29.26616 3.35639,-83.29357 3.93193,-94.49265 0.50479,-9.82216 0.9365,-25.37989 6.97597,-31.29345 5.58885,-6.16681 16.16917,2.01233 20.44848,7.27808 13.82999,16.9384 28.95021,42.73217 39.94789,67.38858 5.52802,12.39365 7.19636,14.0758 7.19636,14.0758 10.21372,15.99561 26.83623,22.29498 43.47234,9.60047 17.22922,-12.37696 18.86233,-39.5161 5.90877,-59.10686 -12.19832,-18.44853 -46.81572,-60.68508 -46.81572,-60.68508 25.99117,59.38686 29.26015,69.78874 24.93481,76.90696 -2.55451,4.20376 -9.23472,2.53271 -18.62518,-12.25473 -10.22337,-16.09885 -29.00301,-50.62021 -29.00301,-50.62021 -16.7998,-33.63664 -27.32697,-57.88867 -45.88056,-58.58233 -12.12127,-0.45315 -16.83064,13.20079 -20.56788,19.90042 -12.48341,26.89685 -12.16354,57.90456 -12.97159,78.29705 -0.30402,7.67282 1.97212,28.8317 1.97212,28.8317 -20.47807,-56.28447 -54.31805,-88.73667 -83.70503,-103.69705 -35.59167,-15.91461 -59.63247,8.12927 -54.45439,69.68472 3.05842,36.35714 4.84571,54.2723 29.42632,99.94879 13.46673,27.33625 14.45444,32.1307 12.27766,33.39096 -3.06461,1.7743 -24.54468,-26.26535 -30.69511,-33.79814 C 113.7787,297.75584 76.558885,245.89777 76.374542,245.63733 z"
id="path4237-0-2-0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#0dbd8b;stroke-width:2.88823676;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
inkscape:connector-curvature="0" /><image
y="-123.97097"
x="1859.6594"
id="image4081"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzdvXmQXNd5H/o7d+99m559xzIASIIAQYgixU0LFTt0qFgynKfYjpO8OHlJlWzHieOXvFTJSdlV SSpxUlHFjhK9smP52Y4p+UVSJEsWaYqLxBUECBDLAIPBYGYwW/f0vtz1nPzRfbtvd9/u6Z7pAYf5 qmb63rOf833f73xnvQT/exBp8+x8b3bvRKzpt9ndze9DSb00ykEiN4aTxudz1ecLBBgnwEL1fcgR d7X6PO5g5iYDDlffVxlwmgGLDDjvFAo3QfhQCsSHSQCaNdnB8HNcndF5Lh4PcNHoOtnaMrhQKEjS 6RSh1MsBQCBASS5HXesdDHIsn+cYIQVGCMc4TqCEcIznx2kicY0BIq0IR5w6hMIpEB86YfgwCEAz wwlwhgBZDlA5wODm5sLc1pbBWZbOU6rwjFmcxxPlFMXkKC1wPnHMH+WHYmExFIPpCXEQgoSjnGUJ hOdNRihhJkXZ5PR8mWTTql7cXszNJznOsHw+nqqqQMtlzSJEoBxXojwvWSl+nCKRp0CAVgTieYoP oTAcVAFo1nauzvQQH48HONNc5m2GK0pIkKSMMMU9MBIUp096Zfm4xEnHeCZOgOPHCUOkMXnWxBVW YxOz/xOmU8rWGOhdk+q3VGpcU63tKwvW+SuUclo6nTNDIdEqFlVreNhjzs9nKHDcchGGAy0IB00A XLT9HAe8ziN+mI+Yy7xpSgKlmiDLvDgZnBkYku77mJ8LPCZB/CjH8bOdk2eO/w63ZuY3+TtdCLGK JjPfUa3y6xkz9drl1Fvv64JX59SsyfO62UYYqKMAB0oQDooANDH+DFfR9rwQjQZ401QFyxJFWebF +4JnD8Wl6Z+SRM8nBcbPMYDrLgsXrW/56cx8pz8DAMYAwtKmZbyetzLfuG689kKyJJXDUt6QJK+5 VAybSCxYwKPWQRWEgyAAzr6dAwI8cI2PRHyCxyOIuh4QR/lYbCL04HN+3vc5gUinAdax3LwsQQn7 IQU9EP0eiF4ZgkcGLwngZRGcKII4U2AAtSxYmlH902GUNOjFMvRCCVq2CC1XAGMOMWGtwkIJUgYt fTtPt//kWvGV97JZqvO8YYyM+Iz5+aJ5EAXhgxQAB+PBAU/zQIqfnubEZLIghsM+aVY6/eCwNPl/ CZz8aQLI7RIS/R74hiLwDUbgiYcgeKTuS8EcD269NaugAKMU5XQepa00CpsplFNZUMuqRWhGCkro zbKW+4Nr+qt/lNeMvKZl9TaC8IHaCB+EADjhngMO80BeiER8gmFwUjjsk07Ij5wNi6NfEDnp427a TjgO/tEY/KMx+IYiELxtZaMzseq/dtM7Vea3+jMwi6K0nUVxaxvplQ0YJdUlAYARa7Ng5r6yWHzj qymjnK0LwohRtREstBqM94zutQA4NL5i3EWjiqgonMRxHvmY7/GPRfn4F0SIj7tF9kSDCM4MITgx CF4Wd18KN613ujdAfbN/axwGhlIyjfTyGvJ3E7BMs9GmYAAF2y6bxf/3pvnu760lV1KCYOlZJaxj s2QCCxY+IDS4VwLQpPVPC/E4BE1bkEwpIB+TT04dChz9osQpP9YckRM4hA+NITw7DCno23tJuoT8 BnenJHQQGACgpoXc2hYSC3eg5vJoTsxidCurp3/zQu6FP9V1aKJI9XQ6YgCzZlO3cE+E4F4IgHNI xwOLQiSSFg2Dk+KC6D0x9OzfCAuRXwNDA3c5gUd4dgTRY5O99emdaA+Q7x6nnRBV/hUSKWxeW0Ap nW0eP8Bi1ltrxuI/v5xffF/QN7WcfFhHAibwAxP3UAj2WwAckH+YR3xcDGoLkiQNyWciDz0Wk8Z/ gzDhmDMCLwmIHB1H5Mg4eEnoTyl2A/ldxmkM63B3/BS2trF5YxGlVLqxXITpRav45cuZt/9jppzI VtBA14EhEzjfPFrYF9pPAbCZzwNP83Nz6+Ldu5YiiiHPE/FP/mJQCP5DBtIwhg9ODGLozGHwcp80 HuiKYW21viF+Y5zW9FvjMOc/BuQ2E1h7/zqMstowbqDMurVZvvMPLm5fuiyKRTWb1XTgSeNeGIj8 fiSKmtaf4QFeiEQKcjpteY4NnBo/G37qKx7e+9fgGIlLAS9GHzuO2PFJcEKfirSjBlf+9QfyGwWC tYkj+32ITo6DMYZyJluLQgiJBsXwZ4e8w4mckLzBmAg9xAElAUCqu/rukvotAFXGg6v092kxFCor uhDzPBb7xCcmPUf+gOf4GuRzPMHAA7MY/egxSAFv/0rRJcP2C/I7IQjhOARiUQQHB1DO5mBqWjVL Ikmc8pcGhfHhtJl6g5Y3rJkZnm1vPwBgad+6gH4KQJOx94oYDJaVYDDqfTz8qS9EhcF/Swj8dmDJ 78H4kw8iOBkHIX3sifoO+Z0RBM0/XSKIKMuIjI+CGgaK2VzNnyfCAyPK+KeIILycLOWLsrzNyuWT bL+EoF8C0MT8H0iBQFQJBGK+R/w//kUfH/hFkPqcfWBsAONPnoTkV/qUPQ4k5Dfm3yqQhBAE4gPw BoPIJ5NglFbduXhIjD7LQf7RRn4l7ffnoKo+ALm+C0G/BIBDjfkJKRCgcswXDjwSfObfKrz35+xA hBDEH5zF4Okj4IQu13C6oS4YxprDuml9g39/IL9tWo44ss+H8OgwSuksDE0DAwMBCQTF0LMRJfjO tpnakmXG9kMI+iEA9swej3hCGg2vKR5uJPJI7OO/I3Oe5+xAnMBj4okHEJoeRj8Rv7MGo3+QDyAQ EBEb8CI+5IPPL4EXOGiqVWFwTwjSmj/Pi4iMjkIt5qEVi3ZIr8IFP+MXotdT5ubyfgjBXllRZ/7Q K2KgFFFCynDkYwNPf1Ug4kftQLwsYeLJk1Ci/g5J9UhdQu5uIV8SeYxOBDAxFcT4ZBBjk0F4vK3z EppqYvl2FtcvJ3H53U2YBm3IvzF9h7tL/gwMjDKsvH8F6fW7dW8CPaOl/u6V4hvfM81yOZ2e1Po1 YbQXAagzH6+IgUBEGeCJ/yPD574scPJfsgOJPgUTT56EFLy3Vn6vkC9JHKZmQpg5FMHUbAhDwz4Q rrfmKRUNvPrCHbz1ympt6bhj/g7GO8vEGMPGzRvYWlqqRmNggJqy7v7su5tvvCbLfDmVGtf7IQS7 FQDHJM8ZMRjcVARB8X588PP/WuGUv24HkgNeTHz8QQieXa7WuVGfIJ/nCMYng5g5FMb0oTDGJgLg emR4O1pdyuL537uCfFbbscwNzK/+cuAYGMP67dtYX7heLxQh2xv67Z+6Xb5xNRzWyvPzlu5YSKLY Be2mxo7p3XFxenpANk3mO+v58V/1ioFfsgMJXhlTnzgN0dcnS78PkB+JKpg5FMbMoQgOHY1Alvdr HgxIJcr4/f90AbmM2lT++jNz6SZs5ttuqwvXydbS7VogE3R1SZ3/yaX0jTsez3g5kYjr1RlD52pi 17QbAahO9DwtRCLLsqbJ3k9NPPdzAT70r1Bdu+clAZOfOA051IfVO6B7yG9qZFGsaPnsoQiOHIsi PtjHbqgL2ljN4yv//h1Qs3nk4C7EHCOs2Y2B4c6VyyS1vuqQdXplvvTq59byhWQ+n1aBcR0475w2 7pp6FYBavz83d0nOZGTP/f6zDw3Js//D3rHDCRzGn3oQ3oFQj0m3oU6QX/VzYz7hgPiAF7xAGuO0 ESLFI4LnAUniIYk8OIFAUQT4/BLCUQVjkwEEgr13Zd//xgJe/4vlriC/bf0YxcLF8ySXStY8dEv7 2mX9tV/O59OlbJaqwKqBXdgDvQiAvW+PB1alQCCiDEcHB894nvk2z3EzlRAEYx+7D4GxgR6SbUNd aH3NqcW/czfRLq0GM8HFbpiaCePZc0cx0AOS5LMa/sMXfwRKqbvWN0F+o3/d3aAWbp1/kxTzmVqQ rJ77x28Vf/iHsqWVHCMDCz0IQC+dYBX6eTEUEuVg0O95OPBjvyU6hnuxE9OIHB7tIck21AXDujH0 XJnc4t+B+U3pZ9Mqbl5L4pEnJ7quiqwIuLOQRiap1h07QH5DIRxuPDj4o3GkN+8Si1b2Ikqc+HiA k1/cKG4lQ6EgLZXOUuBqT11At9NxVe0/zEejiihJAflB6ZlzMuTP2AE88RAG7p/qJe9WqoEXa8+w nTZtuKKBg7sNwlV3Zw1O7REkuItuYGI63JA/B465Mt9ZCJf8FVnG5LEHa4UmIN4Baew/jSvDEU1b kIBFAbVZ2e7QvRsEcEB/UoxEPMqU9PDcuH/8dwEiAgAvi5h8+kHw4h42cHQB+aw5rJvWN/jvDfKd cbxeAR99agLPnpsD3+M0dj6j4fp7CQDdQ36jf71NFK8XpqGTUq6ynExAYn5pwLdlZl6R5TRV1QcZ sNT1iKAbjlUNvwA/PT0i5nIFZXrw8K8zRrxAZVV/+Ozc3sb6fYf8dhrcGscN8v1BCeGIgnDUg/HJ AKYOhTE44tv1qiXhnIzvwHy3Mjvc7eYZmz3Oipk0KRUqW81kIv78Id+hr1/ZfvfteBxWInGOAs+7 qVEL7SQAtdO3c3OXxNVVXX588DM/JnLiJ+wAodnR3Rt9OzKM1V930WBuWk8I4PNLCIUVhCJKldFy 9bfiJvRzoQpAuWh2bYM0+rvXnycEU8dPsWvvvELAGBghfEwY/BeiSD+n64tm0+TQngWAA17n19ch DkXGglFP7J/XCiKJiD8ws0MSbahPkC8rArweAbIiQJZ5KAoPRam+Kzz8fgnBkAx/QEIgKMEXkPo2 49ct3bmZcXASjvLvjGCsxb/y4vX4MDg+g82VRQAMPOEffiD4xLnz5Vf+YG7OY87Pn7OqKEDQQQg6 CYDD8IPIcaJ8yvuJX+TATdkBBk/O7m5/fkcNRgvkK4qAyakgBoe9GBjwIhrzVJjpE3vuj+815dIq Ft5Pdt9NVd1bUa8VQUYmjrB0Yo3oahkAEJQi//ekOvW91bWNdWDRRoG9CkCIN4yieDR2YlIR/L9g eyqxAEKzwx2iu1APkM9zBPefjOPM2RGMjvl7Xpg5CMQYw3efv8lM3V4hrP3r2E210/rmNhN4DmPT J9jt6+crQwpGB8a8c19IWPlfF8WskUqdM3dCgXYC4ND+rMCpIWlKOf73aufzCMHwQ3PoaWG/B8gf GfHjc//HMUQifdwxdI+JWgzf/uN5Nn+xYv33aui1vLRBkNjAEJLhGHKZyiyhLMg/MyQHf3s5V1wF Fu3VwrYC0A4/HdoviOODQ0Me3vPTtmdwIt7b2n6t8jswnwEzs2H8rV84+aFm/p2FNL7yb95mF3+4 Vqlnh7G93Sa1YHC42+Fa0KCxHcenjzPbXieM+Ualmb9tWUSKRrNC5QhexcutrG4I4LT8+ZUVTjqk nPo7cJzciR7rciasS8vcrpcgcnjus0cOfL/uJGoxlEsGttYKWFnMsesXt7C5Uqh47tHQ63bk4/MF EAzHkMskwBggEO/PjwYHvrxVuq0Br/PoMCJo1wXULP+x+GxY4b0/Y0f1j0ShRALtW6SpcL2M7Q8d iuxqpq0fpGsWsmkVhZyOfFar/OU0FHMGDN2CYVgoFw2YJoVpUJSLBgyNgpmOCnbJsL1Afqt/5WF4 7BDLphMEADhCQlPK4Z/NWqn/ODdnGtURgWtX0CwA9WtZ4gneKt0Rj8uP/RwYCdsBoiem0JF2LHDl X6uVC4TC+898Q7ewtpJHcquExEYRyY0iEpsl5Dut27sg2E4reO3Saj+30UHrW9JvigMgFIzBHwij kM8AYJB47//Jcfx/3djIa0DCQKW7t9BEbbqABDftywi6yEle0XPOztQTD3Ve5u3B0HOT8HSq3D7t XVK5ZGD5dhbLi1ksL2awvpIHpc3lc2/8tuv2e5zO3Svkt2vf4bEjbOH6WxUUABk8Kp765IXij74B XONRYX63CJDndL0kHPc9+SDPhMO2Z+RQh5W+XUB+c+UXFzLI53QEgns/G7i1XsTLf76EG1e2YVlW G4FszB8Nr22YzwjbC8P6BflON6vqHooMQJY90LQyGABFln8yEuG/I4o+IZ0+bAILLd2A09pyGH8F PpMJiGEy+rlaQIGD323Klzkr71Yh1pn5rO5uGRTf/dZCax490g9fWsZ//ffnce1SApbZjvmsJf+2 kM92v4LXDyu/I/OdCTOCyMAIqpfaQCLyp0Q2FLMsna/cqdg6Emg2twmQ4DY2ysJg0JJlUfnLtkdg PN56cLMdJDkK7JSPun/7Brv+fhLvvrXeXM6u6dI7G3jxfy7Csmh7DWsD3wyssk+vSSBaIL8TfLso hHv9W/NvJ5ANylX1t1BlflP+sYGJeizGpElh9lmPJypUruJpuJepWrc6VT2u8ZYlCUcCTz9NGDdo e4amm2b9nJVso2FOgW5piQ4N9t1v3cLqnRx2QzeubsOtwXbK3w3ya1q/K4ZVn1o0uFP9m9OHa/va kO+Wv6L44fUFaw4eQfmMKBIxElnmgcMt4+umSYJz3NxcmKNKSAhxvpr2C14Z3sHGTQ2dC7ybTRv1 R8ug+O+/fwVbG0X0SlOzobYC2S7/dpDfGUE6M8yp6PX84ZJ/U1otAtOYfiPkN+dfafvYwHgtQcL4 jyg0NFjpBsbt09s1akKAC0TTPHxYyggCLz9iewRGByrTvm0Y5iywUz7q/js3WHOFSgUdX/0v7/Us BGceHcOhuahL/s1lqjDeFfLZDpDf7OZofLtK3eTfWSEa828H+W4KGYkM15wZY9ywOPYopQpfHQ0A ji7A6cABXsGyOPlo+MxsTBj+FTuH2IlJyAFfQyaNBUZnrW8ucK2SbunUHQyd4uqlBCamgwiFu5sa JhzB/aeHoOsWVpdybcvcbmxP3OrnVo8Gf9YSzOnefZs44jjcXRnfIS2eF7G9vUos0wDAwDE+s8zu vmhMyAa2P0GBq7VDJLYAVDd8TguRCC8fVR56TublZxiqO35OH62c5u2p8tV/HeK0rbzjx9ApLl/Y QjAkY3isu/UHwhEcOhbF+HQIGyt5lApGY7pdHMhoqUdD+R2erDVYZ4Fpp0TuCtFW65vjNLmXSjlS KlW2jfE8fFm28FUha+mqumkBqRpuOCzDc/zc3JqYKHqU44G5v8cR8TgAyGE/YnPjbaW1N62HO/Nd 4jDHP2YxzF9JopDXMXM4DJ7vbq0gOuDBmY+NIRCUsblWhKoarg3Wy+5cV+Z3Vb8OabUZ27ecL2yX lkv+zKJIp9dJxZtESgx/lCtvZjXNtIBcbV3AFgAO0HlKLTGgaJ5Z5aFfJ4QLAEBoYhD+4ahLhfp7 pVpDkDYNtr6Sx9X3EhibDCLY5bQxIQSjk0GcfWIMgZCCxEYRaskE8CGGfNf2bcxfFCRsbN4mVS4R kXFX1ktbV3S9bALF2h23DgQ4yUtSQh73Hx0ZlGZ+1U4pdmwKsvP+nrabFjoUro+7c8tFAxff2kB6 u4yp2TBEqbujDRxXEYRHnhrH6FQQ6YTKChnn4U1nXjtDbmv9e2WYu0LsCvLdujReQCp1l5imbgdZ u1u6/ZIRPmygtFQ7RuZAgJgQizFpRnjoIa8Y+mk7s8H7Z8BLYi2jtlrvWmD3wu0E+d002OZaAZfe 3oCsCBga7X7HECEEsUEvTj82SoYn/CSb1pBL73yKt4H5XdWvQ1r7APmt+TPkC0lSVitL0xxopqis /Q9Byxrl8ict2xDkUTtIQAWvNyyPK8c/qfDKp8AqN1oNPngYpDp93Jb5eygwaxenoULuCKJrJm5c SeLS2xsQJR7DY/6ut24TAgwM+XD6sVEyeyxCSiUD25vFjvm31r+D1jfEd4njcO8H5LvlXyrlSL6Q QoV3BJvGrf9mGKzBEKwjQPys4GUFedZ77LMikc4AlZu8oofHGvjTtpIN/u4M2w3kd4MgarkiCNff S8AfkhEb9PW0Wy0UVXD/w0PkyP0DRC2b2F4vtTR+A/N3xbBWhbBq1WtX/94UojF/Al3XkM5WDEFw XGCbpX5HVQuqpk2Z9uGRqgCc4+cmbgmMUWVKeeBvEQizAIN3IITgxFAPEt6uwK1xGpjfE4K0z79Q 0HHl/CauvZeA4hEQH+7tMEcgLOPEmSFy4uFBYhgWEncL9sSmI/92AtmhLh1W8Nzr15RWTwpRMel4 BlBqYWt72T4ezZmm9Y20WtzS9Q3THglUBeA+jtJVyTCgHA6c/ocEiAKVGUDfYLQpw14Z1lhg1i5O Q4W6QxDns3Nip5jTce1iAu+/tcV4AWRw1A+O714QvH4Jcw/GyclHRwilwNZKobqHoDPkftCQb39a ga++ckTA2tZCbQqXUu41VVqbz+ejJrBuoSoAHKDzw8MBkRBNnlJO/hoBkQAgNDkETyS4A+S5V9Kt wKzx3y4lvDH/Tps21JKBhfe38fZLq9jeKkGUeRKOebruHhSviCMPDJCHnhojsiJg406ufgmUs1wH BPJ5AFzVnzACnuOxnrhFKKscTbeo9W7S3D5fLK4b9lCwKgAnecbWRI8n5p+STvwjVOeKw9Mj9Sng ngrcWEmGJvfdQP4OWu9kfPPY3jIpNlcKuPzmBi6/uQ61bMLjE4mvy40nksxj6miEnH5yjIiygM3l fFUQDhbkAxXGk9oOYWAjeYeYllEtA714J3vzh0Y0qqO4SQGw2o4gxigZwHSkkmIlNU4U2ldypwK3 aP1eKtmaf7vpXJ4niAx64fWK8PhEeP0ivD4Jsqc6TqyGvfl+EhsreRy+LwZvoDtB8PolPPXcLPno M5N4+y+W8eafL7NiTt+xfruD/B0UAgBYRevtV8LqjK+6QBQkqHoRDAw8WMTr9RIvjZIEzgA4DwEA ARYIY34Skj1+OzUGVI5774FhDZDvWklHWm0r2RjHqfXBiIKJ6RAGR3yIj/gwOOon0QHPvp8ikj0C Hn92Fo88M0XefXkVr//ZHZbbduxn3I3WO9276lIdWo/6Si2p+tlCIPACajsxCednTCGx2DpJJCrr Kg17Ai2TyBDredYs6J6ksoPWd1XJ9nEkhcf04TBmj8UwOxfBwFCfLqHaJYkSj0eemcLDH58gL/3p Anv9O0tgtFLYe2Xo1bS+FqTOfMIAQuozpYRjLQc5GwRAEgWxni8D4bguGLa/kE8IwczRME6eHcHx U/Gup373m9KJEhavbGN1IcuWrqaQTdYRoF/TufVnVmNuJ8h3RYLaNzkYGCMtfV2DAFBmh2aNofZp Orelko6fcNSDR54axwMPD8Hr7+MXRHZBlDKs38nhznwaKzczbPl6GuWCXg+wG8jvSSGatL4D5BNH eMLsnR8VT4603gjTIADEMA04MYDRNhVqLPDeIL8x/YEhLx7/1DTuf3jonp/jrxeJYf1OHotXk1ie z7CVGxloZdMZwPFc+TkokA97DFB1o4zWkqQUDqmtUIMAlJlVC8AYwExrxwKzxn+7hvxASMYznzmM +x4a7O8HJLqkfEbDrStJ3L6aZovvJ1HMtWp4Ow0+GJDv7scYrWdJTFcBYMBhRsgCI0QvVcpWiUIt 2pZhjU67h3yOEHzk6Qk8/eMzkPbx6tZmMnQLd26kcftamt26nMTWaqG1zPViumu90/2AQH7lt/5u WjZyMTBwZUJUlkqNMCAPwIEAhHDsLjYzE+xErWyWZm+l2h/IHxzx4bN/4z4MjvbxGvkOpJZM3Li0 hfmLSbbwXhKG7kS43hDsQEG+I61mJDAtrZY2Y1a6VCqxoj/FgEsAHAIQiURZNrtaZB6mERAZACzd 2DfIf/CREfzlnzq671Y9pQy3rmzjwqtr7OZ7W7AsN+Z0j2CWM44z7L5DfmPf3gL5jk8sOwXDMDVH ZjTj9wdYcbOec7ULWGXZLGWEcBTANoBRgMEoN56Y7QfkE0LwE39tDqcf7cONoh3IMinefe0uXv/u MsskS61l3QXDDgTkV9/rz+4zgIQBlFnQqb3ZhcEwjWSxWGJAqBayigCnGSEXmNfrYybMOyKEUQZA K5b6CvmEEHzmrx/HybM93i3UA1HK8N7ra3j1W0sss13uAnI7lNnhfs8hv4mZdbdOkN/oV9IqG1xs m85k6i1CBAps1mpdFYBFFo/zbGVFp9Rj3Ga88CgA6IVSF1rfTSUrP48/M7WvzF9byuGbv3uVba0V dgfTbSG/nQZ3SKtbyN8Vo9tDvvO9qOedayYsLWwuElJgwGEGrNa6AADnMT9/kvp8eWpSc1GqdkRa odiZ+T1M545OBPH0j+/yTsEdyLIYXvuzJfbqtxbv7bp9D2l1o/WVZ2C3kN8cv6Tm6+UgLFEyU7lo dIDmcqsNXQCr/ImMEJ6WoN3yVr/vaBkmTF2DIEq7qCQa/D/92cP7skijlk38ye9cYkvXUh3z70Xr gQ9ubN8ZCXpDiZKerS3mWLBuc5yf5nIFCpxmwAIDGs4GbjKOU60sSzQc0NfyJZcKORqmE/Or/oOj PkzOhtFvKmQ1/P6/O19lvqMwTfl3ZJgL5Ft2Wi31a0qrwb+NQjjTsiGfVQy91v6bNGl543s75hPm 3kUU9VytMJZlLamqTuNxngGLtRLWVwqg0Gh0gKZS76+A0GLFkaGUyjQ1mJuGtW8wBoaj9/fhAxJN ZFkMz3/5Mtu4k99BILtnWDcnbztCfluFaN20URGC7plZ92sNb78TVsmLMAKLmsir9Q9PU7DrPC9a 8/MZCpyvld4hAKdZLlegZoAalmWdt9eQi8l02wZDhwZznrwdHOn/RM/L31xkKzcyOzAMLgxjrgzr 9uRto387hXDmX4F8Vyu/hgROP9LE6B5Ropp1upwErU4DMwAlI/t6qaRbgGjvaWNARQCq0Z9nKX6c plK6VYL2uh2klMqAWo4ZM1cJb2yw5n16/V7UKeZ1vPn9O23zPxiQ79D6GqNbmVlHglat3xHyWRuU AJAqbhBHFVKL5cXrHFeilRFAvXYOBABDIk9DIdEq0tSP7Iak1IKayfUE+c0NVvt+Xp/o2vnNyjSu q0B2z7D9hXx0Dfmoam5byG+j9fY6gJtgbZc3awUzif5mIEANnpcsIN6qUNPLAAAUkElEQVRwYaTz mC0DArRYVK3lwnuXwFjOrlJhO10L0Q3kO/05cGx1MesMvWfKZ3XmzjC4MIy1MKyXyxYa/Vnb+u8V 8sFsrXeBfNT9635o8kMtLZOZyJW3a4U0TfONVEq3Uqm81fwhiSYBiFOelyxLLOsGMd62Q+YTyXol XRpsp/t15t9LolRoWYncNcWGvPV9DjsyrNGt0/06tYcuEWTXkN+Gmfa7K+SjM+Q7USJZ2AB1fEg0 z3KvcZxsVvt/Z60augAAz7NUKm/pumyWdPUl26uUyUIrlbqG/Ob7dSyD4uVvL/UNBU48PISJI+F6 hl0y7MBAfoOh1ywM7Q29TpDvRIm7+VsOo4suJ9mNm7GYYgFKy33BTQgABoi0XE6Z68bFbzLQWued ubveELLXK9XOv3wXi9e20Q8SBA4/+ysPkUeemQLnunG1Mf8PDvK7Gdu7z+W3Qwk3yK+FB4Fhatgq rtYKqlv6Nw1DMWS5bFUmgBoFwLkWW23JQc7r5XhLYtaoMHWaI8IhANBVFbHJCRCQVq3HTpctVB5u XtrG0QdixNflPvxOxPMcDj8wQI49PETUkont9WJtR64z3w/qQIYrhMNd6+0Vf1eUQJvwtjCgUbCW 84vYLK4Q20jKYfufbRXSa2trfh14wURTF9C8GE+AFFFViXg8Pj4sDBKF9z7LwGCZJgKxGETZcTNH O61v8K+7mwbF1Xe2MH0sQgJ9uhjaF5Rw/OEhcvqpcRKISMTUKXLbFeDalwMZVeY0H8Oq/PbOzAYI RyMz7fdWwXIgQZNgXdl+i5TNYrXI9L35/KUvRSJeLZPJ60CqpQtw241BgLNEENa5vFVeH1Omfh6E KBUPhmA83tA4vd6vY+gU77+5AX9YJsMTXVw73yXJioCJwxGcenKMfOTTk2RyLkxiwz5IMg9mMpRL 5u603uluM99mTruJmmq07piJ2nuvKFEzDKtlKxl5XN0+XzOQNaZ/+W5+5c1EYlgDiAmst3xi3u22 cArE6fDwurmyohfViPodBZ7PAwyZtQ0Mzh6CKMt7ui7d0Cx88/eussWrKXz63BHiD/X3mniPT8TR U4M4emqwZgzpqoXkegG5lIrctopS3mC57TJKBQO6akIrGCgXDOhlE9Ri0FUThulAy11s1ao/t/bn 7VCi5ttund9FUOzu4EbmErG7Z0KovmXc+QbPG0blM3KrDRyql6KVqlfGnZECgaLnvtipB8blmRcZ YxwDEJ+awtjRub5dqaZ4BDz5V2bIw09PQBAP7pdCGAPUUmWPJGGVjSf2VnGtZILRimBbBoWuWihl dZSyOgopnWVWS9heKyK/paIzo+vvNc12EyyX7qRsFvH95a8TRisobzHz+bdL87+k0GwxlTqtAs+3 9P/V1FqIAOCAw0IkQhWfL+B/SPnkVwRO+jEA4HgOxx5/gkmitKt+lTX411+CYQUfe3aanPrY6IE5 /dNvKqQ0rF5J4/a72+zma1vQSpYrM5s1u+7WHgkuJd8ki9lrABgICE2oW88k+VuX1tYCJeC8jvpn YxqoXUsTYIaoqkAoLXF+yb8WECKfBxhhjIHjORIIO6+O6wz5zcGc7rabpppYeC+Jd3+wArVsIRL3 EMW7i28SHmCSPAIGpvw48tFBcuavTpHwiELWrmZhqqwLQ8/FlqgKj27pOJ94lTBWWbMxYXxnjSz+ XqmkqqqqG27Gn00dVG0dwHEyOkq5jJXcHhanz9ofjdTyeUTGx8ETHi3M3wHyOwsMg6FRLN9I480X lrF0PQ1qURKOe/63QwWOJxg6FMT4/RFy5c/X20I+aQP5zuHg9fS7JFneAFBp9ZSW+uWN3MZKLufT AK9t/PUkANX8YsjIa4TmCRf2RDZ8XPCnAcCiFqhpkNBAfNeQ30lg7NdssowbF7bwxneXsHwjDbVs kEBYgezZw1fKDxgFBhRce3EDWt5oC/n1cX8rSpSMHM4nXiGsMjsDC8ZL62TxP1uWVlbVYQN43b4T 0JV2UKt1oHiW+HwZUuQzW8P81GlCuBkAKBVyCETjkOx5gR4hvyWQm41qp8WA9GYZC+8l8cZ37+Dm xQQKeZ14fCJ8H9BXxvpFxbSOd/90BZZKd4B8wA0l3t76ASkYOTAABMzKYPsXC1ZiOZHI64DiOvRz UicBqArjEtSQRPyEJwKTLgX5yOdBiAgwlAs5MjBsf0Nwd5Df7WSMfb8OA0MureH2lRTefmEF7764 go07eRSyKmEUUD4E3xO2yTIovvHrl1l6uTJx0y3k236rhSXczF4itsqZVP9v6+z6H5ZKpqqqig5c qd0I2q4MO+3UqI0IolHIHBfynY08+k8V4vmCHWB87j4WHxnrC+S3i9PLdC4PguiQF6EhD4JhGcGY B6LMEUnmwfEcZA9fG76ZOoWpUxiqxcoFA2rBQClX+dWLJkyDwihb0MsmmMlqzcVzBLExH577RyfJ 4PTuJrOym2V8+zeusM0blS+j9DoCoJTi+3e/RopmAQADYyy5TK99IpEtruXzvjJw3qg23Z4EAHDM CwSDm8qoND0wF/nI9znCTzIG8IKAuYcfY7Ki9A3yne77eSBjt7tzx46G8dCzE+S+p0Z6PslsaBYu fmsVb//RHaYVzJbhnLMsNSRwEYYLyR+RxfzVWroFWvzHS+r8/xeNmsX5+ZMa8Lz99fC2zAc6fz3c JlZJaNYcGSkYK6t3s3FP5osxJfa7AINlGli6epHMnfoIA+H2R+v3mFb92Y7T3f06FbdK04/MBDH3 +BA58fgIwkMe9EpqwcCVP1/DO19bZaXqWkWniZ0WJHAIylpxBbfz1xxVs84vFC4+T6mpzc9Ts/qV 0I6aX6tfl+WvosDTwvR0RjFN5jslP/ElgVN+0g4wNDmD0ekjDguwVrq6w73S+pa86loPdDed6/GK mDk9gJlTMXLo9AACsd19zHrtWhbvf3eNXX9pE5ZWt8c6M7qxLDXmM6BsFfH9u18nBtXtUKU023ru ZmrhUsGXL2HzSaPdrJ8bdSsApPrHA2fEQKDoiUYHB09JZ79FQA6xaqkP3f8wC4Vj9Vg9GnrAB3cg gyMchqeDmD0zgNlTMTI+F+npdtFalpRh9f0Mbr2ZZDdf20J+s3H6t5PWd4L8CjspXtr4nySlbdXy s6HfsrRSKgUNWLA/Gb8j84HuugA4EqPArDk6eknPZDLbSax+ISaN/f8ERAYD7sxfInOnHmOyJNej 7QbyexCYvZy8HRj1YebBAUzdFyXT90fh2eU+hexmGXcuprByOc3uvJ1COWd0NObqjG7u/zv5ARdT bzUw32Dmn6yzW3+s63ktm6UGsLqj0ddMvYp4rSuIRJZljZe9j4ae+Dt+LvAv7TwVrw9HTj7CREHs mmH36uTtwIgfkyeimDwRITMPxBCI7g7WM+slrF7NYO16ji2/m0JmveTK6G5W8HaCfDvNW7nruJB6 zcEv69bdwvxPrOvFRD6fVoFxHTi/7wJgdwUcMC6GQpwSCES8Jz1P/GcR0k9Uyw5fMIwj951lvPOa uXsM+QLHYWgygKn7ohifi5Cp41H4d7EJpZw3sLmYw9ZiHus3cmz1cgaldLX/bepO3Ji5F8i3BWS1 uIQ3ki/WoIswFLfZxmdvpW9fGBvj1V6s/mbqvZOrCcEZHliVAoGIEg77wqfkJ75KCPeonX8wEsfs 8YcYD3JPIF+WOYwdCWNyLobxIyEycSwKxdv9lLFaNJBeLyG9XkJyuci2buexdSuPfFLripndjd97 6Q4q4ZPaBl7d/A6xmH04h5kFs/h3l4wbf1bp98d14Ad2v99x1s+NdiMAdjwOOMfH4wlJFNeUGDc8 MqWc+hpH+KM2Y2LDY5ieva/eXH2E/HBMweSxKMYOh8jEXAQj06GORptaMCqbQRIqcikVpbTOMptl pFaLSK+XUMrqOzBz90u1zvjdQj5hBBk9iR9sfrtm8QNgBVr81dvlO39oWelSOj2pOZjfE/TbtFsB AGqfmjkszM3xUiYje8aEqdkxz+zXCbhRoLI4ER0YxuzhkwyE2/P9OgJHMDgRwNGHhxCIyA1SRS0G rWTCKFusXDSgFS2UchoKCQ3ZpApTq39FvJfLFnpl5m5RohkJEtoGfrj1PSfzUbbK/3oVN7+Uz6dL 2SxVgVUTXcz2daK9CIDDHnhaiEZXJU2zPMfCD5wY8c5+jTDUvjQRCEUxe+whxtn31vYB8rux8t32 3aENMzvNAHbPzL2hhB3+bmkZbyZfJBarX05pUOMPl9i1f1Jhflzrdqp3J+rTIvsSK5dPskCgwDaL G5mgEvyhlwQ+DcJ8AKBrZRQySRKODYNzXF7cbyvfObHTjvm72Z1bN9TahG+DEu2sfLcNnXbqtws3 8db2S4TafT4AE8ZX1/I3/5+0mijlcroOnDKrX/3aE/OBvgkA4BSCtdzqtkf0vOQRAp/kQEIAoBsq suktEgwNQODt8XZnyG9wqz3bcdpvze7ETPu91925XTOzqSwNVn5TfKdgMcrwfvYdcjn9JnEcfGRl U/ut68Xzv1mwssVcztCAJ43dWvxu1OdtNnUhKKCQMQzjeyE58jhA4gBgmjq2E3eJrHjhqX+aYBeQ /0EdyCAukO+CEg1pN6FEU3wCQLNU/Cj5fXKneLNWdQJCNVr+4gpu/E65nC/vB/OBvgsAYAvB+HiG rmxvFyhT/ywsDXyEEG4UABizkNneILqhkWAwBuI8nugqEE6/D/pARiNk1/1cUKKp/3eDfABIltfx 8ta3Sdao3+ZBCNGyNPdL67jyx/l8vlyB/f4zH9gXAQCAJba9HWEzMzzLGXk1icK3ohiI8IQ7abdB qZhDLrNF/MEoRN6x+bOtDXBwD2Q447dCvjtKUEZxNXsB76RfISY1alWmBMs5mvmbK+qtv5Bluby5 6deBoAG8YI/z+8b8Ss36T47RwWEeGBeDwQUpGIx5JskDz4Xl6L8ijNmfIgMhHOKD4xgdPcZ4zjlK 2NnQgwsz0YGZdvHarfO3hu/vdK6d9nZ5A++kXyU5I9PQcBboiyvlhV8pkfKGrhe0dFrX+zHU60T7 vNU2xYCzTNMMGo0yulG+u8Bbxvd8QvgMIWQIqMwWFIsZbG/fJZLsgUf2oztDz0YCONxIk9buAiV6 MPR6hXyDariYep1cSL9ONKrWWokAlk7Lv7VovvPPLE5Lh0KWuro6rgOX9jTJ0w3tBwI0p80BZzhg U4hGFVHXIUcioeBx8aF/KnDKz4PRhvnaSHgQoyPHmEfx7/m6dNfwNkq0tQ3Q4/i9fXdgp0WZiVu5 67iWe5fotPG6HEas61k9+2vz+sbbopFQs0pYx2bJrBzn2l/mV0u/79TQJcTj46KmLUimNCTPeUdO DApjv8kT4ZFKUHsARBAODmBseI4FPeF6Qdsws+7XKzP3dzqXWhZuFeYxn3uPqFaxoVEYWFk19d9e K1z77ZRVzosi1SuQ/6hVNfYY9pn5lRrcGyL1v3M8sChEImnRMDhJFKnn/tATPxPg/P+EEVbhdtUG ICAIBQcxPniEBX2RJiRo7Q7aocS9ns41LAO3CtdxM/teA9TbZDHre1vq5r9YppklQd/UZPmwnkjA 3Ou8/m7oXgmAM68qGjwtIA4hpC+KkhSQQ2JsZFqe+2WB8D9NQGoL9TYmeJUg4pFJNhgegyJU9uTt z3RuY/xuUQKUYUNdw0rhJrlbugOL1S17R13ez9HSv9tit75PaVlTVaqnUqpR1XqnlX9PmA/cWwFw 5ulAg9f5uTmfsLZGJFn2SQNibHREGv37Hk75PCXwNkZlIIQg5B/EUHiKDQSGIJDKENK9/99L374z SjAAOS2N5eICWSksQLXKVc418o8S60JJL3/pLi69YFk+zePh9KXyuoHNQHNf3xp5n+mDEABnvlU0 OMcBi0I0mhUMQxBl2SeFpcHhSWnwF0Te83OEoeGLpqg+ERD4PRFEvHGE/UMs6omBI3zH4eBeIb9o 5LFVXkeivEaS5XVotP69wOYSWsx6q0xLX7pLL79sWT5NVTNGVpo1UIH7ZiPvnjLepg9KAJz5O/4O 80CItwVBUXgxKnpCQ+LcTyiC97Mc4z/KwOrbjGo/lQeO8Ah6IvDLIQTkKPPKAXgFHxTB0zBk7GYF z2QmVL2AnJFHQc8gp22TtJ5AyXRcoW9HcRaGsE2dGd8q0NTz22zrSiZT1AcG/MbSUtgEYFYuany+ eVLnA2E+8MELgE1NglBBhLm5Ar+xURYUJSyKIhHj/MBkiBv/nMCkv8oTcgioM79OrYzhCAeJVyAK CmROAk8qI0+Bk0CZCcoYGLOgWxo0qsOwytAtrSFN1pBoYx4MtMyY8UJB17++jdsvb5m8FpbyRrls Gum03wSi1kFjvE0HRQBschGEBIc4hIi5zJumJFCqCbLMi1PS8UMeIfiIIooPC0R8CgyDbt1EIzXe auMe0iVOg1flAgYG67oG40eapr+aMu++YXiyhXTaMnheN4eHPeb8vN8CshYQsm/nPlCMt+mgCYBN thAA9Z1HHBDi4/EAZ1aFweuVeEnShHyeE+dCR++T4PuYyIn38+COMsIfIszxsWTWjBVtILwhRMWN AglQOm8yY94w1bdydPVHm0Ymw/OyWS5rFs/rJs9L1atYjzu1vbl/PzCMt+mgCoBNTmPRgQoXCKBy QJSLRku8Zek8pV7O65V4yxJ5SrO8zxeS4+bAKA/vpCx6xsFYnIcQ4TiEwEjl/npC/ABUMNNkBAYF y4LStAlkDKhrFrFWMjR7R6V3MzwvWKmUbhHCU45TLZ6XrMFBkVY0PUCrTG+n6QeO8TYddAGwibj8 2gJBgAQHLBAMGdxcOMxtbRkcpSZHqZcLBCjJ5ShhzOK8Xi8pFinxer3umRCVlUol5vcHWLFYYoQI lONKNBKJslyuQFP8OEUiTysMX2WVmzdbzuEdeKY76cMiAE4iLs9NAnGBAOMEWCDAEAFWCTAExKNk Lrrets6p1AhLJFIM2AQwziqfV7O/sHWaVT61ct7J4A8l0530YRSAZnITCPvZxe9M9XW2qe6LDDhv vzgZ6cbcds8fOvpfMzvdLlvhNv4AAAAASUVORK5CYII= "
height="1365.3334"
width="1365.3334" /></svg>
;;; ement-tests.el --- Tests for Ement.el -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require 'ert)
(require 'map)
(require 'ement-lib)
;;;; Tests
(ert-deftest ement--format-body-mentions ()
(let ((room (make-ement-room
:members (map-into
`(("@foo:matrix.org" . ,(make-ement-user :id "@foo:matrix.org"
:displayname "foo"))
("@bar:matrix.org" . ,(make-ement-user :id "@bar:matrix.org"
:displayname "bar")))
'(hash-table :test equal)))))
(should (equal (ement--format-body-mentions "@foo: hi" room)
"<a href=\"https://matrix.to/#/@foo:matrix.org\">foo</a>: hi"))
(should (equal (ement--format-body-mentions "@foo:matrix.org: hi" room)
"<a href=\"https://matrix.to/#/@foo:matrix.org\">foo</a>: hi"))
(should (equal (ement--format-body-mentions "foo: hi" room)
"<a href=\"https://matrix.to/#/@foo:matrix.org\">foo</a>: hi"))
(should (equal (ement--format-body-mentions "@foo and @bar:matrix.org: hi" room)
"<a href=\"https://matrix.to/#/@foo:matrix.org\">foo</a> and <a href=\"https://matrix.to/#/@bar:matrix.org\">bar</a>: hi"))
(should (equal (ement--format-body-mentions "foo: how about you and @bar ..." room)
"<a href=\"https://matrix.to/#/@foo:matrix.org\">foo</a>: how about you and <a href=\"https://matrix.to/#/@bar:matrix.org\">bar</a> ..."))
(should (equal (ement--format-body-mentions "Hello, @foo:matrix.org." room)
"Hello, <a href=\"https://matrix.to/#/@foo:matrix.org\">foo</a>."))
(should (equal (ement--format-body-mentions "Hello, @foo:matrix.org, how are you?" room)
"Hello, <a href=\"https://matrix.to/#/@foo:matrix.org\">foo</a>, how are you?"))))
(provide 'ement-tests)
;;; ement-tests.el ends here
# * test.yml --- Test Emacs packages using makem.sh on GitHub Actions
# URL: https://github.com/alphapapa/makem.sh
# Version: 0.6-pre
# * Commentary:
# Based on Steve Purcell's examples at
# <https://github.com/purcell/setup-emacs/blob/master/.github/workflows/test.yml>,
# <https://github.com/purcell/package-lint/blob/master/.github/workflows/test.yml>.
# * License:
# This program 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.
# This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
# * Code:
name: "CI"
on:
pull_request:
push:
# Comment out this section to enable testing of all branches.
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
emacs_version:
- 26.3
- 27.1
- snapshot
steps:
- uses: purcell/setup-emacs@master
with:
version: ${{ matrix.emacs_version }}
- uses: actions/checkout@v2
- name: Install Ispell
run: |
sudo apt-get install ispell
- name: Initialize sandbox
run: |
SANDBOX_DIR=$(mktemp -d) || exit 1
echo "SANDBOX_DIR=$SANDBOX_DIR" >> $GITHUB_ENV
./makem.sh -vv --sandbox=$SANDBOX_DIR --install-deps --install-linters
# The "all" rule is not used, because it treats compilation warnings
# as failures, so linting and testing are run as separate steps.
- name: Lint
# NOTE: Uncomment this line to treat lint failures as passing
# so the job doesn't show failure.
# continue-on-error: true
run: ./makem.sh -vv --sandbox=$SANDBOX_DIR lint
- name: Test
if: always() # Run test even if linting fails.
run: ./makem.sh -vv --sandbox=$SANDBOX_DIR test
# Local Variables:
# eval: (outline-minor-mode)
# End:
━━━━━━━━
PLZ.EL
━━━━━━━━
[file:http://elpa.gnu.org/packages/plz.svg]
`plz' is an HTTP library for Emacs. It uses `curl' as a backend, which
avoids some of the issues with using Emacs's built-in `url' library. It
supports both synchronous and asynchronous requests. Its API is
intended to be simple, natural, and expressive. Its code is intended to
be simple and well-organized. Every feature is tested against
[httpbin].
[file:http://elpa.gnu.org/packages/plz.svg]
<http://elpa.gnu.org/packages/plz.html>
[httpbin] <https://httpbin.org/>
1 Installation
══════════════
1.1 GNU ELPA
────────────
`plz' is available in [GNU ELPA]. It may be installed in Emacs using
the `package-install' command.
[GNU ELPA] <http://elpa.gnu.org/packages/plz.html>
1.2 Manual
──────────
`plz' has no dependencies other than Emacs and `curl'. It's known to
work on Emacs 26.3 or later. To install it manually, simply place
`plz.el' in your `load-path' and `(require 'plz)'.
2 Usage
═══════
The main public function is `plz', which sends an HTTP request and
returns either the result of the specified type (for a synchronous
request), or the `curl' process object (for asynchronous requests).
For asynchronous requests, callback, error-handling, and finalizer
functions may be specified, as well as various other options.
2.1 Examples
────────────
Synchronously `GET' a URL and return the response body as a decoded
string (here, raw JSON):
┌────
│ (plz 'get "https://httpbin.org/user-agent")
└────
┌────
│ "{\n \"user-agent\": \"curl/7.35.0\"\n}\n"
└────
Synchronously `GET' a URL that returns a JSON object, and parse and
return it as an alist:
┌────
│ (plz 'get "https://httpbin.org/get" :as #'json-read)
└────
┌────
│ ((args)
│ (headers
│ (Accept . "*/*")
│ (Accept-Encoding . "deflate, gzip")
│ (Host . "httpbin.org")
│ (User-Agent . "curl/7.35.0"))
│ (url . "https://httpbin.org/get"))
└────
Asynchronously `POST' a JSON object in the request body, then parse a
JSON object from the response body, and call a function with the
result:
┌────
│ (plz 'post "https://httpbin.org/post"
│ :headers '(("Content-Type" . "application/json"))
│ :body (json-encode '(("key" . "value")))
│ :as #'json-read
│ :then (lambda (alist)
│ (message "Result: %s" (alist-get 'data alist))))
└────
┌────
│ Result: {"key":"value"}
└────
Synchronously download a JPEG file, then create an Emacs image object
from the data:
┌────
│ (let ((jpeg-data (plz 'get "https://httpbin.org/image/jpeg" :as 'binary)))
│ (create-image jpeg-data nil 'data))
└────
┌────
│ (image :type jpeg :data ""ÿØÿà^@^PJFIF...")
└────
2.2 Functions
─────────────
`plz'
/(method url &key headers body else finally noquery (as 'string)
(then 'sync) (body-type 'text) (decode t decode-s)
(connect-timeout plz-connect-timeout) (timeout plz-timeout))/
Request `METHOD' from `URL' with curl. Return the curl process
object or, for a synchronous request, the selected result.
`HEADERS' may be an alist of extra headers to send with the
request.
`BODY-TYPE' may be `text' to send `BODY' as text, or `binary' to
send it as binary.
`AS' selects the kind of result to pass to the callback function
`THEN', or the kind of result to return for synchronous
requests. It may be:
• `buffer' to pass the response buffer.
• `binary' to pass the response body as an undecoded string.
• `string' to pass the response body as a decoded string.
• `response' to pass a `plz-response' struct.
• A function, to pass its return value; it is called in the
response buffer, which is narrowed to the response body
(suitable for, e.g. `json-read').
• `file' to pass a temporary filename to which the response
body has been saved without decoding.
• `(file FILENAME)' to pass `FILENAME' after having saved the
response body to it without decoding. `FILENAME' must be a
non-existent file; if it exists, it will not be overwritten,
and an error will be signaled.
If `DECODE' is non-nil, the response body is decoded
automatically. For binary content, it should be nil. When `AS'
is `binary', `DECODE' is automatically set to nil.
`THEN' is a callback function, whose sole argument is selected
above with `AS'. Or `THEN' may be `sync' to make a synchronous
request, in which case the result is returned directly.
`ELSE' is an optional callback function called when the request
fails with one argument, a `plz-error' struct. If `ELSE' is
nil, an error is signaled when the request fails, either
`plz-curl-error' or `plz-http-error' as appropriate, with a
`plz-error' struct as the error data. For synchronous requests,
this argument is ignored.
`FINALLY' is an optional function called without argument after
`THEN' or `ELSE', as appropriate. For synchronous requests,
this argument is ignored.
`CONNECT-TIMEOUT' and `TIMEOUT' are a number of seconds that
limit how long it takes to connect to a host and to receive a
response from a host, respectively.
`NOQUERY' is passed to `make-process', which see.
2.3 Queueing
────────────
`plz' provides a simple system for queueing HTTP requests. First,
make a `plz-queue' struct by calling `make-plz-queue'. Then call
`plz-queue' with the struct as the first argument, and the rest of the
arguments being the same as those passed to `plz'. Then call
`plz-run' to run the queued requests.
All of the queue-related functions return the queue as their value,
making them easy to use. For example:
┌────
│ (defvar my-queue (make-plz-queue :limit 2))
│
│ (plz-run
│ (plz-queue my-queue
│ 'get "https://httpbin.org/get?foo=0"
│ :then (lambda (body) (message "%s" body))))
└────
Or:
┌────
│ (let ((queue (make-plz-queue :limit 2))
│ (urls '("https://httpbin.org/get?foo=0"
│ "https://httpbin.org/get?foo=1")))
│ (plz-run
│ (dolist (url urls queue)
│ (plz-queue queue 'get url
│ :then (lambda (body) (message "%s" body))))))
└────
You may also clear a queue with `plz-clear', which cancels any active
or queued requests and calls their `:else' functions. And
`plz-length' returns the number of a queue's active and queued
requests.
2.4 Tips
────────
⁃ You can customize settings in the `plz' group, but this can only be
used to adjust a few defaults. It's not intended that changing or
binding global variables be necessary for normal operation.
3 Changelog
═══════════
3.1 0.3
───────
*Additions*
⁃ Handle HTTP proxy headers from Curl. ([#2]. Thanks to [Alan Third]
and [Sawyer Zheng] for reporting.)
*Fixes*
⁃ Replaced words not in Ispell's default dictionaries (so `checkdoc'
linting succeeds).
[#2] <https://github.com/alphapapa/plz.el/issues/2>
[Alan Third] <https://github.com/alanthird>
[Sawyer Zheng] <https://github.com/sawyerzheng>
3.2 0.2.1
─────────
*Fixes*
⁃ Handle when Curl process is interrupted.
3.3 0.2
───────
*Added*
⁃ Simple request queueing.
3.4 0.1
───────
Initial release.
4 Credits
═════════
⁃ Thanks to [Chris Wellons], author of the [Elfeed] feed reader and
the popular blog [null program], for his invaluable advice, review,
and encouragement.
[Chris Wellons] <https://github.com/skeeto>
[Elfeed] <https://github.com/skeeto/elfeed>
[null program] <https://nullprogram.com/>
5 Development
═════════════
Bug reports, feature requests, suggestions — /oh my/!
Note that `plz' is a young library, and its only client so far is
[Ement.el]. There are a variety of HTTP and `curl' features it does
not yet support, since they have not been needed by the author.
Patches are welcome, as long as they include passing tests.
[Ement.el] <https://github.com/alphapapa/ement.el>
5.1 Copyright assignment
────────────────────────
This package is part of [GNU Emacs], being distributed in [GNU ELPA].
Contributions to this project must follow GNU guidelines, which means
that, as with other parts of Emacs, patches of more than a few lines
must be accompanied by having assigned copyright for the contribution
to the FSF. Contributors who wish to do so may contact
[emacs-devel@gnu.org] to request the assignment form.
[GNU Emacs] <https://www.gnu.org/software/emacs/>
[GNU ELPA] <https://elpa.gnu.org/>
[emacs-devel@gnu.org] <mailto:emacs-devel@gnu.org>
6 License
═════════
GPLv3
#+TITLE: plz.el
#+PROPERTY: LOGGING nil
# Note: This readme works with the org-make-toc <https://github.com/alphapapa/org-make-toc> package, which automatically updates the table of contents.
[[http://elpa.gnu.org/packages/plz.html][file:http://elpa.gnu.org/packages/plz.svg]]
#+HTML: <img src="images/mascot.png" align="right">
~plz~ is an HTTP library for Emacs. It uses ~curl~ as a backend, which avoids some of the issues with using Emacs's built-in ~url~ library. It supports both synchronous and asynchronous requests. Its API is intended to be simple, natural, and expressive. Its code is intended to be simple and well-organized. Every feature is tested against [[https://httpbin.org/][httpbin]].
* Contents :noexport:
:PROPERTIES:
:TOC: :include siblings
:END:
:CONTENTS:
- [[#installation][Installation]]
- [[#usage][Usage]]
- [[#examples][Examples]]
- [[#functions][Functions]]
- [[#queueing][Queueing]]
- [[#changelog][Changelog]]
- [[#credits][Credits]]
- [[#development][Development]]
- [[#copyright-assignment][Copyright assignment]]
:END:
* Installation
:PROPERTIES:
:TOC: :depth 0
:END:
** GNU ELPA
~plz~ is available in [[http://elpa.gnu.org/packages/plz.html][GNU ELPA]]. It may be installed in Emacs using the ~package-install~ command.
** Manual
~plz~ has no dependencies other than Emacs and ~curl~. It's known to work on Emacs 26.3 or later. To install it manually, simply place =plz.el= in your ~load-path~ and ~(require 'plz)~.
* Usage
:PROPERTIES:
:TOC: :depth 1
:END:
The main public function is ~plz~, which sends an HTTP request and returns either the result of the specified type (for a synchronous request), or the ~curl~ process object (for asynchronous requests). For asynchronous requests, callback, error-handling, and finalizer functions may be specified, as well as various other options.
** Examples
Synchronously =GET= a URL and return the response body as a decoded string (here, raw JSON):
#+BEGIN_SRC elisp :exports both :results value code :cache yes
(plz 'get "https://httpbin.org/user-agent")
#+END_SRC
#+RESULTS:
#+BEGIN_SRC elisp
"{\n \"user-agent\": \"curl/7.35.0\"\n}\n"
#+END_SRC
Synchronously =GET= a URL that returns a JSON object, and parse and return it as an alist:
#+BEGIN_SRC elisp :exports both :results value code :cache yes
(plz 'get "https://httpbin.org/get" :as #'json-read)
#+END_SRC
#+RESULTS:
#+BEGIN_SRC elisp
((args)
(headers
(Accept . "*/*")
(Accept-Encoding . "deflate, gzip")
(Host . "httpbin.org")
(User-Agent . "curl/7.35.0"))
(url . "https://httpbin.org/get"))
#+END_SRC
Asynchronously =POST= a JSON object in the request body, then parse a JSON object from the response body, and call a function with the result:
#+BEGIN_SRC elisp :exports both :cache yes
(plz 'post "https://httpbin.org/post"
:headers '(("Content-Type" . "application/json"))
:body (json-encode '(("key" . "value")))
:as #'json-read
:then (lambda (alist)
(message "Result: %s" (alist-get 'data alist))))
#+END_SRC
#+RESULTS:
: Result: {"key":"value"}
Synchronously download a JPEG file, then create an Emacs image object from the data:
#+BEGIN_SRC elisp :exports both :cache yes
(let ((jpeg-data (plz 'get "https://httpbin.org/image/jpeg" :as 'binary)))
(create-image jpeg-data nil 'data))
#+END_SRC
#+RESULTS:
: (image :type jpeg :data ""ÿØÿà^@^PJFIF...")
** Functions
+ ~plz~ :: /(method url &key headers body else finally noquery (as 'string) (then 'sync) (body-type 'text) (decode t decode-s) (connect-timeout plz-connect-timeout) (timeout plz-timeout))/
Request ~METHOD~ from ~URL~ with curl. Return the curl process object or, for a synchronous request, the selected result.
~HEADERS~ may be an alist of extra headers to send with the request.
~BODY-TYPE~ may be ~text~ to send ~BODY~ as text, or ~binary~ to send it as binary.
~AS~ selects the kind of result to pass to the callback function ~THEN~, or the kind of result to return for synchronous requests. It may be:
- ~buffer~ to pass the response buffer.
- ~binary~ to pass the response body as an undecoded string.
- ~string~ to pass the response body as a decoded string.
- ~response~ to pass a ~plz-response~ struct.
- A function, to pass its return value; it is called in the response buffer, which is narrowed to the response body (suitable for, e.g. ~json-read~).
- ~file~ to pass a temporary filename to which the response body has been saved without decoding.
- ~(file FILENAME)~ to pass ~FILENAME~ after having saved the response body to it without decoding. ~FILENAME~ must be a non-existent file; if it exists, it will not be overwritten, and an error will be signaled.
If ~DECODE~ is non-nil, the response body is decoded automatically. For binary content, it should be nil. When ~AS~ is ~binary~, ~DECODE~ is automatically set to nil.
~THEN~ is a callback function, whose sole argument is selected above with ~AS~. Or ~THEN~ may be ~sync~ to make a synchronous request, in which case the result is returned directly.
~ELSE~ is an optional callback function called when the request fails with one argument, a ~plz-error~ struct. If ~ELSE~ is nil, an error is signaled when the request fails, either ~plz-curl-error~ or ~plz-http-error~ as appropriate, with a ~plz-error~ struct as the error data. For synchronous requests, this argument is ignored.
~FINALLY~ is an optional function called without argument after ~THEN~ or ~ELSE~, as appropriate. For synchronous requests, this argument is ignored.
~CONNECT-TIMEOUT~ and ~TIMEOUT~ are a number of seconds that limit how long it takes to connect to a host and to receive a response from a host, respectively.
~NOQUERY~ is passed to ~make-process~, which see.
** Queueing
~plz~ provides a simple system for queueing HTTP requests. First, make a ~plz-queue~ struct by calling ~make-plz-queue~. Then call ~plz-queue~ with the struct as the first argument, and the rest of the arguments being the same as those passed to ~plz~. Then call ~plz-run~ to run the queued requests.
All of the queue-related functions return the queue as their value, making them easy to use. For example:
#+begin_src elisp
(defvar my-queue (make-plz-queue :limit 2))
(plz-run
(plz-queue my-queue
'get "https://httpbin.org/get?foo=0"
:then (lambda (body) (message "%s" body))))
#+end_src
Or:
#+begin_src elisp
(let ((queue (make-plz-queue :limit 2))
(urls '("https://httpbin.org/get?foo=0"
"https://httpbin.org/get?foo=1")))
(plz-run
(dolist (url urls queue)
(plz-queue queue 'get url
:then (lambda (body) (message "%s" body))))))
#+end_src
You may also clear a queue with ~plz-clear~, which cancels any active or queued requests and calls their ~:else~ functions. And ~plz-length~ returns the number of a queue's active and queued requests.
** Tips
:PROPERTIES:
:TOC: :ignore (this)
:END:
+ You can customize settings in the =plz= group, but this can only be used to adjust a few defaults. It's not intended that changing or binding global variables be necessary for normal operation.
* Changelog
:PROPERTIES:
:TOC: :depth 0
:END:
** 0.3
*Additions*
+ Handle HTTP proxy headers from Curl. ([[https://github.com/alphapapa/plz.el/issues/2][#2]]. Thanks to [[https://github.com/alanthird][Alan Third]] and [[https://github.com/sawyerzheng][Sawyer Zheng]] for reporting.)
*Fixes*
+ Replaced words not in Ispell's default dictionaries (so ~checkdoc~ linting succeeds).
** 0.2.1
*Fixes*
+ Handle when Curl process is interrupted.
** 0.2
*Added*
+ Simple request queueing.
** 0.1
Initial release.
* Credits
+ Thanks to [[https://github.com/skeeto][Chris Wellons]], author of the [[https://github.com/skeeto/elfeed][Elfeed]] feed reader and the popular blog [[https://nullprogram.com/][null program]], for his invaluable advice, review, and encouragement.
* Development
Bug reports, feature requests, suggestions — /oh my/!
Note that ~plz~ is a young library, and its only client so far is [[https://github.com/alphapapa/ement.el][Ement.el]]. There are a variety of HTTP and ~curl~ features it does not yet support, since they have not been needed by the author. Patches are welcome, as long as they include passing tests.
** Copyright assignment
This package is part of [[https://www.gnu.org/software/emacs/][GNU Emacs]], being distributed in [[https://elpa.gnu.org/][GNU ELPA]]. Contributions to this project must follow GNU guidelines, which means that, as with other parts of Emacs, patches of more than a few lines must be accompanied by having assigned copyright for the contribution to the FSF. Contributors who wish to do so may contact [[mailto:emacs-devel@gnu.org][emacs-devel@gnu.org]] to request the assignment form.
* License
:PROPERTIES:
:TOC: :ignore (this)
:END:
GPLv3
* COMMENT Export setup :noexport:
:PROPERTIES:
:TOC: :ignore (this descendants)
:END:
# Copied from org-super-agenda's readme, in which much was borrowed from Org's =org-manual.org=.
#+OPTIONS: broken-links:t *:t
** Info export options
#+TEXINFO_DIR_CATEGORY: Emacs
#+TEXINFO_DIR_TITLE: Plz: (plz)
#+TEXINFO_DIR_DESC: HTTP library using Curl as a backend
# NOTE: We could use these, but that causes a pointless error, "org-compile-file: File "..README.info" wasn't produced...", so we just rename the files in the after-save-hook instead.
# #+TEXINFO_FILENAME: plz.info
# #+EXPORT_FILE_NAME: plz.texi
** File-local variables
# NOTE: Setting org-comment-string buffer-locally is a nasty hack to work around GitHub's org-ruby's HTML rendering, which does not respect noexport tags. The only way to hide this tree from its output is to use the COMMENT keyword, but that prevents Org from processing the export options declared in it. So since these file-local variables don't affect org-ruby, wet set org-comment-string to an unused keyword, which prevents Org from deleting this tree from the export buffer, which allows it to find the export options in it. And since org-export does respect the noexport tag, the tree is excluded from the info page.
# Local Variables:
# eval: (require 'org-make-toc)
# after-save-hook: (lambda nil (when (and (require 'ox-texinfo nil t) (org-texinfo-export-to-info)) (delete-file "README.texi") (rename-file "README.info" "plz.info" t)))
# before-save-hook: org-make-toc
# org-export-with-properties: ()
# org-export-with-title: t
# org-export-initial-scope: buffer
# org-comment-string: "NOTCOMMENT"
# End:
This is the file .../info/dir, which contains the
topmost node of the Info hierarchy, called (dir)Top.
The first time you invoke Info you start off looking at this node.
File: dir, Node: Top This is the top of the INFO tree
This (the Directory node) gives a menu of major topics.
Typing "q" exits, "H" lists all Info commands, "d" returns here,
"h" gives a primer for first-timers,
"mEmacs<Return>" visits the Emacs manual, etc.
In Emacs, you can click mouse button 2 on a menu item or cross reference
to select it.
* Menu:
;;; plz-autoloads.el --- automatically extracted autoloads -*- lexical-binding: t -*-
;;
;;; Code:
(add-to-list 'load-path (directory-file-name
(or (file-name-directory #$) (car load-path))))
;;;### (autoloads nil "plz" "plz.el" (0 0 0 0))
;;; Generated autoloads from plz.el
(register-definition-prefixes "plz" '("plz-"))
;;;***
;;;### (autoloads nil nil ("plz-pkg.el") (0 0 0 0))
;;;***
;; Local Variables:
;; version-control: never
;; no-byte-compile: t
;; no-update-autoloads: t
;; coding: utf-8
;; End:
;;; plz-autoloads.el ends here
;; Generated package description from plz.el -*- no-byte-compile: t -*-
(define-package "plz" "0.3" "HTTP library" '((emacs "26.3")) :commit "ecd1de2a7967bd8bc61737078d717d0ee050b344" :authors '(("Adam Porter" . "adam@alphapapa.net")) :maintainer '("Adam Porter" . "adam@alphapapa.net") :keywords '("comm" "network" "http") :url "https://github.com/alphapapa/plz.el")
;;; plz.el --- HTTP library -*- lexical-binding: t; -*-
;; Copyright (C) 2019-2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; URL: https://github.com/alphapapa/plz.el
;; Version: 0.3
;; Package-Requires: ((emacs "26.3"))
;; Keywords: comm, network, http
;; This file is part of GNU Emacs.
;;; License:
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; An HTTP library that uses curl as a backend. Inspired by, and some
;; code copied from, Christopher Wellons's library, elfeed-curl.el.
;;
;; Why this package?
;;
;; 1. `url' works well for many things, but it has some issues.
;; 2. `request' works well for many things, but it has some issues.
;; 3. Chris Wellons doesn't have time to factor his excellent
;; elfeed-curl.el library out of Elfeed. This will have to do.
;;
;; Why is it called `plz'?
;;
;; 1. There's already a package called `http'.
;; 2. There's already a package called `request'.
;; 3. Naming things is hard.
;;;; Usage:
;; Call function `plz' to make an HTTP request. Its docstring
;; explains its arguments. `plz' also supports other HTTP methods,
;; uploading and downloading binary files, sending URL parameters and
;; HTTP headers, configurable timeouts, error-handling "else" and
;; always-called "finally" functions, and more.
;; Basic usage is simple. For example, to make a synchronous request
;; and return the HTTP response body as a string:
;;
;; (plz 'get "https://httpbin.org/get")
;;
;; Which returns the JSON object as a string:
;;
;; "{
;; \"args\": {},
;; \"headers\": {
;; \"Accept\": \"*/*\",
;; \"Accept-Encoding\": \"deflate, gzip\",
;; \"Host\": \"httpbin.org\",
;; \"User-Agent\": \"curl/7.35.0\"
;; },
;; \"origin\": \"xxx.xxx.xxx.xxx\",
;; \"url\": \"https://httpbin.org/get\"
;; }"
;;
;; To make the same request asynchronously, decoding the JSON and
;; printing a message with a value from it:
;;
;; (plz 'get "https://httpbin.org/get" :as #'json-read
;; :then (lambda (alist) (message "URL: %s" (alist-get 'url alist))))
;;
;; Which, after the request returns, prints:
;;
;; URL: https://httpbin.org/get
;;;; Credits:
;; Thanks to Chris Wellons for inspiration, encouragement, and advice.
;;; Code:
;;;; Requirements
(require 'cl-lib)
(require 'rx)
(require 'subr-x)
;;;; Errors
;; FIXME: `condition-case' can't catch these...?
(define-error 'plz-curl-error "Curl error")
(define-error 'plz-http-error "HTTP error")
;;;; Structs
(cl-defstruct plz-response
version status headers body)
(cl-defstruct plz-error
curl-error response message)
;;;; Constants
(defconst plz-http-response-status-line-regexp
(rx "HTTP/" (group (or "1.0" "1.1" "2")) " "
;; Status code
(group (1+ digit)) " "
;; Reason phrase
(optional (group (1+ (not (any "\r\n")))))
(or
;; HTTP 1
"\r\n"
;; HTTP 2
"\n"))
"Regular expression matching HTTP response status line.")
(defconst plz-http-end-of-headers-regexp
(rx (or "\r\n\r\n" "\n\n"))
"Regular expression matching the end of HTTP headers.
This must work with both HTTP/1 (using CRLF) and HTTP/2 (using
only LF).")
(defconst plz-curl-errors
;; Copied from elfeed-curl.el.
'((1 . "Unsupported protocol.")
(2 . "Failed to initialize.")
(3 . "URL malformed. The syntax was not correct.")
(4 . "A feature or option that was needed to perform the desired request was not enabled or was explicitly disabled at build-time.")
(5 . "Couldn't resolve proxy. The given proxy host could not be resolved.")
(6 . "Couldn't resolve host. The given remote host was not resolved.")
(7 . "Failed to connect to host.")
(8 . "FTP weird server reply. The server sent data curl couldn't parse.")
(9 . "FTP access denied.")
(11 . "FTP weird PASS reply.")
(13 . "FTP weird PASV reply.")
(14 . "FTP weird 227 format.")
(15 . "FTP can't get host.")
(17 . "FTP couldn't set binary.")
(18 . "Partial file. Only a part of the file was transferred.")
(19 . "FTP couldn't download/access the given file, the RETR (or similar) command failed.")
(21 . "FTP quote error. A quote command returned error from the server.")
(22 . "HTTP page not retrieved.")
(23 . "Write error.")
(25 . "FTP couldn't STOR file.")
(26 . "Read error. Various reading problems.")
(27 . "Out of memory. A memory allocation request failed.")
(28 . "Operation timeout.")
(30 . "FTP PORT failed.")
(31 . "FTP couldn't use REST.")
(33 . "HTTP range error. The range \"command\" didn't work.")
(34 . "HTTP post error. Internal post-request generation error.")
(35 . "SSL connect error. The SSL handshaking failed.")
(36 . "FTP bad download resume.")
(37 . "FILE couldn't read file.")
(38 . "LDAP bind operation failed.")
(39 . "LDAP search failed.")
(41 . "Function not found. A required LDAP function was not found.")
(42 . "Aborted by callback.")
(43 . "Internal error. A function was called with a bad parameter.")
(45 . "Interface error. A specified outgoing interface could not be used.")
(47 . "Too many redirects.")
(48 . "Unknown option specified to libcurl.")
(49 . "Malformed telnet option.")
(51 . "The peer's SSL certificate or SSH MD5 fingerprint was not OK.")
(52 . "The server didn't reply anything, which here is considered an error.")
(53 . "SSL crypto engine not found.")
(54 . "Cannot set SSL crypto engine as default.")
(55 . "Failed sending network data.")
(56 . "Failure in receiving network data.")
(58 . "Problem with the local certificate.")
(59 . "Couldn't use specified SSL cipher.")
(60 . "Peer certificate cannot be authenticated with known CA certificates.")
(61 . "Unrecognized transfer encoding.")
(62 . "Invalid LDAP URL.")
(63 . "Maximum file size exceeded.")
(64 . "Requested FTP SSL level failed.")
(65 . "Sending the data requires a rewind that failed.")
(66 . "Failed to initialise SSL Engine.")
(67 . "The user name, password, or similar was not accepted and curl failed to log in.")
(68 . "File not found on TFTP server.")
(69 . "Permission problem on TFTP server.")
(70 . "Out of disk space on TFTP server.")
(71 . "Illegal TFTP operation.")
(72 . "Unknown TFTP transfer ID.")
(73 . "File already exists (TFTP).")
(74 . "No such user (TFTP).")
(75 . "Character conversion failed.")
(76 . "Character conversion functions required.")
(77 . "Problem with reading the SSL CA cert (path? access rights?).")
(78 . "The resource referenced in the URL does not exist.")
(79 . "An unspecified error occurred during the SSH session.")
(80 . "Failed to shut down the SSL connection.")
(82 . "Could not load CRL file, missing or wrong format (added in 7.19.0).")
(83 . "Issuer check failed (added in 7.19.0).")
(84 . "The FTP PRET command failed")
(85 . "RTSP: mismatch of CSeq numbers")
(86 . "RTSP: mismatch of Session Identifiers")
(87 . "unable to parse FTP file list")
(88 . "FTP chunk callback reported error")
(89 . "No connection available, the session will be queued")
(90 . "SSL public key does not matched pinned public key"))
"Alist mapping curl error code integers to helpful error messages.")
;;;; Variables
(defvar-local plz-else nil
"Callback function for unsuccessful completion of request.
Called in current curl process buffer.")
(defvar-local plz-then nil
"Callback function for successful completion of request.
Called in current curl process buffer.")
(defvar-local plz-finally nil
"Function called unconditionally after completion of request.
Called after the then/else function, without arguments, outside
the curl process buffer.")
(defvar-local plz-result nil
"Used when `plz' is called synchronously.")
(defvar-local plz-sync nil
"Used when `plz' is called synchronously.")
;;;; Customization
(defgroup plz nil
"Options for `plz'."
:group 'network
:link '(url-link "https://github.com/alphapapa/plz.el"))
(defcustom plz-curl-program "curl"
"Name of curl program to call."
:type 'string)
(defcustom plz-curl-default-args
'("--silent"
"--compressed"
"--location"
"--dump-header" "-")
"Default arguments to curl.
Note that these arguments are passed on the command line, which
may be visible to other users on the local system."
:type '(repeat string))
(defcustom plz-connect-timeout 5
"Default connection timeout in seconds.
This limits how long the connection phase may last (the
\"--connect-timeout\" argument to curl)."
:type 'number)
(defcustom plz-timeout 60
"Default request timeout in seconds.
This limits how long an entire request may take, including the
connection phase and waiting to receive the response (the
\"--max-time\" argument to curl)."
:type 'number)
;;;; Functions
;;;;; Public
(cl-defun plz (method url &key headers body else finally noquery
(as 'string) (then 'sync)
(body-type 'text) (decode t decode-s)
(connect-timeout plz-connect-timeout) (timeout plz-timeout))
"Request METHOD from URL with curl.
Return the curl process object or, for a synchronous request, the
selected result.
HEADERS may be an alist of extra headers to send with the
request.
BODY-TYPE may be `text' to send BODY as text, or `binary' to send
it as binary.
AS selects the kind of result to pass to the callback function
THEN, or the kind of result to return for synchronous requests.
It may be:
- `buffer' to pass the response buffer.
- `binary' to pass the response body as an un-decoded string.
- `string' to pass the response body as a decoded string.
- `response' to pass a `plz-response' structure.
- `file' to pass a temporary filename to which the response body
has been saved without decoding.
- `(file FILENAME)' to pass FILENAME after having saved the
response body to it without decoding. FILENAME must be a
non-existent file; if it exists, it will not be overwritten,
and an error will be signaled.
- A function, which is called in the response buffer with it
narrowed to the response body (suitable for, e.g. `json-read').
If DECODE is non-nil, the response body is decoded automatically.
For binary content, it should be nil. When AS is `binary',
DECODE is automatically set to nil.
THEN is a callback function, whose sole argument is selected
above with AS. Or THEN may be `sync' to make a synchronous
request, in which case the result is returned directly.
ELSE is an optional callback function called when the request
fails with one argument, a `plz-error' structure. If ELSE is
nil, an error is signaled when the request fails, either
`plz-curl-error' or `plz-http-error' as appropriate, with a
`plz-error' structure as the error data. For synchronous
requests, this argument is ignored.
FINALLY is an optional function called without argument after
THEN or ELSE, as appropriate. For synchronous requests, this
argument is ignored.
CONNECT-TIMEOUT and TIMEOUT are a number of seconds that limit
how long it takes to connect to a host and to receive a response
from a host, respectively.
NOQUERY is passed to `make-process', which see."
;; Inspired by and copied from `elfeed-curl-retrieve'.
(declare (indent defun))
(setf decode (if (and decode-s (not decode))
nil decode))
;; NOTE: By default, for PUT requests and POST requests >1KB, curl sends an
;; "Expect:" header, which causes servers to send a "100 Continue" response, which
;; we don't want to have to deal with, so we disable it by setting the header to
;; the empty string. See <https://gms.tf/when-curl-sends-100-continue.html>.
;; TODO: Handle "100 Continue" responses and remove this workaround.
(push (cons "Expect" "") headers)
(let* ((data-arg (pcase-exhaustive body-type
('binary "--data-binary")
('text "--data")))
(curl-command-line-args (append plz-curl-default-args
(list "--config" "-")))
(curl-config-header-args (cl-loop for (key . value) in headers
collect (cons "--header" (format "%s: %s" key value))))
(curl-config-args (append curl-config-header-args
(list (cons "--url" url))
(when connect-timeout
(list (cons "--connect-timeout"
(number-to-string connect-timeout))))
(when timeout
(list (cons "--max-time" (number-to-string timeout))))
(pcase method
((or 'put 'post)
(cl-assert body)
(list (cons "--request" (upcase (symbol-name method)))
;; It appears that this must be the last argument
;; in order to pass data on the rest of STDIN.
(cons data-arg "@-")))
('delete
(list (cons "--request" (upcase (symbol-name method))))))))
(curl-config (cl-loop for (key . value) in curl-config-args
concat (format "%s \"%s\"\n" key value)))
(decode (pcase as
('binary nil)
(_ decode)))
sync-p)
(when (eq 'sync then)
(setf sync-p t
then (lambda (result)
(setf plz-result result))))
(with-current-buffer (generate-new-buffer " *plz-request-curl*")
;; Avoid making process in a nonexistent directory (in case the current
;; default-directory has since been removed). It's unclear what the best
;; directory is, but this seems to make sense, and it should still exist.
(let ((default-directory temporary-file-directory)
(process (make-process :name "plz-request-curl"
:buffer (current-buffer)
:coding 'binary
:command (append (list plz-curl-program) curl-command-line-args)
:connection-type 'pipe
:sentinel #'plz--sentinel
:stderr (current-buffer)
:noquery noquery))
;; The THEN function is called in the response buffer.
(then (pcase-exhaustive as
((or 'binary 'string)
(lambda ()
(let ((coding-system (or (plz--coding-system) 'utf-8)))
(pcase as
('binary (set-buffer-multibyte nil)))
(plz--narrow-to-body)
(when decode
(decode-coding-region (point) (point-max) coding-system))
(funcall then (buffer-string)))))
('buffer (lambda ()
(funcall then (current-buffer))))
('response (lambda ()
(funcall then (plz--response :decode-p decode))))
('file (lambda ()
(set-buffer-multibyte nil)
(plz--narrow-to-body)
(let ((filename (make-temp-file "plz-")))
(condition-case err
(write-region (point-min) (point-max) filename)
;; In case of an error writing to the file, delete the temp file
;; and signal the error. Ignore any errors encountered while
;; deleting the file, which would obscure the original error.
(error (ignore-errors
(delete-file filename))
(signal (car err) (cdr err))))
(funcall then filename))))
(`(file ,(and (pred stringp) filename))
(lambda ()
(set-buffer-multibyte nil)
(plz--narrow-to-body)
(condition-case err
(write-region (point-min) (point-max) filename nil nil nil 'excl)
;; Since we are creating the file, it seems sensible to delete it in case of an
;; error while writing to it (e.g. a disk-full error). And we ignore any errors
;; encountered while deleting the file, which would obscure the original error.
(error (ignore-errors
(when (file-exists-p filename)
(delete-file filename)))
(signal (car err) (cdr err))))
(funcall then filename)))
((pred functionp) (lambda ()
(let ((coding-system (or (plz--coding-system) 'utf-8)))
(plz--narrow-to-body)
(when decode
(decode-coding-region (point) (point-max) coding-system))
(funcall then (funcall as))))))))
(setf plz-then then
plz-else else
plz-finally finally
plz-sync sync-p)
;; Send --config arguments.
(process-send-string process curl-config)
(when body
(cl-typecase body
(string (process-send-string process body))
(buffer (with-current-buffer body
(process-send-region process (point-min) (point-max))))))
(process-send-eof process)
(if sync-p
(progn
(while
;; According to the Elisp manual, blocking on a process's
;; output is really this simple. And it seems to work.
(accept-process-output process))
(prog1 plz-result
(unless (eq as 'buffer)
(kill-buffer))))
process)))))
;;;;; Queue
;; A simple queue system.
(cl-defstruct plz-queued-request
"Structure representing a queued `plz' HTTP request.
For more details on these slots, see arguments to the function
`plz'."
method url headers body else finally noquery
as then body-type decode
connect-timeout timeout
next previous process)
(cl-defstruct plz-queue
"Structure forming a queue for `plz' requests.
The queue may be appended to (the default) and prepended to, and
items may be removed from the front of the queue (i.e. by
default, it's FIFO). Use functions `plz-queue', `plz-run', and
`plz-clear' to queue, run, and clear requests, respectively."
(limit 1
:documentation "Number of simultaneous requests.")
(active nil
:documentation "Active requests.")
(requests nil
:documentation "Queued requests.")
(canceled-p nil
:documentation "Non-nil when queue has been canceled.")
first-active last-active
first-request last-request)
(defun plz-queue (queue &rest args)
"Enqueue request for ARGS on QUEUE and return QUEUE.
To prepend to QUEUE rather than append, it may be a list of the
form (`prepend' QUEUE). QUEUE is a `plz-request' queue. ARGS
are those passed to `plz', which see. Use `plz-run' to start
making QUEUE's requests."
(declare (indent defun))
(cl-assert (not (equal 'sync (plist-get (cddr args) :then))) nil
"Only async requests may be queued")
(pcase-let* ((`(,method ,url . ,rest) args)
(args `(:method ,method :url ,url ,@rest))
(request (apply #'make-plz-queued-request args)))
(pcase queue
(`(prepend ,queue) (plz--queue-prepend request queue))
(_ (plz--queue-append request queue))))
queue)
(defun plz--queue-append (request queue)
"Append REQUEST to QUEUE and return QUEUE."
(cl-check-type request plz-queued-request
"REQUEST must be a `plz-queued-request' structure.")
(cl-check-type queue plz-queue
"QUEUE must be a `plz-queue' structure.")
(when (plz-queue-last-request queue)
(setf (plz-queued-request-next (plz-queue-last-request queue)) request))
(setf (plz-queued-request-previous request) (plz-queue-last-request queue)
(plz-queue-last-request queue) request)
(unless (plz-queue-first-request queue)
(setf (plz-queue-first-request queue) request))
(unless (plz-queue-last-request queue)
(setf (plz-queue-last-request queue) request))
(push request (plz-queue-requests queue))
queue)
(defun plz--queue-prepend (request queue)
"Prepend REQUEST to QUEUE and return QUEUE."
(cl-check-type request plz-queued-request
"REQUEST must be a `plz-queued-request' structure.")
(cl-check-type queue plz-queue
"QUEUE must be a `plz-queue' structure.")
(when (plz-queue-requests queue)
(setf (plz-queued-request-next request) (car (plz-queue-requests queue))
(plz-queued-request-previous (plz-queued-request-next request)) request))
(setf (plz-queue-first-request queue) request)
(unless (plz-queue-first-request queue)
(setf (plz-queue-first-request queue) request))
(unless (plz-queue-last-request queue)
(setf (plz-queue-last-request queue) request))
(push request (plz-queue-requests queue))
queue)
(defun plz--queue-pop (queue)
"Return the first queued request on QUEUE and remove it from QUEUE."
(let* ((request (plz-queue-first-request queue))
(next (plz-queued-request-next request)))
(when next
(setf (plz-queued-request-previous next) nil))
(setf (plz-queue-first-request queue) next
(plz-queue-requests queue) (delq request (plz-queue-requests queue)))
(when (eq request (plz-queue-last-request queue))
(setf (plz-queue-last-request queue) nil))
request))
(defun plz-run (queue)
"Process requests in QUEUE and return QUEUE.
Return when QUEUE is at limit or has no more queued requests.
QUEUE should be a `plz-queue' structure."
(cl-labels ((readyp
(queue) (and (not (plz-queue-canceled-p queue))
(plz-queue-requests queue)
;; With apologies to skeeto...
(< (length (plz-queue-active queue)) (plz-queue-limit queue)))))
(while (readyp queue)
(pcase-let* ((request (plz--queue-pop queue))
((cl-struct plz-queued-request method url
headers body finally noquery as body-type decode connect-timeout timeout
(else orig-else) (then orig-then))
request)
(then (lambda (response)
(unwind-protect
;; Ensure any errors in the THEN function don't abort the queue.
(funcall orig-then response)
(setf (plz-queue-active queue) (delq request (plz-queue-active queue)))
(plz-run queue))))
(else (lambda (arg)
(unwind-protect
;; Ensure any errors in the THEN function don't abort the queue.
(when orig-else
(funcall orig-else arg))
(setf (plz-queue-active queue) (delq request (plz-queue-active queue)))
(plz-run queue))))
(args (list method url
;; Omit arguments for which `plz' has defaults so as not to nil them.
:headers headers :body body :finally finally :noquery noquery
:connect-timeout connect-timeout :timeout timeout)))
;; Add arguments which override defaults.
(when as
(setf args (plist-put args :as as)))
(when else
(setf args (plist-put args :else else)))
(when then
(setf args (plist-put args :then then)))
(when decode
(setf args (plist-put args :decode decode)))
(when body-type
(setf args (plist-put args :body-type body-type)))
(when connect-timeout
(setf args (plist-put args :connect-timeout connect-timeout)))
(when timeout
(setf args (plist-put args :timeout timeout)))
(setf (plz-queued-request-process request) (apply #'plz args))
(push request (plz-queue-active queue))))
queue))
(defun plz-clear (queue)
"Clear QUEUE and return it.
Cancels any active or pending requests. For pending requests,
their ELSE functions will be called with a `plz-error' structure
with the message, \"`plz' queue cleared; request canceled.\";
active requests will have their curl processes killed and their
ELSE functions called with the corresponding data."
(setf (plz-queue-canceled-p queue) t)
(dolist (request (plz-queue-active queue))
(kill-process (plz-queued-request-process request))
(setf (plz-queue-active queue) (delq request (plz-queue-active queue))))
(dolist (request (plz-queue-requests queue))
(funcall (plz-queued-request-else request)
(make-plz-error :message "`plz' queue cleared; request canceled."))
(setf (plz-queue-requests queue) (delq request (plz-queue-requests queue))))
(setf (plz-queue-first-active queue) nil
(plz-queue-last-active queue) nil
(plz-queue-first-request queue) nil
(plz-queue-last-request queue) nil
(plz-queue-canceled-p queue) nil)
queue)
(defun plz-length (queue)
"Return number of of QUEUE's outstanding requests.
Includes active and queued requests."
(+ (length (plz-queue-active queue))
(length (plz-queue-requests queue))))
;;;;; Private
(defun plz--sentinel (process-or-buffer status)
"Process buffer of curl output in PROCESS-OR-BUFFER.
If PROCESS-OR-BUFFER if a process, uses its buffer; if a buffer,
uses it. STATUS should be the process's event string (see info
node `(elisp) Sentinels'). Kills the buffer before returning."
;; Inspired by and some code copied from `elfeed-curl--sentinel'.
(let* ((buffer (cl-etypecase process-or-buffer
(process (process-buffer process-or-buffer))
(buffer process-or-buffer)))
(finally (buffer-local-value 'plz-finally buffer))
sync)
(unwind-protect
(with-current-buffer buffer
(setf sync plz-sync)
(pcase-exhaustive status
((or 0 "finished\n")
;; Curl exited normally: check HTTP status code.
(goto-char (point-min))
(plz--skip-proxy-headers)
(pcase (plz--http-status)
(200 (funcall plz-then))
(_ (let ((err (make-plz-error :response (plz--response))))
(pcase-exhaustive plz-else
(`nil (signal 'plz-http-error err))
((pred functionp) (funcall plz-else err)))))))
((or (and (pred numberp) code)
(rx "exited abnormally with code " (let code (group (1+ digit)))))
;; Curl error.
(let* ((curl-exit-code (cl-typecase code
(string (string-to-number code))
(number code)))
(curl-error-message (alist-get curl-exit-code plz-curl-errors))
(err (make-plz-error :curl-error (cons curl-exit-code curl-error-message))))
(pcase-exhaustive plz-else
;; FIXME: Returning a plz-error structure which has a curl-error slot, wrapped in a plz-curl-error, is confusing.
(`nil (signal 'plz-curl-error err))
((pred functionp) (funcall plz-else err)))))
((and (or "killed\n" "interrupt\n") status)
;; Curl process killed or interrupted.
(let* ((message (pcase status
("killed\n" "curl process killed")
("interrupt\n" "curl process interrupted")))
(err (make-plz-error :message message)))
(pcase-exhaustive plz-else
(`nil (signal 'plz-curl-error err))
((pred functionp) (funcall plz-else err)))))))
(when finally
(funcall finally))
(unless sync
(kill-buffer buffer)))))
;;;;;; HTTP Responses
;; Functions for parsing HTTP responses.
(defun plz--skip-proxy-headers ()
"Skip proxy headers in current buffer."
(when (looking-at plz-http-response-status-line-regexp)
(let* ((status-code (string-to-number (match-string 2)))
(reason-phrase (match-string 3)))
(when (and (equal 200 status-code)
(equal "Connection established" reason-phrase))
;; Skip proxy headers (curl apparently offers no way to omit
;; them).
(unless (re-search-forward "\r\n\r\n" nil t)
(signal 'plz-http-error '("plz--response: End of proxy headers not found")))))))
(cl-defun plz--response (&key (decode-p t))
"Return response structure for HTTP response in current buffer.
When DECODE-P is non-nil, decode the response body automatically
according to the apparent coding system.
Assumes that point is at beginning of HTTP response."
(save-excursion
;; Parse HTTP version and status code.
(unless (looking-at plz-http-response-status-line-regexp)
(signal 'plz-http-error
(list "plz--response: Unable to parse HTTP response status line"
(buffer-substring (point) (line-end-position)))))
(let* ((http-version (string-to-number (match-string 1)))
(status-code (string-to-number (match-string 2)))
(headers (plz--headers))
(coding-system (or (plz--coding-system headers) 'utf-8)))
(plz--narrow-to-body)
(when decode-p
(decode-coding-region (point) (point-max) coding-system))
(make-plz-response
:version http-version
:status status-code
:headers headers
:body (buffer-string)))))
(defun plz--coding-system (&optional headers)
"Return coding system for HTTP response in current buffer.
HEADERS may optionally be an alist of parsed HTTP headers to
refer to rather than the current buffer's unparsed headers."
(let* ((headers (or headers (plz--headers)))
(content-type (alist-get 'content-type headers)))
(when content-type
(coding-system-from-name content-type))))
(defun plz--http-status ()
"Return HTTP status code for HTTP response in current buffer.
Assumes point is at start of HTTP response."
(when (looking-at plz-http-response-status-line-regexp)
(string-to-number (match-string 2))))
(defun plz--headers ()
"Return headers alist for HTTP response in current buffer.
Assumes point is at start of HTTP response."
(save-excursion
(forward-line 1)
(let ((limit (save-excursion
(re-search-forward plz-http-end-of-headers-regexp nil)
(point))))
(cl-loop while (re-search-forward (rx bol (group (1+ (not (in ":")))) ":" (1+ blank)
(group (1+ (not (in "\r\n")))))
limit t)
;; NOTE: Some HTTP servers send all-lowercase header keys, which means an alist
;; lookup with `equal' or `string=' fails when the case differs. We don't want
;; users to have to worry about this, so for consistency, we downcase the
;; header name. And while we're at it, we might as well intern it so we can
;; use `alist-get' without having to add "nil nil #'equal" every time.
collect (cons (intern (downcase (match-string 1))) (match-string 2))))))
(defun plz--narrow-to-body ()
"Narrow to body of HTTP response in current buffer.
Assumes point is at start of HTTP response."
(unless (re-search-forward plz-http-end-of-headers-regexp nil t)
(signal 'plz-http-error '("plz--narrow-to-body: Unable to find end of headers")))
(narrow-to-region (point) (point-max)))
;;;; Footer
(provide 'plz)
;;; plz.el ends here
This is README.info, produced by makeinfo version 5.2 from README.texi.
INFO-DIR-SECTION Emacs
START-INFO-DIR-ENTRY
* Plz: (plz). HTTP library using Curl as a backend.
END-INFO-DIR-ENTRY
File: README.info, Node: Top, Next: Installation, Up: (dir)
plz.el
******
file:http://elpa.gnu.org/packages/plz.svg
(http://elpa.gnu.org/packages/plz.html)
‘plz’ is an HTTP library for Emacs. It uses ‘curl’ as a backend,
which avoids some of the issues with using Emacs’s built-in ‘url’
library. It supports both synchronous and asynchronous requests. Its
API is intended to be simple, natural, and expressive. Its code is
intended to be simple and well-organized. Every feature is tested
against httpbin (https://httpbin.org/).
* Menu:
* Installation::
* Usage::
* Changelog::
* Credits::
* Development::
* License::
— The Detailed Node Listing —
Installation
* GNU ELPA::
* Manual::
Usage
* Examples::
* Functions::
* Queueing::
* Tips::
Changelog
* 0.3: 03.
* 0.2.1: 021.
* 0.2: 02.
* 0.1: 01.
Development
* Copyright assignment::
File: README.info, Node: Installation, Next: Usage, Prev: Top, Up: Top
1 Installation
**************
* Menu:
* GNU ELPA::
* Manual::
File: README.info, Node: GNU ELPA, Next: Manual, Up: Installation
1.1 GNU ELPA
============
‘plz’ is available in GNU ELPA (http://elpa.gnu.org/packages/plz.html).
It may be installed in Emacs using the ‘package-install’ command.
File: README.info, Node: Manual, Prev: GNU ELPA, Up: Installation
1.2 Manual
==========
‘plz’ has no dependencies other than Emacs and ‘curl’. It’s known to
work on Emacs 26.3 or later. To install it manually, simply place
‘plz.el’ in your ‘load-path’ and ‘(require 'plz)’.
File: README.info, Node: Usage, Next: Changelog, Prev: Installation, Up: Top
2 Usage
*******
The main public function is ‘plz’, which sends an HTTP request and
returns either the result of the specified type (for a synchronous
request), or the ‘curl’ process object (for asynchronous requests). For
asynchronous requests, callback, error-handling, and finalizer functions
may be specified, as well as various other options.
* Menu:
* Examples::
* Functions::
* Queueing::
* Tips::
File: README.info, Node: Examples, Next: Functions, Up: Usage
2.1 Examples
============
Synchronously ‘GET’ a URL and return the response body as a decoded
string (here, raw JSON):
(plz 'get "https://httpbin.org/user-agent")
"{\n \"user-agent\": \"curl/7.35.0\"\n}\n"
Synchronously ‘GET’ a URL that returns a JSON object, and parse and
return it as an alist:
(plz 'get "https://httpbin.org/get" :as #'json-read)
((args)
(headers
(Accept . "*/*")
(Accept-Encoding . "deflate, gzip")
(Host . "httpbin.org")
(User-Agent . "curl/7.35.0"))
(url . "https://httpbin.org/get"))
Asynchronously ‘POST’ a JSON object in the request body, then parse a
JSON object from the response body, and call a function with the result:
(plz 'post "https://httpbin.org/post"
:headers '(("Content-Type" . "application/json"))
:body (json-encode '(("key" . "value")))
:as #'json-read
:then (lambda (alist)
(message "Result: %s" (alist-get 'data alist))))
Result: {"key":"value"}
Synchronously download a JPEG file, then create an Emacs image object
from the data:
(let ((jpeg-data (plz 'get "https://httpbin.org/image/jpeg" :as 'binary)))
(create-image jpeg-data nil 'data))
(image :type jpeg :data ""ÿØÿà^@^PJFIF...")
File: README.info, Node: Functions, Next: Queueing, Prev: Examples, Up: Usage
2.2 Functions
=============
‘plz’
_(method url &key headers body else finally noquery (as ’string)
(then ’sync) (body-type ’text) (decode t decode-s) (connect-timeout
plz-connect-timeout) (timeout plz-timeout))_
Request ‘METHOD’ from ‘URL’ with curl. Return the curl process
object or, for a synchronous request, the selected result.
‘HEADERS’ may be an alist of extra headers to send with the
request.
‘BODY-TYPE’ may be ‘text’ to send ‘BODY’ as text, or ‘binary’ to
send it as binary.
‘AS’ selects the kind of result to pass to the callback function
‘THEN’, or the kind of result to return for synchronous requests.
It may be:
• ‘buffer’ to pass the response buffer.
• ‘binary’ to pass the response body as an undecoded string.
• ‘string’ to pass the response body as a decoded string.
• ‘response’ to pass a ‘plz-response’ struct.
• A function, to pass its return value; it is called in the
response buffer, which is narrowed to the response body
(suitable for, e.g. ‘json-read’).
• ‘file’ to pass a temporary filename to which the response body
has been saved without decoding.
• ‘(file FILENAME)’ to pass ‘FILENAME’ after having saved the
response body to it without decoding. ‘FILENAME’ must be a
non-existent file; if it exists, it will not be overwritten,
and an error will be signaled.
If ‘DECODE’ is non-nil, the response body is decoded automatically.
For binary content, it should be nil. When ‘AS’ is ‘binary’,
‘DECODE’ is automatically set to nil.
‘THEN’ is a callback function, whose sole argument is selected
above with ‘AS’. Or ‘THEN’ may be ‘sync’ to make a synchronous
request, in which case the result is returned directly.
‘ELSE’ is an optional callback function called when the request
fails with one argument, a ‘plz-error’ struct. If ‘ELSE’ is nil,
an error is signaled when the request fails, either
‘plz-curl-error’ or ‘plz-http-error’ as appropriate, with a
‘plz-error’ struct as the error data. For synchronous requests,
this argument is ignored.
‘FINALLY’ is an optional function called without argument after
‘THEN’ or ‘ELSE’, as appropriate. For synchronous requests, this
argument is ignored.
‘CONNECT-TIMEOUT’ and ‘TIMEOUT’ are a number of seconds that limit
how long it takes to connect to a host and to receive a response
from a host, respectively.
‘NOQUERY’ is passed to ‘make-process’, which see.
File: README.info, Node: Queueing, Next: Tips, Prev: Functions, Up: Usage
2.3 Queueing
============
‘plz’ provides a simple system for queueing HTTP requests. First, make
a ‘plz-queue’ struct by calling ‘make-plz-queue’. Then call ‘plz-queue’
with the struct as the first argument, and the rest of the arguments
being the same as those passed to ‘plz’. Then call ‘plz-run’ to run the
queued requests.
All of the queue-related functions return the queue as their value,
making them easy to use. For example:
(defvar my-queue (make-plz-queue :limit 2))
(plz-run
(plz-queue my-queue
'get "https://httpbin.org/get?foo=0"
:then (lambda (body) (message "%s" body))))
Or:
(let ((queue (make-plz-queue :limit 2))
(urls '("https://httpbin.org/get?foo=0"
"https://httpbin.org/get?foo=1")))
(plz-run
(dolist (url urls queue)
(plz-queue queue 'get url
:then (lambda (body) (message "%s" body))))))
You may also clear a queue with ‘plz-clear’, which cancels any active
or queued requests and calls their ‘:else’ functions. And ‘plz-length’
returns the number of a queue’s active and queued requests.
File: README.info, Node: Tips, Prev: Queueing, Up: Usage
2.4 Tips
========
• You can customize settings in the ‘plz’ group, but this can only be
used to adjust a few defaults. It’s not intended that changing or
binding global variables be necessary for normal operation.
File: README.info, Node: Changelog, Next: Credits, Prev: Usage, Up: Top
3 Changelog
***********
* Menu:
* 0.3: 03.
* 0.2.1: 021.
* 0.2: 02.
* 0.1: 01.
File: README.info, Node: 03, Next: 021, Up: Changelog
3.1 0.3
=======
*Additions*
• Handle HTTP proxy headers from Curl.
*Fixes*
• Replaced words not in Ispell’s default dictionaries (so ‘checkdoc’
linting succeeds).
File: README.info, Node: 021, Next: 02, Prev: 03, Up: Changelog
3.2 0.2.1
=========
*Fixes*
• Handle when Curl process is interrupted.
File: README.info, Node: 02, Next: 01, Prev: 021, Up: Changelog
3.3 0.2
=======
*Added*
• Simple request queueing.
File: README.info, Node: 01, Prev: 02, Up: Changelog
3.4 0.1
=======
Initial release.
File: README.info, Node: Credits, Next: Development, Prev: Changelog, Up: Top
4 Credits
*********
• Thanks to Chris Wellons (https://github.com/skeeto), author of the
Elfeed (https://github.com/skeeto/elfeed) feed reader and the
popular blog null program (https://nullprogram.com/), for his
invaluable advice, review, and encouragement.
File: README.info, Node: Development, Next: License, Prev: Credits, Up: Top
5 Development
*************
Bug reports, feature requests, suggestions — _oh my_!
Note that ‘plz’ is a young library, and its only client so far is
Ement.el (https://github.com/alphapapa/ement.el). There are a variety
of HTTP and ‘curl’ features it does not yet support, since they have not
been needed by the author. Patches are welcome, as long as they include
passing tests.
* Menu:
* Copyright assignment::
File: README.info, Node: Copyright assignment, Up: Development
5.1 Copyright assignment
========================
This package is part of GNU Emacs (https://www.gnu.org/software/emacs/),
being distributed in GNU ELPA (https://elpa.gnu.org/). Contributions to
this project must follow GNU guidelines, which means that, as with other
parts of Emacs, patches of more than a few lines must be accompanied by
having assigned copyright for the contribution to the FSF. Contributors
who wish to do so may contact emacs-devel@gnu.org <emacs-devel@gnu.org>
to request the assignment form.
File: README.info, Node: License, Prev: Development, Up: Top
6 License
*********
GPLv3
Tag Table:
Node: Top199
Node: Installation1084
Node: GNU ELPA1227
Node: Manual1473
Node: Usage1779
Node: Examples2280
Node: Functions3647
Node: Queueing6542
Node: Tips7800
Node: Changelog8101
Node: 038266
Node: 0218516
Node: 028665
Node: 018794
Node: Credits8888
Node: Development9254
Node: Copyright assignment9768
Node: License10356
End Tag Table
Local Variables:
coding: utf-8
End:
;;; test-plz.el --- Tests for plz -*- lexical-binding: t; -*-
;; Copyright (C) 2019-2022 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This file is part of GNU Emacs.
;;; License:
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; NOTE: NOTE: NOTE: NOTE: Yes, four NOTEs, because this is important:
;; As of this moment, all of the tests pass when run with makem.sh.
;; And when running them in an interactive Emacs with ERT, one test at
;; a time, individual tests pass, or almost always do (depending on
;; whether the httpbin.org server is overloaded). But when running
;; multiple tests in ERT at one time,
;; i.e. (ert-run-tests-interactively "plz-"), multiple, if not most,
;; tests fail, but not the same ones every time.
;; I have now spent hours trying to figure out why, inserting many
;; debug statements in many functions, and come up with nothing. I
;; tried changing the way `accept-process-output' is called, like
;; using timeouts or JUST-THIS-ONE, but it made no difference. I
;; tried calling it extra times, nope. I tried calling the sentinel
;; extra times when it seemed that it hadn't run the THEN function,
;; nope. Nothing seems to make a difference.
;; I even checked out an earlier commit, before the commit that
;; rewrote/merged the synchronous request code into the `plz'
;; function, thinking that surely I broke something--but, nope, they
;; apparently failed the same way back then: passing with makem.sh,
;; passing individually, but failing when run in close succession by
;; ERT.
;; After inserting enough debug statements, I noticed that the process
;; sentinel sometimes seemed to run for the last time after the ERT
;; test had returned, which suggests that ERT might be doing something
;; weird, or somehow its instrumentation interferes with the
;; process-handling code. But if that's not the cause, then I'm out
;; of ideas.
;; So then I tried rewriting the synchronous request code to use
;; `call-process-region', instead of calling `accept-process-output'
;; in a loop to block on the curl process (which is how the Elisp
;; manual says to do it), but that still made no difference: even the
;; async requests fail in the same way with ERT. So that doesn't
;; appear to be the problem, either.
;; So is there some kind of fundamental flaw in the `plz' design?
;; Maybe. Is there a simple, logical oversight in its code that only
;; manifests under certain conditions? Maybe. Is ERT doing something
;; weird that's interfering with process-related code? Maybe. Is
;; Emacs's own process-handling code still broken in some mysterious
;; way? Maybe.
;; But despite all of that, when using `plz' "in anger", in `ement',
;; it seems to work reliably for me. I did get one report from one
;; user that sounded like the same kind of problem I'm seeing with ERT
;; here, but then he tried `ement-connect' again, and it worked. And
;; I'm sitting here watching `ement' constantly using `plz' to talk to
;; the matrix.org server, and I haven't had a single error or failure,
;; even after hours of being connected. It *seems* to *actually*
;; work.
;; So, if you're reading this, and you're wondering whether you should
;; use `plz': Well, please do, and please let me know if you have any
;; problems; I do need to know whether it's working for other users.
;; And if you think you might know what's going wrong when running the
;; tests in ERT, please let me know, because I'm out of ideas: as far
;; as I can tell, when it comes to process-handling in Emacs, "there
;; be dragons."
;;; Code:
;;;; Requirements
(require 'ert)
(require 'json)
(require 'let-alist)
(require 'plz)
;;;; Variables
;;;; Customization
;;;; Commands
;;;; Macros
(cl-defun plz-test-wait (process &optional (seconds 0.1) (times 100))
"Wait for SECONDS seconds TIMES times for PROCESS to finish."
(when process
;; Sometimes it seems that the process is killed, the THEN
;; function called by its sentinel, and its buffer killed, all
;; before this function gets called with the process argument;
;; when that happens, tests that use this can fail. Testing
;; whether PROCESS is non-nil seems to fix it, but it's possible
;; that something funny is going on...
(cl-loop for i upto times ;; 10 seconds
while (equal 'run (process-status process))
do (sleep-for seconds))))
;;;; Functions
(defmacro plz-test-get-response (response)
"Test parts of RESPONSE with `should'."
`(and (should (plz-response-p ,response))
(should (numberp (plz-response-version ,response)))
(should (eq 200 (plz-response-status ,response)))
(should (equal "application/json" (alist-get 'content-type (plz-response-headers ,response))))
(let* ((json (json-read-from-string (plz-response-body ,response)))
(headers (alist-get 'headers json))
(user-agent (alist-get 'User-Agent headers nil nil #'equal)))
(should (string-match "curl" user-agent)))))
;;;; Tests
;;;;; Async
(ert-deftest plz-get-string nil
(let* ((test-string)
(process (plz 'get "https://httpbin.org/get"
:as 'string
:then (lambda (string)
(setf test-string string)))))
(plz-test-wait process)
(should (string-match "curl" test-string))))
(ert-deftest plz-get-buffer nil
;; The sentinel kills the buffer, so we get the buffer as a string.
(let* ((test-buffer-string)
(process (plz 'get "https://httpbin.org/get"
:as 'buffer
:then (lambda (buffer)
(with-current-buffer buffer
(setf test-buffer-string (buffer-string)))))))
(plz-test-wait process)
(should (string-match "curl" test-buffer-string))))
(ert-deftest plz-get-response nil
(let* ((test-response)
(process (plz 'get "https://httpbin.org/get"
:as 'response
:then (lambda (response)
(setf test-response response)))))
(plz-test-wait process)
(plz-test-get-response test-response)))
(ert-deftest plz-get-json nil
(let* ((test-json)
(process (plz 'get "https://httpbin.org/get"
:as #'json-read
:then (lambda (json)
(setf test-json json)))))
(plz-test-wait process)
(let-alist test-json
(should (string-match "curl" .headers.User-Agent)))))
(ert-deftest plz-post-json-string nil
(let* ((json-string (json-encode (list (cons "key" "value"))))
(response-json)
(process (plz 'post "https://httpbin.org/post"
:headers '(("Content-Type" . "application/json"))
:body json-string
:as #'json-read
:then (lambda (json)
(setf response-json json)))))
(plz-test-wait process)
(let-alist response-json
(should (string-match "curl" .headers.User-Agent))
(should (string= "value" (alist-get 'key (json-read-from-string .data)))))))
(ert-deftest plz-post-jpeg-string nil
(let* ((jpeg-to-upload (plz 'get "https://httpbin.org/image/jpeg"
:as 'binary :then 'sync))
(_ (unless jpeg-to-upload
(error "jpeg-to-upload is nil")))
(response-json)
(response-jpeg)
(process (plz 'post "https://httpbin.org/post"
:headers '(("Content-Type" . "image/jpeg"))
:body jpeg-to-upload :body-type 'binary
:as #'json-read
:then (lambda (json)
(setf response-json json
response-jpeg
(base64-decode-string
(string-remove-prefix "data:application/octet-stream;base64,"
(alist-get 'data json))))))))
(should (equal 'jpeg (image-type-from-data jpeg-to-upload)))
(plz-test-wait process)
(should response-json)
(should (equal 'jpeg (image-type-from-data response-jpeg)))
(should (equal (length jpeg-to-upload) (length response-jpeg)))
(should (equal jpeg-to-upload response-jpeg))))
;; TODO: POST JSON buffer.
(ert-deftest plz-put-json-string nil
(let* ((json-string (json-encode (list (cons "key" "value"))))
(response-json)
(process (plz 'put "https://httpbin.org/put"
:headers '(("Content-Type" . "application/json"))
:body json-string
:as #'json-read
:then (lambda (json)
(setf response-json json)))))
(plz-test-wait process)
(let-alist response-json
(should (string-match "curl" .headers.User-Agent))
(should (string= "value" (alist-get 'key (json-read-from-string .data)))))))
;; TODO: Put JSON buffer.
;;;;; Sync
(ert-deftest plz-get-string-sync nil
(let-alist (json-read-from-string (plz 'get "https://httpbin.org/get"
:as 'string :then 'sync))
(should (equal "https://httpbin.org/get" .url))))
(ert-deftest plz-get-response-sync nil
(plz-test-get-response (plz 'get "https://httpbin.org/get"
:as 'response :then 'sync)))
(ert-deftest plz-get-json-sync nil
(let-alist (plz 'get "https://httpbin.org/get"
:as #'json-read :then 'sync)
(should (string-match "curl" .headers.User-Agent))))
(ert-deftest plz-get-buffer-sync nil
(let ((buffer (plz 'get "https://httpbin.org/get"
:as 'buffer :then 'sync)))
(unwind-protect
(should (buffer-live-p buffer))
(kill-buffer buffer))))
;;;;; Headers
;; These tests were added when plz--curl was changed to send headers
;; with "--config" rather than on the command line.
(ert-deftest plz-get-with-headers ()
(let* ((response-json)
(process (plz 'get "https://httpbin.org/get"
:headers '(("X-Plz-Test-Header" . "plz-test-header-value"))
:as #'json-read
:then (lambda (json)
(setf response-json json)))))
(plz-test-wait process)
(let-alist response-json
(should (equal "plz-test-header-value" .headers.X-Plz-Test-Header)))))
(ert-deftest plz-post-with-headers ()
(let* ((alist (list (cons "key" "value")))
(response-json)
(process (plz 'post "https://httpbin.org/post"
:headers '(("Content-Type" . "application/json")
("X-Plz-Test-Header" . "plz-test-header-value"))
:body (json-encode alist)
:as #'json-read
:then (lambda (json)
(setf response-json json)))))
(plz-test-wait process)
(let-alist response-json
(should (equal "plz-test-header-value" .headers.X-Plz-Test-Header))
(should (equal "value" (alist-get 'key (json-read-from-string .data)))))))
(ert-deftest plz-get-json-with-headers-sync ()
(let-alist (plz 'get "https://httpbin.org/get"
:headers '(("X-Plz-Test-Header" . "plz-test-header-value"))
:as #'json-read :then 'sync)
(should (string-match "curl" .headers.User-Agent))
(should (equal "plz-test-header-value" .headers.X-Plz-Test-Header))))
;;;;; Errors
(ert-deftest plz-get-curl-error nil
;; Async.
(let* ((err)
(process (plz 'get "https://httpbinnnnnn.org/get/status/404"
:as 'string
:else (lambda (e)
(setf err e)))))
(plz-test-wait process)
(should (and (plz-error-p err)
(equal '(6 . "Couldn't resolve host. The given remote host was not resolved.")
(plz-error-curl-error err))))))
;; FIXME: This test works interactively but not in batch mode: it
;; stalls the Emacs process indefinitely, using either sleep-for or
;; sit-for.
;; (ert-deftest plz-get-killed-error nil
;; ;; Async.
;; (let* ((err)
;; (process (plz 'get "https://httpbinnnnnn.org/get/status/404"
;; :as 'string
;; :else (lambda (e)
;; (setf err e)))))
;; (sit-for 0.01)
;; (delete-process process)
;; (should (not (process-live-p process)))
;; (should (plz-error-p err))
;; (should (equal "curl process killed"
;; (plz-error-message err)))))
(ert-deftest plz-get-curl-error-sync nil
;; Sync.
(let ((err (should-error (plz 'get "https://httpbinnnnnn.org/get/status/404"
:as 'string :then 'sync)
:type 'plz-curl-error)))
(should (eq 'plz-curl-error (car err)))
(should (plz-error-p (cdr err)))
(should (equal '(6 . "Couldn't resolve host. The given remote host was not resolved.")
(plz-error-curl-error (cdr err))))))
(ert-deftest plz-get-404-error nil
;; FIXME: Wrap each test expression in `should' rather than using `should-and'.
;; Async.
(let* ((err)
(process (plz 'get "https://httpbin.org/get/status/404"
:as 'string
:else (lambda (e)
(setf err e)))))
(plz-test-wait process)
(should (and (plz-error-p err)
(plz-response-p (plz-error-response err))
(eq 404 (plz-response-status (plz-error-response err))))))
;; Sync.
(let ((err (should-error (plz 'get "https://httpbin.org/get/status/404"
:as 'string :then 'sync)
:type 'plz-http-error)))
(should (and (eq 'plz-http-error (car err))
(plz-error-p (cdr err))
(plz-response-p (plz-error-response (cdr err)))
(eq 404 (plz-response-status (plz-error-response (cdr err))))))))
(ert-deftest plz-get-timeout-error nil
;; Async.
(let* ((start-time (current-time))
(end-time)
(plz-error)
(process (plz 'get "https://httpbin.org/delay/5"
:as 'response :timeout 1
:else (lambda (e)
(setf end-time (current-time)
plz-error e)))))
(plz-test-wait process)
(should (eq 28 (car (plz-error-curl-error plz-error))))
(should (equal "Operation timeout." (cdr (plz-error-curl-error plz-error))))
(should (< (time-to-seconds (time-subtract end-time start-time)) 1.1)))
;; Sync.
(let ((start-time (current-time))
(err (cdr
(should-error (plz 'get "https://httpbin.org/delay/5"
:as 'string :then 'sync :timeout 1)
:type 'plz-curl-error)))
(end-time (current-time)))
(should (eq 28 (car (plz-error-curl-error err))))
(should (equal "Operation timeout." (cdr (plz-error-curl-error err))))
(should (< (time-to-seconds (time-subtract end-time start-time)) 1.1))))
;;;;; Finally
(ert-deftest plz-get-finally nil
(let* ((finally-null t)
(process (plz 'get "https://httpbin.org/get"
:as 'string
:then #'ignore
:finally (lambda ()
(setf finally-null nil)))))
(plz-test-wait process)
(should-not finally-null)))
;;;;; Binary
(ert-deftest plz-get-jpeg ()
(let* ((test-jpeg)
(process (plz 'get "https://httpbin.org/image/jpeg"
:as 'binary
:then (lambda (string)
(setf test-jpeg string)))))
(plz-test-wait process)
(should (equal 'jpeg (image-type-from-data test-jpeg)))))
(ert-deftest plz-get-jpeg-sync ()
(let ((jpeg (plz 'get "https://httpbin.org/image/jpeg"
:as 'binary :then 'sync)))
(should (equal 'jpeg (image-type-from-data jpeg)))))
;;;;; Downloading to files
(ert-deftest plz-get-temp-file ()
(let ((filename (plz 'get "https://httpbin.org/image/jpeg"
:as 'file :then 'sync)))
(unwind-protect
(let ((jpeg-data (with-temp-buffer
(insert-file-contents filename)
(buffer-string))))
(should (equal 'jpeg (image-type-from-data jpeg-data))))
;; It's a temp file, so it should always be deleted.
(delete-file filename))))
(ert-deftest plz-get-named-file ()
(let ((filename (make-temp-file "plz-")))
;; HACK: Delete the temp file and reuse its name, because
;; `make-temp-name' is less convenient to use.
(delete-file filename)
(unwind-protect
(progn
(plz 'get "https://httpbin.org/image/jpeg"
:as `(file ,filename) :then 'sync)
(let ((jpeg-data (with-temp-buffer
(insert-file-contents filename)
(buffer-string))))
(should (equal 'jpeg (image-type-from-data jpeg-data)))))
;; It's a temp file, so it should always be deleted.
(when (file-exists-p filename)
(delete-file filename)))))
;;;;; Queue
;; TODO: Test that limit is enforced (though it seems to work fine).
(ert-deftest plz-queue ()
(let ((queue (make-plz-queue :limit 2))
(urls '("https://httpbin.org/get?foo=0"
"https://httpbin.org/get?foo=1"))
completed-urls)
(dolist (url urls)
(plz-queue queue
'get url :then (lambda (_)
(push url completed-urls))))
(plz-run queue)
(cl-loop with waits = 0
while (and (plz-queue-active queue) (< waits 20))
do (progn
(sleep-for 0.1)
(cl-incf waits)))
(and (seq-set-equal-p urls completed-urls)
(zerop (plz-length queue)))))
;;;; Footer
(provide 'test-plz)
;;; test-plz.el ends here
;;; nadvice-autoloads.el --- automatically extracted autoloads
;;
;;; Code:
(add-to-list 'load-path (directory-file-name
(or (file-name-directory #$) (car load-path))))
;; Local Variables:
;; version-control: never
;; no-byte-compile: t
;; no-update-autoloads: t
;; coding: utf-8
;; End:
;;; nadvice-autoloads.el ends here
;;; -*- no-byte-compile: t -*-
(define-package "nadvice" "0.3" "Forward compatibility for Emacs-24.4's nadvice" 'nil :url "http://elpa.gnu.org/packages/nadvice.html" :authors '(("Stefan Monnier" . "monnier@iro.umontreal.ca")) :maintainer '("Stefan Monnier" . "monnier@iro.umontreal.ca"))
;;; nadvice.el --- Forward compatibility for Emacs-24.4's nadvice
;; Copyright (C) 2018 Free Software Foundation, Inc.
;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
;; Version: 0.3
;; Keywords:
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This package tries to re-implement some of nadvice.el's functionality
;; on top of the old defadvice system, to help users of defadvice
;; move to the new advice system without dropping support for Emacs<24.4.
;;
;; Limitations;
;; - only supports `advice-add' and `advice-remove';
;; - only handles the :before, :after, :override, and :around kinds of advice;
;; - requires a named rather than anonymous function;
;; - and does not support any additional properties like `name' or `depth'.
;;
;; It was tested on Emacs-22 and I can't see any obvious reason why it
;; wouldn't work on older Emacsen.
;;; Code:
(declare-function ad-remove-advice "advice")
(unless (fboundp 'add-function)
;; If `add-function' is defined, we're presumably running on
;; an Emacs that comes with the real nadvice.el, so let's be careful
;; to do nothing in that case!
;; Load `advice' manually, in case `advice-remove' is called first,
;; since ad-remove-advice is not autoloaded.
(require 'advice)
;;;###autoload
(defun advice-add (symbol where function &optional props)
(when props
(error "This version of nadvice.el does not support PROPS"))
(unless (symbolp function)
(error "This version of nadvice.el requires FUNCTION to be a symbol"))
(let ((body (cond
((eq where :before)
`(progn (apply #',function (ad-get-args 0)) ad-do-it))
((eq where :after)
`(progn ad-do-it (apply #',function (ad-get-args 0))))
((eq where :override)
`(setq ad-return-value (apply #',function (ad-get-args 0))))
((eq where :around)
`(setq ad-return-value
(apply #',function
(lambda (&rest nadvice--rest-arg)
(ad-set-args 0 nadvice--rest-arg)
ad-do-it)
(ad-get-args 0))))
(t (error "This version of nadvice.el does not handle %S"
where)))))
(ad-add-advice symbol
`(,function nil t (advice lambda () ,body))
'around
nil)
(ad-activate symbol)))
;;;###autoload
(defun advice-remove (symbol function)
;; Just return nil if there is no advice, rather than signaling an
;; error.
(condition-case nil
(ad-remove-advice symbol 'around function)
(error nil))
(condition-case nil
(ad-activate symbol)
(error nil)))
)
;;;; ChangeLog:
;; 2018-09-15 Thomas Fitzsimmons <fitzsim@fitzsim.org>
;;
;; packages/nadvice: Fix advice-remove behaviour
;;
;; * packages/nadvice/nadvice.el: Bump version to 0.3.
;; (advice-remove): Do not signal an error if the function already has no
;; advice.
;;
;; 2018-09-12 Stefan Monnier <monnier@iro.umontreal.ca>
;;
;; * nadvice.el: ad-remove-advice is not autoloaded
;;
;; 2018-09-12 Stefan Monnier <monnier@iro.umontreal.ca>
;;
;; * nadvice.el: Fix typo
;;
;; 2018-09-12 Stefan Monnier <monnier@iro.umontreal.ca>
;;
;; * nadvice/nadvice.el (advice-add): Add support for :override
;;
;; 2018-09-12 Stefan Monnier <monnier@iro.umontreal.ca>
;;
;; * nadvice.el: Fix copyright!
;;
;; 2018-09-12 Stefan Monnier <monnier@iro.umontreal.ca>
;;
;; * nadvice: New forward compatibility package
;;
(provide 'nadvice)
;;; nadvice.el ends here
;;; cider-apropos.el --- Apropos functionality for Clojure -*- lexical-binding: t -*-
;; Copyright © 2014-2022 Jeff Valk, Bozhidar Batsov and CIDER contributors
;;
;; Author: Jeff Valk <jv@jeffvalk.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Apropos functionality for Clojure.
;;; Code:
(require 'cider-doc) ; for cider-doc-lookup
(require 'cider-find) ; for cider--find-var
(require 'cider-util)
(require 'subr-x)
(require 'cider-connection) ; for cider-ensure-connected
(require 'cider-client)
(require 'cider-popup)
(require 'nrepl-dict)
(require 'apropos)
(require 'button)
(defconst cider-apropos-buffer "*cider-apropos*")
(defcustom cider-apropos-actions '(("display-doc" . cider-doc-lookup)
("find-def" . cider--find-var)
("lookup-on-clojuredocs" . cider-clojuredocs-lookup))
"Controls the actions to be applied on the symbol found by an apropos search.
The first action key in the list will be selected as default. If the list
contains only one action key, the associated action function will be
applied automatically. An action function can be any function that receives
the symbol found by the apropos search as argument."
:type '(alist :key-type string :value-type function)
:group 'cider
:package-version '(cider . "0.13.0"))
(define-button-type 'apropos-special-form
'apropos-label "Special form"
'apropos-short-label "s"
'face 'font-lock-keyword-face
'help-echo "mouse-2, RET: Display more help on this special form"
'follow-link t
'action (lambda (button)
(describe-function (button-get button 'apropos-symbol))))
(defun cider-apropos-doc (button)
"Display documentation for the symbol represented at BUTTON."
(cider-doc-lookup (button-get button 'apropos-symbol)))
(defun cider-apropos-summary (query ns docs-p include-private-p case-sensitive-p)
"Return a short description for the performed apropos search.
QUERY can be a regular expression list of space-separated words
\(e.g take while) which will be converted to a regular expression
\(like take.+while) automatically behind the scenes. The search may be
limited to the namespace NS, and may optionally search doc strings
\(based on DOCS-P), include private vars (based on INCLUDE-PRIVATE-P),
and be case-sensitive (based on CASE-SENSITIVE-P)."
(concat (if case-sensitive-p "Case-sensitive " "")
(if docs-p "Documentation " "")
(format "Apropos for %S" query)
(if ns (format " in namespace %S" ns) "")
(if include-private-p
" (public and private symbols)"
" (public symbols only)")))
(defun cider-apropos-highlight (doc query)
"Return the DOC string propertized to highlight QUERY matches."
(let ((pos 0))
(while (string-match query doc pos)
(setq pos (match-end 0))
(put-text-property (match-beginning 0)
(match-end 0)
'font-lock-face apropos-match-face doc)))
doc)
(defvar cider-use-tooltips)
(defun cider-apropos-result (result query docs-p)
"Emit a RESULT matching QUERY into current buffer, formatted for DOCS-P."
(nrepl-dbind-response result (name type doc)
(let* ((label (capitalize (if (string= type "variable") "var" type)))
(help (concat "Display doc for this " (downcase label)))
(props (list 'apropos-symbol name
'action #'cider-apropos-doc))
(props (if cider-use-tooltips
(append props (list 'help-echo help))
props)))
(cider-propertize-region props
(insert-text-button name 'type 'apropos-symbol)
(insert "\n ")
(insert-text-button label 'type (intern (concat "apropos-" type)))
(insert ": ")
(let ((beg (point)))
(if docs-p
(insert (cider-apropos-highlight doc query) "\n")
(insert doc)
(fill-region beg (point))))
(insert "\n")))))
(defun cider-show-apropos (summary results query docs-p)
"Show SUMMARY and RESULTS for QUERY in a pop-up buffer, formatted for DOCS-P."
(with-current-buffer (cider-popup-buffer cider-apropos-buffer 'select 'apropos-mode 'ancillary)
(let ((inhibit-read-only t))
(if (boundp 'header-line-format)
(setq-local header-line-format summary)
(insert summary "\n\n"))
(dolist (result results)
(cider-apropos-result result query docs-p))
(goto-char (point-min)))))
;;;###autoload
(defun cider-apropos (query &optional ns docs-p privates-p case-sensitive-p)
"Show all symbols whose names match QUERY, a regular expression.
QUERY can also be a list of space-separated words (e.g. take while) which
will be converted to a regular expression (like take.+while) automatically
behind the scenes. The search may be limited to the namespace NS, and may
optionally search doc strings (based on DOCS-P), include private vars
\(based on PRIVATES-P), and be case-sensitive (based on CASE-SENSITIVE-P)."
(interactive
(cons (read-string "Search for Clojure symbol (a regular expression): ")
(when current-prefix-arg
(list (let ((ns (completing-read "Namespace (default is all): " (cider-sync-request:ns-list))))
(if (string= ns "") nil ns))
(y-or-n-p "Search doc strings? ")
(y-or-n-p "Include private symbols? ")
(y-or-n-p "Case-sensitive? ")))))
(cider-ensure-connected)
(cider-ensure-op-supported "apropos")
(if-let* ((summary (cider-apropos-summary
query ns docs-p privates-p case-sensitive-p))
(results (cider-sync-request:apropos query ns docs-p privates-p case-sensitive-p)))
(cider-show-apropos summary results query docs-p)
(message "No apropos matches for %S" query)))
;;;###autoload
(defun cider-apropos-documentation ()
"Shortcut for (cider-apropos <query> nil t)."
(interactive)
(cider-ensure-connected)
(cider-ensure-op-supported "apropos")
(cider-apropos (read-string "Search for Clojure documentation (a regular expression): ") nil t))
(defun cider-apropos-act-on-symbol (symbol)
"Apply selected action on SYMBOL."
(let* ((first-action-key (car (car cider-apropos-actions)))
(action-key (if (= 1 (length cider-apropos-actions))
first-action-key
(completing-read (format "Choose action to apply to `%s` (default %s): "
symbol first-action-key)
cider-apropos-actions nil nil nil nil first-action-key)))
(action-fn (cdr (assoc action-key cider-apropos-actions))))
(if action-fn
(funcall action-fn symbol)
(user-error "Unknown action `%s`" action-key))))
;;;###autoload
(defun cider-apropos-select (query &optional ns docs-p privates-p case-sensitive-p)
"Similar to `cider-apropos', but presents the results in a completing read.
Show all symbols whose names match QUERY, a regular expression.
QUERY can also be a list of space-separated words (e.g. take while) which
will be converted to a regular expression (like take.+while) automatically
behind the scenes. The search may be limited to the namespace NS, and may
optionally search doc strings (based on DOCS-P), include private vars
\(based on PRIVATES-P), and be case-sensitive (based on CASE-SENSITIVE-P)."
(interactive
(cons (read-string "Search for Clojure symbol (a regular expression): ")
(when current-prefix-arg
(list (let ((ns (completing-read "Namespace (default is all): " (cider-sync-request:ns-list))))
(if (string= ns "") nil ns))
(y-or-n-p "Search doc strings? ")
(y-or-n-p "Include private symbols? ")
(y-or-n-p "Case-sensitive? ")))))
(cider-ensure-connected)
(cider-ensure-op-supported "apropos")
(if-let* ((summary (cider-apropos-summary
query ns docs-p privates-p case-sensitive-p))
(results (mapcar (lambda (r) (nrepl-dict-get r "name"))
(cider-sync-request:apropos query ns docs-p privates-p case-sensitive-p))))
(cider-apropos-act-on-symbol (completing-read (concat summary ": ") results))
(message "No apropos matches for %S" query)))
;;;###autoload
(defun cider-apropos-documentation-select ()
"Shortcut for (cider-apropos-select <query> nil t)."
(interactive)
(cider-ensure-connected)
(cider-ensure-op-supported "apropos")
(cider-apropos-select (read-string "Search for Clojure documentation (a regular expression): ") nil t))
(provide 'cider-apropos)
;;; cider-apropos.el ends here
;;; cider-autoloads.el --- automatically extracted autoloads -*- lexical-binding: t -*-
;;
;;; Code:
(add-to-list 'load-path (directory-file-name
(or (file-name-directory #$) (car load-path))))
;;;### (autoloads nil "cider" "cider.el" (0 0 0 0))
;;; Generated autoloads from cider.el
(autoload 'cider-version "cider" "\
Display CIDER's version." t nil)
(autoload 'cider-start-map "cider" "CIDER jack-in and connect keymap." t 'keymap)
(autoload 'cider-jack-in-clj "cider" "\
Start an nREPL server for the current project and connect to it.
PARAMS is a plist optionally containing :project-dir and :jack-in-cmd.
With the prefix argument, allow editing of the jack in command; with a
double prefix prompt for all these parameters.
\(fn PARAMS)" t nil)
(autoload 'cider-jack-in-cljs "cider" "\
Start an nREPL server for the current project and connect to it.
PARAMS is a plist optionally containing :project-dir, :jack-in-cmd and
:cljs-repl-type (e.g. Node, Figwheel, etc). With the prefix argument,
allow editing of the jack in command; with a double prefix prompt for all
these parameters.
\(fn PARAMS)" t nil)
(autoload 'cider-jack-in-clj&cljs "cider" "\
Start an nREPL server and connect with clj and cljs REPLs.
PARAMS is a plist optionally containing :project-dir, :jack-in-cmd and
:cljs-repl-type (e.g. Node, Figwheel, etc). With the prefix argument,
allow for editing of the jack in command; with a double prefix prompt for
all these parameters. When SOFT-CLJS-START is non-nil, start cljs REPL
only when the ClojureScript dependencies are met.
\(fn &optional PARAMS SOFT-CLJS-START)" t nil)
(autoload 'cider-connect-sibling-clj "cider" "\
Create a Clojure REPL with the same server as OTHER-REPL.
PARAMS is for consistency with other connection commands and is currently
ignored. OTHER-REPL defaults to `cider-current-repl' and in programs can
also be a server buffer, in which case a new session with a REPL for that
server is created.
\(fn PARAMS &optional OTHER-REPL)" t nil)
(autoload 'cider-connect-sibling-cljs "cider" "\
Create a ClojureScript REPL with the same server as OTHER-REPL.
PARAMS is a plist optionally containing :cljs-repl-type (e.g. Node,
Figwheel, etc). All other parameters are inferred from the OTHER-REPL.
OTHER-REPL defaults to `cider-current-repl' but in programs can also be a
server buffer, in which case a new session for that server is created.
\(fn PARAMS &optional OTHER-REPL)" t nil)
(autoload 'cider-connect-clj "cider" "\
Initialize a Clojure connection to an nREPL server.
PARAMS is a plist optionally containing :host, :port and :project-dir. On
prefix argument, prompt for all the parameters.
\(fn &optional PARAMS)" t nil)
(autoload 'cider-connect-cljs "cider" "\
Initialize a ClojureScript connection to an nREPL server.
PARAMS is a plist optionally containing :host, :port, :project-dir and
:cljs-repl-type (e.g. Node, Figwheel, etc). On prefix, prompt for all the
parameters regardless of their supplied or default values.
\(fn &optional PARAMS)" t nil)
(autoload 'cider-connect-clj&cljs "cider" "\
Initialize a Clojure and ClojureScript connection to an nREPL server.
PARAMS is a plist optionally containing :host, :port, :project-dir and
:cljs-repl-type (e.g. Node, Figwheel, etc). When SOFT-CLJS-START is
non-nil, don't start if ClojureScript requirements are not met.
\(fn PARAMS &optional SOFT-CLJS-START)" t nil)
(autoload 'cider "cider" "\
Start a connection of any type interactively." t nil)
(defalias 'cider-jack-in #'cider-jack-in-clj)
(defalias 'cider-connect #'cider-connect-clj)
(with-eval-after-load 'clojure-mode (define-key clojure-mode-map (kbd "C-c M-x") #'cider) (define-key clojure-mode-map (kbd "C-c M-j") #'cider-jack-in-clj) (define-key clojure-mode-map (kbd "C-c M-J") #'cider-jack-in-cljs) (define-key clojure-mode-map (kbd "C-c M-c") #'cider-connect-clj) (define-key clojure-mode-map (kbd "C-c M-C") #'cider-connect-cljs) (define-key clojure-mode-map (kbd "C-c C-x") 'cider-start-map) (define-key clojure-mode-map (kbd "C-c C-s") 'sesman-map) (require 'sesman) (sesman-install-menu clojure-mode-map) (add-hook 'clojure-mode-hook (lambda nil (setq-local sesman-system 'CIDER))))
(register-definition-prefixes "cider" '("cider-"))
;;;***
;;;### (autoloads nil "cider-apropos" "cider-apropos.el" (0 0 0 0))
;;; Generated autoloads from cider-apropos.el
(autoload 'cider-apropos "cider-apropos" "\
Show all symbols whose names match QUERY, a regular expression.
QUERY can also be a list of space-separated words (e.g. take while) which
will be converted to a regular expression (like take.+while) automatically
behind the scenes. The search may be limited to the namespace NS, and may
optionally search doc strings (based on DOCS-P), include private vars
\(based on PRIVATES-P), and be case-sensitive (based on CASE-SENSITIVE-P).
\(fn QUERY &optional NS DOCS-P PRIVATES-P CASE-SENSITIVE-P)" t nil)
(autoload 'cider-apropos-documentation "cider-apropos" "\
Shortcut for (cider-apropos <query> nil t)." t nil)
(autoload 'cider-apropos-select "cider-apropos" "\
Similar to `cider-apropos', but presents the results in a completing read.
Show all symbols whose names match QUERY, a regular expression.
QUERY can also be a list of space-separated words (e.g. take while) which
will be converted to a regular expression (like take.+while) automatically
behind the scenes. The search may be limited to the namespace NS, and may
optionally search doc strings (based on DOCS-P), include private vars
\(based on PRIVATES-P), and be case-sensitive (based on CASE-SENSITIVE-P).
\(fn QUERY &optional NS DOCS-P PRIVATES-P CASE-SENSITIVE-P)" t nil)
(autoload 'cider-apropos-documentation-select "cider-apropos" "\
Shortcut for (cider-apropos-select <query> nil t)." t nil)
(register-definition-prefixes "cider-apropos" '("apropos-special-form" "cider-"))
;;;***
;;;### (autoloads nil "cider-browse-ns" "cider-browse-ns.el" (0 0
;;;;;; 0 0))
;;; Generated autoloads from cider-browse-ns.el
(autoload 'cider-browse-ns "cider-browse-ns" "\
List all NAMESPACE's vars in BUFFER.
\(fn NAMESPACE)" t nil)
(autoload 'cider-browse-ns-all "cider-browse-ns" "\
List all loaded namespaces in BUFFER." t nil)
(register-definition-prefixes "cider-browse-ns" '("cider-browse-ns-"))
;;;***
;;;### (autoloads nil "cider-browse-spec" "cider-browse-spec.el"
;;;;;; (0 0 0 0))
;;; Generated autoloads from cider-browse-spec.el
(autoload 'cider-browse-spec "cider-browse-spec" "\
Browse SPEC definition.
\(fn SPEC)" t nil)
(autoload 'cider-browse-spec-all "cider-browse-spec" "\
Open list of specs in a popup buffer.
With a prefix argument ARG, prompts for a regexp to filter specs.
No filter applied if the regexp is the empty string.
\(fn &optional ARG)" t nil)
(register-definition-prefixes "cider-browse-spec" '("cider-"))
;;;***
;;;### (autoloads nil "cider-cheatsheet" "cider-cheatsheet.el" (0
;;;;;; 0 0 0))
;;; Generated autoloads from cider-cheatsheet.el
(autoload 'cider-cheatsheet "cider-cheatsheet" "\
Navigate `cider-cheatsheet-hierarchy' with `completing-read'.
When you make it to a Clojure var its doc buffer gets displayed." t nil)
(register-definition-prefixes "cider-cheatsheet" '("cider-cheatsheet-"))
;;;***
;;;### (autoloads nil "cider-classpath" "cider-classpath.el" (0 0
;;;;;; 0 0))
;;; Generated autoloads from cider-classpath.el
(autoload 'cider-classpath "cider-classpath" "\
List all classpath entries." t nil)
(autoload 'cider-open-classpath-entry "cider-classpath" "\
Open a classpath entry." t nil)
(register-definition-prefixes "cider-classpath" '("cider-classpath-"))
;;;***
;;;### (autoloads nil "cider-client" "cider-client.el" (0 0 0 0))
;;; Generated autoloads from cider-client.el
(register-definition-prefixes "cider-client" '("cider-"))
;;;***
;;;### (autoloads nil "cider-clojuredocs" "cider-clojuredocs.el"
;;;;;; (0 0 0 0))
;;; Generated autoloads from cider-clojuredocs.el
(autoload 'cider-clojuredocs-web "cider-clojuredocs" "\
Open ClojureDocs documentation in the default web browser.
Prompts for the symbol to use, or uses the symbol at point, depending on
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
opposite of what that option dictates.
\(fn &optional ARG)" t nil)
(autoload 'cider-clojuredocs-refresh-cache "cider-clojuredocs" "\
Refresh the ClojureDocs cache." t nil)
(autoload 'cider-clojuredocs "cider-clojuredocs" "\
Open ClojureDocs documentation in a popup buffer.
Prompts for the symbol to use, or uses the symbol at point, depending on
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
opposite of what that option dictates.
\(fn &optional ARG)" t nil)
(register-definition-prefixes "cider-clojuredocs" '("cider-"))
;;;***
;;;### (autoloads nil "cider-common" "cider-common.el" (0 0 0 0))
;;; Generated autoloads from cider-common.el
(register-definition-prefixes "cider-common" '("cider-"))
;;;***
;;;### (autoloads nil "cider-completion" "cider-completion.el" (0
;;;;;; 0 0 0))
;;; Generated autoloads from cider-completion.el
(register-definition-prefixes "cider-completion" '("cider-"))
;;;***
;;;### (autoloads nil "cider-connection" "cider-connection.el" (0
;;;;;; 0 0 0))
;;; Generated autoloads from cider-connection.el
(defvar cider-merge-sessions nil "\
Controls session combination behaviour.
Symbol `host' combines all sessions of a project associated with the same host.
Symbol `project' combines all sessions of a project.
All other values do not combine any sessions.")
(custom-autoload 'cider-merge-sessions "cider-connection" t)
(put 'cider-merge-sessions 'safe-local-variable #'symbolp)
(register-definition-prefixes "cider-connection" '("cider-"))
;;;***
;;;### (autoloads nil "cider-debug" "cider-debug.el" (0 0 0 0))
;;; Generated autoloads from cider-debug.el
(autoload 'cider-debug-defun-at-point "cider-debug" "\
Instrument the \"top-level\" expression at point.
If it is a defn, dispatch the instrumented definition. Otherwise,
immediately evaluate the instrumented expression.
While debugged code is being evaluated, the user is taken through the
source code and displayed the value of various expressions. At each step,
a number of keys will be prompted to the user." t nil)
(register-definition-prefixes "cider-debug" '("cider-"))
;;;***
;;;### (autoloads nil "cider-doc" "cider-doc.el" (0 0 0 0))
;;; Generated autoloads from cider-doc.el
(register-definition-prefixes "cider-doc" '("cider-"))
;;;***
;;;### (autoloads nil "cider-eldoc" "cider-eldoc.el" (0 0 0 0))
;;; Generated autoloads from cider-eldoc.el
(register-definition-prefixes "cider-eldoc" '("cider-"))
;;;***
;;;### (autoloads nil "cider-eval" "cider-eval.el" (0 0 0 0))
;;; Generated autoloads from cider-eval.el
(register-definition-prefixes "cider-eval" '("cider-"))
;;;***
;;;### (autoloads nil "cider-find" "cider-find.el" (0 0 0 0))
;;; Generated autoloads from cider-find.el
(autoload 'cider-find-var "cider-find" "\
Find definition for VAR at LINE.
Prompt according to prefix ARG and `cider-prompt-for-symbol'.
A single or double prefix argument inverts the meaning of
`cider-prompt-for-symbol'. A prefix of `-` or a double prefix argument causes
the results to be displayed in a different window. The default value is
thing at point.
\(fn &optional ARG VAR LINE)" t nil)
(autoload 'cider-find-dwim-at-mouse "cider-find" "\
Find and display variable or resource at mouse EVENT.
\(fn EVENT)" t nil)
(autoload 'cider-find-dwim "cider-find" "\
Find and display the SYMBOL-FILE at point.
SYMBOL-FILE could be a var or a resource. If thing at point is empty then
show Dired on project. If var is not found, try to jump to resource of the
same name. When called interactively, a prompt is given according to the
variable `cider-prompt-for-symbol'. A single or double prefix argument
inverts the meaning. A prefix of `-' or a double prefix argument causes
the results to be displayed in a different window. A default value of thing
at point is given when prompted.
\(fn SYMBOL-FILE)" t nil)
(autoload 'cider-find-resource "cider-find" "\
Find the resource at PATH.
Prompt for input as indicated by the variable `cider-prompt-for-symbol'.
A single or double prefix argument inverts the meaning of
`cider-prompt-for-symbol'. A prefix argument of `-` or a double prefix
argument causes the results to be displayed in other window. The default
value is thing at point.
\(fn PATH)" t nil)
(autoload 'cider-find-ns "cider-find" "\
Find the file containing NS.
A prefix ARG of `-` or a double prefix argument causes
the results to be displayed in a different window.
\(fn &optional ARG NS)" t nil)
(autoload 'cider-find-keyword "cider-find" "\
Find the namespace of the keyword at point and its first occurrence there.
For instance - if the keyword at point is \":cider.demo/keyword\", this command
would find the namespace \"cider.demo\" and afterwards find the first mention
of \"::keyword\" there.
Prompt according to prefix ARG and `cider-prompt-for-symbol'.
A single or double prefix argument inverts the meaning of
`cider-prompt-for-symbol'. A prefix of `-` or a double prefix argument causes
the results to be displayed in a different window. The default value is
thing at point.
\(fn &optional ARG)" t nil)
(register-definition-prefixes "cider-find" '("cider-"))
;;;***
;;;### (autoloads nil "cider-format" "cider-format.el" (0 0 0 0))
;;; Generated autoloads from cider-format.el
(autoload 'cider-format-region "cider-format" "\
Format the Clojure code in the current region.
START and END represent the region's boundaries.
\(fn START END)" t nil)
(autoload 'cider-format-defun "cider-format" "\
Format the code in the current defun." t nil)
(autoload 'cider-format-buffer "cider-format" "\
Format the Clojure code in the current buffer." t nil)
(autoload 'cider-format-edn-buffer "cider-format" "\
Format the EDN data in the current buffer." t nil)
(autoload 'cider-format-edn-region "cider-format" "\
Format the EDN data in the current region.
START and END represent the region's boundaries.
\(fn START END)" t nil)
(autoload 'cider-format-edn-last-sexp "cider-format" "\
Format the EDN data of the last sexp." t nil)
(register-definition-prefixes "cider-format" '("cider--format-"))
;;;***
;;;### (autoloads nil "cider-inspector" "cider-inspector.el" (0 0
;;;;;; 0 0))
;;; Generated autoloads from cider-inspector.el
(autoload 'cider-inspect-last-sexp "cider-inspector" "\
Inspect the result of the the expression preceding point." t nil)
(autoload 'cider-inspect-defun-at-point "cider-inspector" "\
Inspect the result of the \"top-level\" expression at point." t nil)
(autoload 'cider-inspect-last-result "cider-inspector" "\
Inspect the most recent eval result." t nil)
(autoload 'cider-inspect "cider-inspector" "\
Inspect the result of the preceding sexp.
With a prefix argument ARG it inspects the result of the \"top-level\" form.
With a second prefix argument it prompts for an expression to eval and inspect.
\(fn &optional ARG)" t nil)
(autoload 'cider-inspect-expr "cider-inspector" "\
Evaluate EXPR in NS and inspect its value.
Interactively, EXPR is read from the minibuffer, and NS the
current buffer's namespace.
\(fn EXPR NS)" t nil)
(register-definition-prefixes "cider-inspector" '("cider-"))
;;;***
;;;### (autoloads nil "cider-jar" "cider-jar.el" (0 0 0 0))
;;; Generated autoloads from cider-jar.el
(register-definition-prefixes "cider-jar" '("cider-jar-"))
;;;***
;;;### (autoloads nil "cider-macroexpansion" "cider-macroexpansion.el"
;;;;;; (0 0 0 0))
;;; Generated autoloads from cider-macroexpansion.el
(autoload 'cider-macroexpand-1 "cider-macroexpansion" "\
Invoke \\=`macroexpand-1\\=` on the expression preceding point.
If invoked with a PREFIX argument, use \\=`macroexpand\\=` instead of
\\=`macroexpand-1\\=`.
\(fn &optional PREFIX)" t nil)
(autoload 'cider-macroexpand-all "cider-macroexpansion" "\
Invoke \\=`macroexpand-all\\=` on the expression preceding point." t nil)
(register-definition-prefixes "cider-macroexpansion" '("cider-"))
;;;***
;;;### (autoloads nil "cider-mode" "cider-mode.el" (0 0 0 0))
;;; Generated autoloads from cider-mode.el
(defvar cider-mode-line '(:eval (format " cider[%s]" (cider--modeline-info))) "\
Mode line lighter for cider mode.
The value of this variable is a mode line template as in
`mode-line-format'. See Info Node `(elisp)Mode Line Format' for details
about mode line templates.
Customize this variable to change how cider mode displays its status in the
mode line. The default value displays the current connection. Set this
variable to nil to disable the mode line entirely.")
(custom-autoload 'cider-mode-line "cider-mode" t)
(with-eval-after-load 'clojure-mode (easy-menu-define cider-clojure-mode-menu-open clojure-mode-map "Menu for Clojure mode.\n This is displayed in `clojure-mode' buffers, if `cider-mode' is not active." `("CIDER" :visible (not cider-mode) ["Start a Clojure REPL" cider-jack-in-clj :help "Starts an nREPL server and connects a Clojure REPL to it."] ["Connect to a Clojure REPL" cider-connect-clj :help "Connects to a REPL that's already running."] ["Start a ClojureScript REPL" cider-jack-in-cljs :help "Starts an nREPL server and connects a ClojureScript REPL to it."] ["Connect to a ClojureScript REPL" cider-connect-cljs :help "Connects to a ClojureScript REPL that's already running."] ["Start a Clojure REPL, and a ClojureScript REPL" cider-jack-in-clj&cljs :help "Starts an nREPL server, connects a Clojure REPL to it, and then a ClojureScript REPL."] "--" ["View user manual" cider-view-manual])))
(autoload 'cider-mode "cider-mode" "\
Minor mode for REPL interaction from a Clojure buffer.
This is a minor mode. If called interactively, toggle the `Cider
mode' mode. If the prefix argument is positive, enable the mode,
and if it is zero or negative, disable the mode.
If called from Lisp, toggle the mode if ARG is `toggle'. Enable
the mode if ARG is nil, omitted, or is a positive number.
Disable the mode if ARG is a negative number.
To check whether the minor mode is enabled in the current buffer,
evaluate `cider-mode'.
The mode's hook is called both when the mode is enabled and when
it is disabled.
\\{cider-mode-map}
\(fn &optional ARG)" t nil)
(register-definition-prefixes "cider-mode" '("cider-"))
;;;***
;;;### (autoloads nil "cider-ns" "cider-ns.el" (0 0 0 0))
;;; Generated autoloads from cider-ns.el
(autoload 'cider-ns-reload "cider-ns" "\
Send a (require 'ns :reload) to the REPL.
With an argument PROMPT, it prompts for a namespace name. This is the
Clojure out of the box reloading experience and does not rely on
org.clojure/tools.namespace. See Commentary of this file for a longer list
of differences. From the Clojure doc: \":reload forces loading of all the
identified libs even if they are already loaded\".
\(fn &optional PROMPT)" t nil)
(autoload 'cider-ns-reload-all "cider-ns" "\
Send a (require 'ns :reload-all) to the REPL.
With an argument PROMPT, it prompts for a namespace name. This is the
Clojure out of the box reloading experience and does not rely on
org.clojure/tools.namespace. See Commentary of this file for a longer list
of differences. From the Clojure doc: \":reload-all implies :reload and
also forces loading of all libs that the identified libs directly or
indirectly load via require\".
\(fn &optional PROMPT)" t nil)
(autoload 'cider-ns-refresh "cider-ns" "\
Reload modified and unloaded namespaces on the classpath.
With a single prefix argument, or if MODE is `refresh-all', reload all
namespaces on the classpath unconditionally.
With a double prefix argument, or if MODE is `clear', clear the state of
the namespace tracker before reloading. This is useful for recovering from
some classes of error (for example, those caused by circular dependencies)
that a normal reload would not otherwise recover from. The trade-off of
clearing is that stale code from any deleted files may not be completely
unloaded.
With a negative prefix argument, or if MODE is `inhibit-fns', prevent any
refresh functions (defined in `cider-ns-refresh-before-fn' and
`cider-ns-refresh-after-fn') from being invoked.
\(fn &optional MODE)" t nil)
(register-definition-prefixes "cider-ns" '("cider-ns-"))
;;;***
;;;### (autoloads nil "cider-overlays" "cider-overlays.el" (0 0 0
;;;;;; 0))
;;; Generated autoloads from cider-overlays.el
(register-definition-prefixes "cider-overlays" '("cider-"))
;;;***
;;;### (autoloads nil "cider-popup" "cider-popup.el" (0 0 0 0))
;;; Generated autoloads from cider-popup.el
(register-definition-prefixes "cider-popup" '("cider-"))
;;;***
;;;### (autoloads nil "cider-profile" "cider-profile.el" (0 0 0 0))
;;; Generated autoloads from cider-profile.el
(autoload 'cider-profile-samples "cider-profile" "\
Displays current max-sample-count.
If optional QUERY is specified, set max-sample-count and display new value.
\(fn &optional QUERY)" t nil)
(autoload 'cider-profile-var-profiled-p "cider-profile" "\
Displays the profiling status of var under point.
Prompts for var if none under point or QUERY is present.
\(fn QUERY)" t nil)
(autoload 'cider-profile-ns-toggle "cider-profile" "\
Toggle profiling for the ns associated with optional QUERY.
If optional argument QUERY is non-nil, prompt for ns. Otherwise use
current ns.
\(fn &optional QUERY)" t nil)
(autoload 'cider-profile-toggle "cider-profile" "\
Toggle profiling for the given QUERY.
Defaults to the symbol at point.
With prefix arg or no symbol at point, prompts for a var.
\(fn QUERY)" t nil)
(autoload 'cider-profile-summary "cider-profile" "\
Display a summary of currently collected profile data." t nil)
(autoload 'cider-profile-var-summary "cider-profile" "\
Display profile data for var under point QUERY.
Defaults to the symbol at point. With prefix arg or no symbol at point,
prompts for a var.
\(fn QUERY)" t nil)
(autoload 'cider-profile-clear "cider-profile" "\
Clear any collected profile data." t nil)
(register-definition-prefixes "cider-profile" '("cider-profile-"))
;;;***
;;;### (autoloads nil "cider-repl" "cider-repl.el" (0 0 0 0))
;;; Generated autoloads from cider-repl.el
(register-definition-prefixes "cider-repl" '("cider-"))
;;;***
;;;### (autoloads nil "cider-repl-history" "cider-repl-history.el"
;;;;;; (0 0 0 0))
;;; Generated autoloads from cider-repl-history.el
(autoload 'cider-repl-history "cider-repl-history" "\
Display items in the CIDER command history in another buffer." t nil)
(register-definition-prefixes "cider-repl-history" '("cider-repl-history-"))
;;;***
;;;### (autoloads nil "cider-resolve" "cider-resolve.el" (0 0 0 0))
;;; Generated autoloads from cider-resolve.el
(register-definition-prefixes "cider-resolve" '("cider-resolve-"))
;;;***
;;;### (autoloads nil "cider-scratch" "cider-scratch.el" (0 0 0 0))
;;; Generated autoloads from cider-scratch.el
(autoload 'cider-scratch "cider-scratch" "\
Go to the scratch buffer named `cider-scratch-buffer-name'." t nil)
(register-definition-prefixes "cider-scratch" '("cider-"))
;;;***
;;;### (autoloads nil "cider-selector" "cider-selector.el" (0 0 0
;;;;;; 0))
;;; Generated autoloads from cider-selector.el
(autoload 'cider-selector "cider-selector" "\
Select a new buffer by type, indicated by a single character.
The user is prompted for a single character indicating the method by
which to choose a new buffer. The `?' character describes the
available methods. OTHER-WINDOW provides an optional target.
See `def-cider-selector-method' for defining new methods.
\(fn &optional OTHER-WINDOW)" t nil)
(register-definition-prefixes "cider-selector" '("??" "?c" "?d" "?e" "?m" "?p" "?q" "?r" "?s" "?x" "cider-selector-" "def-cider-selector-method"))
;;;***
;;;### (autoloads nil "cider-stacktrace" "cider-stacktrace.el" (0
;;;;;; 0 0 0))
;;; Generated autoloads from cider-stacktrace.el
(register-definition-prefixes "cider-stacktrace" '("cider-"))
;;;***
;;;### (autoloads nil "cider-test" "cider-test.el" (0 0 0 0))
;;; Generated autoloads from cider-test.el
(defvar cider-auto-test-mode nil "\
Non-nil if Cider-Auto-Test mode is enabled.
See the `cider-auto-test-mode' command
for a description of this minor mode.
Setting this variable directly does not take effect;
either customize it (see the info node `Easy Customization')
or call the function `cider-auto-test-mode'.")
(custom-autoload 'cider-auto-test-mode "cider-test" nil)
(autoload 'cider-auto-test-mode "cider-test" "\
Toggle automatic testing of Clojure files.
This is a minor mode. If called interactively, toggle the
`Cider-Auto-Test mode' mode. If the prefix argument is positive,
enable the mode, and if it is zero or negative, disable the mode.
If called from Lisp, toggle the mode if ARG is `toggle'. Enable
the mode if ARG is nil, omitted, or is a positive number.
Disable the mode if ARG is a negative number.
To check whether the minor mode is enabled in the current buffer,
evaluate `(default-value \\='cider-auto-test-mode)'.
The mode's hook is called both when the mode is enabled and when
it is disabled.
When enabled this reruns tests every time a Clojure file is loaded.
Only runs tests corresponding to the loaded file's namespace and does
nothing if no tests are defined or if the file failed to load.
\(fn &optional ARG)" t nil)
(register-definition-prefixes "cider-test" '("cider-"))
;;;***
;;;### (autoloads nil "cider-tracing" "cider-tracing.el" (0 0 0 0))
;;; Generated autoloads from cider-tracing.el
(autoload 'cider-toggle-trace-var "cider-tracing" "\
Toggle var tracing.
Prompts for the symbol to use, or uses the symbol at point, depending on
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
opposite of what that option dictates.
\(fn ARG)" t nil)
(autoload 'cider-toggle-trace-ns "cider-tracing" "\
Toggle ns tracing.
Defaults to the current ns. With prefix arg QUERY, prompts for a ns.
\(fn QUERY)" t nil)
(register-definition-prefixes "cider-tracing" '("cider-"))
;;;***
;;;### (autoloads nil "cider-util" "cider-util.el" (0 0 0 0))
;;; Generated autoloads from cider-util.el
(autoload 'cider-view-manual "cider-util" "\
View the manual in your default browser." t nil)
(register-definition-prefixes "cider-util" '("cider-"))
;;;***
;;;### (autoloads nil "cider-xref" "cider-xref.el" (0 0 0 0))
;;; Generated autoloads from cider-xref.el
(autoload 'cider-xref-fn-refs "cider-xref" "\
Show all functions that reference the var matching NS and SYMBOL.
\(fn &optional NS SYMBOL)" t nil)
(autoload 'cider-xref-fn-deps "cider-xref" "\
Show all functions referenced by the var matching NS and SYMBOL.
\(fn &optional NS SYMBOL)" t nil)
(autoload 'cider-xref-fn-refs-select "cider-xref" "\
Displays the references for NS and SYMBOL using completing read.
\(fn &optional NS SYMBOL)" t nil)
(autoload 'cider-xref-fn-deps-select "cider-xref" "\
Displays the function dependencies for NS and SYMBOL using completing read.
\(fn &optional NS SYMBOL)" t nil)
(register-definition-prefixes "cider-xref" '("cider-"))
;;;***
;;;### (autoloads nil "nrepl-client" "nrepl-client.el" (0 0 0 0))
;;; Generated autoloads from nrepl-client.el
(register-definition-prefixes "nrepl-client" '("cider-enlighten-mode" "emacs-bug-46284/when-27.1-windows-nt" "nrepl-"))
;;;***
;;;### (autoloads nil "nrepl-dict" "nrepl-dict.el" (0 0 0 0))
;;; Generated autoloads from nrepl-dict.el
(register-definition-prefixes "nrepl-dict" '("nrepl-"))
;;;***
;;;### (autoloads nil nil ("cider-pkg.el") (0 0 0 0))
;;;***
;; Local Variables:
;; version-control: never
;; no-byte-compile: t
;; no-update-autoloads: t
;; coding: utf-8
;; End:
;;; cider-autoloads.el ends here
;;; cider-browse-ns.el --- CIDER namespace browser -*- lexical-binding: t; -*-
;; Copyright © 2014-2022 John Andrews, Bozhidar Batsov and CIDER contributors
;; Author: John Andrews <john.m.andrews@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; M-x cider-browse-ns
;;
;; Display a list of all vars in a namespace.
;; Pressing <enter> will take you to the cider-doc buffer for that var.
;; Pressing ^ will take you to a list of all namespaces (akin to `dired-mode').
;; M-x cider-browse-ns-all
;;
;; Explore Clojure namespaces by browsing a list of all namespaces.
;; Pressing <enter> expands into a list of that namespace's vars as if by
;; executing the command (cider-browse-ns "my.ns").
;;; Code:
(require 'cider-client)
(require 'cider-popup)
(require 'cider-util)
(require 'nrepl-dict)
(require 'subr-x)
(require 'easymenu)
(require 'button)
(require 'cl-lib)
(require 'thingatpt)
(defgroup cider-browse-ns nil
"Display contents of namespaces for CIDER."
:prefix "cider-browse-ns-"
:group 'cider)
(defface cider-browse-ns-extra-info-face
'((t (:inherit shadow)))
"Face for displaying extra info of namespace vars."
:package-version '(cider . "1.4.0"))
(defcustom cider-browse-ns-default-filters nil
"List of default hide filters to apply to browse-ns buffer.
Available options include `private', `test', `macro', `function', and
`var'."
:type 'list
:package-version '(cider . "1.4.0"))
(defconst cider-browse-ns-buffer "*cider-ns-browser*")
(defvar-local cider-browse-ns-current-ns nil)
(defvar-local cider-browse-ns-filters nil)
(defvar-local cider-browse-ns-show-all nil)
(defvar-local cider-browse-ns-group-by nil)
(defvar-local cider-browse-ns-items nil)
(defvar-local cider-browse-ns-title nil)
(defvar-local cider-browse-ns-group-by nil)
;; Mode Definition
(defvar cider-browse-ns-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map cider-popup-buffer-mode-map)
(define-key map "d" #'cider-browse-ns-doc-at-point)
(define-key map "s" #'cider-browse-ns-find-at-point)
(define-key map (kbd "RET") #'cider-browse-ns-operate-at-point)
(define-key map "^" #'cider-browse-ns-all)
(define-key map "n" #'next-line)
(define-key map "p" #'previous-line)
(define-key map "a" #'cider-browse-ns-toggle-all)
(define-key map (kbd "h p") #'cider-browse-ns-toggle-hide-private)
(define-key map (kbd "h t") #'cider-browse-ns-toggle-hide-test)
(define-key map (kbd "h m") #'cider-browse-ns-toggle-hide-macro)
(define-key map (kbd "h f") #'cider-browse-ns-toggle-hide-function)
(define-key map (kbd "h v") #'cider-browse-ns-toggle-hide-var)
(define-key map (kbd "g t") #'cider-browse-ns-group-by-type)
(define-key map (kbd "g v") #'cider-browse-ns-group-by-visibility)
(easy-menu-define cider-browse-ns-mode-menu map
"Menu for CIDER's namespace browser"
'("Namespace Browser"
["Show doc" cider-browse-ns-doc-at-point]
["Go to definition" cider-browse-ns-find-at-point]
"--"
["Browse all namespaces" cider-browse-ns-all]))
map))
(defvar cider-browse-ns-mouse-map
(let ((map (make-sparse-keymap)))
(define-key map [mouse-1] #'cider-browse-ns-handle-mouse)
map))
(define-derived-mode cider-browse-ns-mode special-mode "browse-ns"
"Major mode for browsing Clojure namespaces.
\\{cider-browse-ns-mode-map}"
(setq-local electric-indent-chars nil)
(setq-local sesman-system 'CIDER)
(when cider-special-mode-truncate-lines
(setq-local truncate-lines t))
(setq-local cider-browse-ns-current-ns nil))
(defun cider-browse-ns--text-face (var-meta)
"Return font-lock-face for a var.
VAR-META contains the metadata information used to decide a face.
Presence of \"arglists\" and \"macro\" indicates a macro form.
Only \"arglists\" indicates a function. Otherwise, its a variable.
If the NAMESPACE is not loaded in the REPL, assume TEXT is a fn."
(cond
((not var-meta) 'font-lock-function-name-face)
((and (nrepl-dict-contains var-meta "arglists")
(string= (nrepl-dict-get var-meta "macro") "true"))
'font-lock-keyword-face)
((nrepl-dict-contains var-meta "arglists") 'font-lock-function-name-face)
(t 'font-lock-variable-name-face)))
(defun cider-browse-ns--properties (var var-meta)
"Decorate VAR with a clickable keymap and a face.
VAR-META is used to decide a font-lock face."
(let ((face (cider-browse-ns--text-face var-meta)))
(propertize var
'font-lock-face face
'mouse-face 'highlight
'keymap cider-browse-ns-mouse-map)))
(defun cider-browse-ns--ns-list (buffer title nss)
"List the namespaces NSS in BUFFER.
Buffer is rendered with TITLE at the top and lists ITEMS filtered according
to user settings."
(let ((dict (nrepl-dict)))
(dolist (ns nss)
(nrepl-dict-put dict ns (nrepl-dict "ns" "true")))
(cider-browse-ns--list buffer title dict nil)))
(defun cider-browse-ns--list (buffer title items ns)
"Initialize rendering of browse-ns BUFFER.
Initialize the buffer's TITLE, namespace NS, and the nrepl-dict ITEMS to be
displayed."
(with-current-buffer buffer
(cider-browse-ns-mode)
(setq-local cider-browse-ns-items items)
(setq-local cider-browse-ns-title title)
(setq-local cider-browse-ns-filters cider-browse-ns-default-filters)
(setq-local cider-browse-ns-current-ns ns))
(cider-browse-ns--render-buffer))
(defun cider-browse-ns--meta-macro-p (var-meta)
"Return non-nil if VAR-META is the metadata of a macro."
(and (nrepl-dict-contains var-meta "arglists")
(string= (nrepl-dict-get var-meta "macro") "true")))
(defun cider-browse-ns--meta-test-p (var-meta)
"Return non-nil if VAR-META is the metadata of a test."
(nrepl-dict-contains var-meta "test"))
(defun cider-browse-ns--meta-function-p (var-meta)
"Return non-nil if VAR-META is the metadata of a function."
(and (nrepl-dict-contains var-meta "arglists")
(not (cider-browse-ns--meta-macro-p var-meta))))
(defun cider-browse-ns--meta-private-p (var-meta)
"Return non-nil if VAR-META indicates a private element."
(string= (nrepl-dict-get var-meta "private") "true"))
(defun cider-browse-ns--meta-var-p (var-meta)
"Return non-nil if VAR-META indicates a var."
(not (or (cider-browse-ns--meta-test-p var-meta)
(cider-browse-ns--meta-macro-p var-meta)
(cider-browse-ns--meta-function-p var-meta))))
(defun cider-browse-ns--item-filter (_ var-meta)
"Return non-nil if item containing VAR-META should be listed in buffer."
(let ((function-filter-p (memq 'function cider-browse-ns-filters))
(var-filter-p (memq 'var cider-browse-ns-filters))
(private-filter-p (memq 'private cider-browse-ns-filters))
(test-filter-p (memq 'test cider-browse-ns-filters))
(macro-filter-p (memq 'macro cider-browse-ns-filters)))
;; check if item should be displayed
(let* ((macro-p (cider-browse-ns--meta-macro-p var-meta))
(function-p (cider-browse-ns--meta-function-p var-meta))
(private-p (cider-browse-ns--meta-private-p var-meta))
(test-p (cider-browse-ns--meta-test-p var-meta))
(var-p (cider-browse-ns--meta-var-p var-meta)))
(or cider-browse-ns-show-all
(not (or (and macro-p macro-filter-p)
(and function-p function-filter-p)
(and test-p test-filter-p)
(and var-p var-filter-p)
(and private-p private-filter-p)))))))
(defun cider-browse-ns--propertized-item (key items)
"Return propertized line of item KEY in nrepl-dict ITEMS."
(let* ((var-meta (nrepl-dict-get items key))
(face (cider-browse-ns--text-face (nrepl-dict-get items key)))
(private-p (string= (nrepl-dict-get var-meta "private") "true"))
(test-p (nrepl-dict-contains var-meta "test"))
(ns-p (nrepl-dict-contains var-meta "ns")))
(concat
(propertize key
'font-lock-face face
'ns ns-p)
" "
(cond
(test-p (propertize "(test) " 'face 'cider-browse-ns-extra-info-face))
(private-p (propertize "(-) " 'face 'cider-browse-ns-extra-info-face))
(t "")))))
(defun cider-browse-ns--display-list (keys items max-length &optional label)
"Render the items of KEYS as condained in the nrepl-dict ITEMS.
Pad the row to be MAX-LENGTH+1. If LABEL is non-nil, add a header to the
list of items."
(when keys
(when label
(insert " " label ":\n"))
(dolist (key keys)
(let* ((doc (nrepl-dict-get-in items (list key "doc")))
(doc (when doc (read doc)))
(first-doc-line (cider-browse-ns--first-doc-line doc))
(item-line (cider-browse-ns--propertized-item key items)))
(insert " ")
(insert item-line)
(when cider-browse-ns-current-ns
(insert (make-string (+ (- max-length (string-width item-line)) 1) ?·))
(insert " " (propertize first-doc-line 'font-lock-face 'font-lock-doc-face)))
(insert "\n")))
(insert "\n")))
(defun cider-browse-ns--column-width (items)
"Determine the display width of displayed ITEMS."
(let* ((propertized-lines
(seq-map (lambda (key)
(cider-browse-ns--propertized-item key items))
(nrepl-dict-keys items))))
(if propertized-lines
(apply #'max (seq-map (lambda (entry) (string-width entry))
propertized-lines))
0)))
(defun cider-browse-ns--render-items (items)
"Render the nrepl-dict ITEMS to the browse-ns buffer."
(let* ((max-length (cider-browse-ns--column-width items)))
(cl-labels
((keys-from-pred
(pred items)
(nrepl-dict-keys (nrepl-dict-filter (lambda (_ var-meta)
(funcall pred var-meta))
items))))
(cond
((eql cider-browse-ns-group-by 'type)
(let* ((func-keys (keys-from-pred #'cider-browse-ns--meta-function-p items))
(macro-keys (keys-from-pred #'cider-browse-ns--meta-macro-p items))
(var-keys (keys-from-pred #'cider-browse-ns--meta-var-p items))
(test-keys (keys-from-pred #'cider-browse-ns--meta-test-p items)))
(cider-browse-ns--display-list func-keys items max-length "Functions")
(cider-browse-ns--display-list macro-keys items max-length "Macros")
(cider-browse-ns--display-list var-keys items max-length "Vars")
(cider-browse-ns--display-list test-keys items max-length "Tests")))
((eql cider-browse-ns-group-by 'visibility)
(let* ((public-keys
(keys-from-pred
(lambda (var-meta)
(not (cider-browse-ns--meta-private-p var-meta)))
items))
(private-keys (keys-from-pred #'cider-browse-ns--meta-private-p items)))
(cider-browse-ns--display-list public-keys items max-length "Public")
(cider-browse-ns--display-list private-keys items max-length "Private")))
(t
(cider-browse-ns--display-list
(nrepl-dict-keys items) items max-length))))))
(defun cider-browse-ns--filter (flag)
"Toggle the filter indicated by FLAG and re-render the buffer."
(setq cider-browse-ns-filters
(if (memq flag cider-browse-ns-filters)
(remq flag cider-browse-ns-filters)
(cons flag cider-browse-ns-filters)))
(cider-browse-ns--render-buffer))
(defun cider-browse-ns--button-filter (button)
"Handle filter action for BUTTON."
(let ((flag (button-get button 'filter)))
(cider-browse-ns--filter flag)))
(defun cider-browse-ns--group (flag)
"Set the group-by option to FLAG and re-renderthe buffer."
(setq cider-browse-ns-group-by
(if (eql flag cider-browse-ns-group-by) nil flag))
(cider-browse-ns--render-buffer))
(defun cider-browse-ns--button-group (button)
"Handle grouping action for BUTTON."
(let ((flag (button-get button 'group-by)))
(cider-browse-ns--group flag)))
(defun cider-browse-ns--toggle-all (_button)
"Toggle the display-all visibility setting."
(setq cider-browse-ns-show-all (not cider-browse-ns-show-all))
(cider-browse-ns--render-buffer))
(defun cider-browse-ns--render-header (&optional filtered-items-ct)
"Render the section at the top of the buffer displaying visibility controls.
If FILTERED-ITEMS-CT is non-nil, then display a message of how many items
are being filtered."
;; Display Show line
(insert " Show: ")
(insert-text-button "All"
'follow-link t
'action #'cider-browse-ns--toggle-all
;; 'help-echo (cider-stacktrace-tooltip)
'face (if cider-browse-ns-show-all
'cider-stacktrace-filter-active-face
nil))
(insert "\n")
;; Display Filters
(let ((filters '(("Private" private)
("Test" test)
("Macro" macro)
("Function" function)
("Var" var))))
(insert " Hide: ")
(dolist (filter filters)
(seq-let (title key) filter
(let ((is-active (memq key cider-browse-ns-filters)))
(insert-text-button title
'filter key
'follow-link t
'action #'cider-browse-ns--button-filter
;; 'help-echo (cider-stacktrace-tooltip)
'face (if (and is-active (not cider-browse-ns-show-all))
'cider-stacktrace-filter-active-face
nil))
(insert " "))))
(when filtered-items-ct
(insert (format "(%d items filtered)" filtered-items-ct))))
(insert "\n")
;; Groupings
(insert " Group-by: ")
(let ((groupings '(("Type" type)
("Visibility" visibility))))
(dolist (grouping groupings)
(seq-let (title key) grouping
(let ((is-active (eql key cider-browse-ns-group-by)))
(insert-text-button title
'group-by key
'follow-link t
'action #'cider-browse-ns--button-group
;; 'help-echo ()
'face (if is-active
'cider-stacktrace-filter-active-face
nil)))
(insert " "))))
(insert "\n\n"))
(defun cider-browse-ns--render-buffer (&optional buffer)
"Render the sections of the browse-ns buffer.
Render occurs in BUFFER if non-nil. This function is the main entrypoint
for redisplaying the buffer when filters change."
(with-current-buffer (or buffer (current-buffer))
(let* ((inhibit-read-only t)
(point (point))
(filtered-items (nrepl-dict-filter #'cider-browse-ns--item-filter
cider-browse-ns-items))
(filtered-item-ct (- (length (nrepl-dict-keys cider-browse-ns-items))
(length (nrepl-dict-keys filtered-items)))))
(erase-buffer)
(insert (propertize (cider-propertize cider-browse-ns-title 'ns) 'ns t) "\n")
(when cider-browse-ns-current-ns
(cider-browse-ns--render-header filtered-item-ct))
(cider-browse-ns--render-items filtered-items)
(goto-char point))))
(defun cider-browse-ns--first-doc-line (doc)
"Return the first line of the given DOC string.
If the first line of the DOC string contains multiple sentences, only
the first sentence is returned. If the DOC string is nil, a Not documented
string is returned."
(if doc
(let* ((split-newline (split-string doc "\n"))
(first-line (car split-newline)))
(cond
((string-match "\\. " first-line) (substring first-line 0 (match-end 0)))
((= 1 (length split-newline)) first-line)
(t (concat first-line "..."))))
"Not documented."))
(defun cider-browse-ns--combined-vars-with-meta (namespace)
"Return the combined public and private vars in NAMESPACE.
Private vars have the additional metadata \"private\": \"true\" in their
var-meta map."
(let ((items (cider-sync-request:ns-vars-with-meta namespace))
(private-items (cider-sync-request:private-ns-vars-with-meta namespace)))
(when private-items
(dolist (key (nrepl-dict-keys private-items))
(let ((var-meta (nrepl-dict-put (nrepl-dict-get private-items key)
"private" "true")))
(setq items (nrepl-dict-put items key var-meta)))))
items))
;; Interactive Functions
;;;###autoload
(defun cider-browse-ns (namespace)
"List all NAMESPACE's vars in BUFFER."
(interactive (list (completing-read "Browse namespace: " (cider-sync-request:ns-list))))
(with-current-buffer (cider-popup-buffer cider-browse-ns-buffer 'select nil 'ancillary)
(cider-browse-ns--list (current-buffer)
namespace
(cider-browse-ns--combined-vars-with-meta namespace)
namespace)))
;;;###autoload
(defun cider-browse-ns-all ()
"List all loaded namespaces in BUFFER."
(interactive)
(with-current-buffer (cider-popup-buffer cider-browse-ns-buffer 'select nil 'ancillary)
(let ((names (cider-sync-request:ns-list)))
(cider-browse-ns--ns-list
(current-buffer)
"All loaded namespaces"
(mapcar (lambda (name)
(cider-browse-ns--properties name nil))
names)))))
(defun cider-browse-ns--thing-at-point ()
"Get the thing at point.
Return a list of the type ('ns or 'var) and the value."
(let ((ns-p (get-text-property (point) 'ns))
(line (car (split-string (string-trim (thing-at-point 'line)) " "))))
(if (or ns-p (string-match "\\." line))
`(ns ,line)
`(var ,(format "%s/%s"
(or (get-text-property (point) 'cider-browse-ns-current-ns)
cider-browse-ns-current-ns)
line)))))
(defun cider-browse-ns-toggle-all ()
"Toggle showing all of the items in the browse-ns buffer."
(interactive)
(cider-browse-ns--toggle-all nil))
(defun cider-browse-ns-toggle-hide-private ()
"Toggle visibility of private items displayed in browse-ns buffer."
(interactive)
(cider-browse-ns--filter 'private))
(defun cider-browse-ns-toggle-hide-test ()
"Toggle visibility of test items displayed in browse-ns buffer."
(interactive)
(cider-browse-ns--filter 'test))
(defun cider-browse-ns-toggle-hide-macro ()
"Toggle visibility of macro items displayed in browse-ns buffer."
(interactive)
(cider-browse-ns--filter 'macro))
(defun cider-browse-ns-toggle-hide-function ()
"Toggle visibility of function items displayed in browse-ns buffer."
(interactive)
(cider-browse-ns--filter 'function))
(defun cider-browse-ns-toggle-hide-var ()
"Toggle visibility of var items displayed in browse-ns buffer."
(interactive)
(cider-browse-ns--filter 'var))
(defun cider-browse-ns-group-by-type ()
"Toggle visibility of var items displayed in browse-ns buffer."
(interactive)
(cider-browse-ns--group 'type))
(defun cider-browse-ns-group-by-visibility ()
"Toggle visibility of var items displayed in browse-ns buffer."
(interactive)
(cider-browse-ns--group 'visibility))
(declare-function cider-doc-lookup "cider-doc")
(defun cider-browse-ns-doc-at-point ()
"Show the documentation for the thing at current point."
(interactive)
(let* ((thing (cider-browse-ns--thing-at-point))
(value (cadr thing)))
;; value is either some ns or a var
(cider-doc-lookup value)))
(defun cider-browse-ns-operate-at-point ()
"Expand browser according to thing at current point.
If the thing at point is a ns it will be browsed,
and if the thing at point is some var - its documentation will
be displayed."
(interactive)
(let* ((thing (cider-browse-ns--thing-at-point))
(type (car thing))
(value (cadr thing)))
(if (eq type 'ns)
(cider-browse-ns value)
(cider-doc-lookup value))))
(declare-function cider-find-ns "cider-find")
(declare-function cider-find-var "cider-find")
(defun cider-browse-ns-find-at-point ()
"Find the definition of the thing at point."
(interactive)
(let* ((thing (cider-browse-ns--thing-at-point))
(type (car thing))
(value (cadr thing)))
(if (eq type 'ns)
(cider-find-ns nil value)
(cider-find-var current-prefix-arg value))))
(defun cider-browse-ns-handle-mouse (_event)
"Handle mouse click EVENT."
(interactive "e")
(cider-browse-ns-operate-at-point))
(provide 'cider-browse-ns)
;;; cider-browse-ns.el ends here
;;; cider-browse-spec.el --- CIDER spec browser -*- lexical-binding: t; -*-
;; Copyright © 2017-2022 Juan Monetta, Bozhidar Batsov and CIDER contributors
;; Author: Juan Monetta <jpmonettas@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; M-x cider-browse-spec
;;
;; Display a spec description you can browse.
;; Pressing <enter> over a sub spec will take you to the description of that sub spec.
;; Pressing ^ takes you to the list of all specs.
;; M-x cider-browse-spec-all
;;
;; Explore clojure.spec registry by browsing a list of all specs.
;; Pressing <enter> over a spec display the spec description you can browse.
;;; Code:
(require 'cider-client)
(require 'cider-popup)
(require 'cider-util)
(require 'cl-lib)
(require 'nrepl-dict)
(require 'seq)
(require 'subr-x)
(require 'help-mode)
;; The buffer names used by the spec browser
(defconst cider-browse-spec-buffer "*cider-spec-browser*")
(defconst cider-browse-spec-example-buffer "*cider-spec-example*")
;; Mode Definition
(defvar cider-browse-spec-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map (make-composed-keymap button-buffer-map
cider-popup-buffer-mode-map))
(define-key map (kbd "RET") #'cider-browse-spec--browse-at)
(define-key map "n" #'forward-button)
(define-key map "p" #'backward-button)
map)
"Keymap for `cider-browse-spec-mode'.")
(define-derived-mode cider-browse-spec-mode special-mode "Specs"
"Major mode for browsing Clojure specs.
\\{cider-browse-spec-mode-map}"
(setq-local electric-indent-chars nil)
(setq-local sesman-system 'CIDER)
(when cider-special-mode-truncate-lines
(setq-local truncate-lines t)))
(defvar cider-browse-spec--current-spec nil)
(defvar cider-browse-spec-view-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map help-mode-map)
(define-key map (kbd "RET") #'cider-browse-spec--browse-at)
(define-key map "^" #'cider-browse-spec-all)
(define-key map "e" #'cider-browse-spec--print-curr-spec-example)
(define-key map "n" #'forward-button)
(define-key map "p" #'backward-button)
map)
"Keymap for `cider-browse-spec-view-mode'.")
(define-derived-mode cider-browse-spec-view-mode help-mode "Spec"
"Major mode for displaying CIDER spec.
\\{cider-browse-spec-view-mode-map}"
(setq-local cider-browse-spec--current-spec nil)
(setq-local electric-indent-chars nil)
(setq-local sesman-system 'CIDER)
(when cider-special-mode-truncate-lines
(setq-local truncate-lines t)))
(defvar cider-browse-spec-example-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map cider-popup-buffer-mode-map)
(define-key map "^" #'cider-browse-spec-all)
(define-key map "e" #'cider-browse-spec--print-curr-spec-example)
(define-key map "g" #'revert-buffer)
map)
"Keymap for `cider-browse-spec-example-mode'.")
(define-derived-mode cider-browse-spec-example-mode special-mode "Example"
"Major mode for Clojure spec examples.
\\{cider-browse-spec-example-mode-map}"
(setq-local electric-indent-chars nil)
(setq-local revert-buffer-function #'cider-browse-spec--example-revert-buffer-function)
(setq-local sesman-system 'CIDER)
(when cider-special-mode-truncate-lines
(setq-local truncate-lines t)))
;; Non interactive functions
(define-button-type 'cider-browse-spec--spec
'action #'cider-browse-spec--browse-at
'face nil
'follow-link t
'help-echo "View spec")
(defun cider-browse-spec--draw-list-buffer (buffer title specs)
"Reset contents of BUFFER.
Display TITLE at the top and SPECS are indented underneath."
(with-current-buffer buffer
(cider-browse-spec-mode)
(let ((inhibit-read-only t))
(erase-buffer)
(goto-char (point-max))
(insert (cider-propertize title 'emph) "\n")
(dolist (spec-name specs)
(insert (propertize " " 'spec-name spec-name))
(thread-first
(cider-font-lock-as-clojure spec-name)
(insert-text-button 'type 'cider-browse-spec--spec)
(button-put 'spec-name spec-name))
(insert (propertize "\n" 'spec-name spec-name)))
(goto-char (point-min)))))
(defun cider--qualified-keyword-p (str)
"Return non nil if STR is a namespaced keyword."
(string-match-p "^:.+/.+$" str))
(defun cider--spec-fn-p (value fn-name)
"Return non nil if VALUE is clojure.spec.[alpha]/FN-NAME."
(string-match-p (concat "^\\(clojure.spec\\|clojure.spec.alpha\\|clojure.alpha.spec\\)/" fn-name "$") value))
(defun cider-browse-spec--render-schema-map (spec-form)
"Render the s/schema map declaration SPEC-FORM."
(let ((name-spec-pairs (seq-partition (cdaadr spec-form) 2)))
(format "(s/schema\n {%s})"
(string-join
(thread-last
(seq-sort-by #'car #'string< name-spec-pairs)
(mapcar (lambda (s) (concat (cl-first s) " " (cider-browse-spec--pprint (cl-second s))))))
"\n "))))
(defun cider-browse-spec--render-schema-vector (spec-form)
"Render the s/schema vector declaration SPEC-FORM."
(format "(s/schema\n [%s])"
(string-join
(thread-last
(cl-second spec-form)
(mapcar (lambda (s) (cider-browse-spec--pprint s))))
"\n ")))
(defun cider-browse-spec--render-schema (spec-form)
"Render the s/schema SPEC-FORM."
(let ((schema-args (cl-second spec-form)))
(if (and (listp schema-args)
(nrepl-dict-p (cl-first schema-args)))
(cider-browse-spec--render-schema-map spec-form)
(cider-browse-spec--render-schema-vector spec-form))))
(defun cider-browse-spec--render-select (spec-form)
"Render the s/select SPEC-FORM."
(let ((keyset (cl-second spec-form))
(selection (cl-third spec-form)))
(format "(s/select\n %s\n [%s])"
(cider-browse-spec--pprint keyset)
(string-join
(thread-last
selection
(mapcar (lambda (s) (cider-browse-spec--pprint s))))
"\n "))))
(defun cider-browse-spec--render-union (spec-form)
"Render the s/union SPEC-FORM."
(let ((keyset (cl-second spec-form))
(selection (cl-third spec-form)))
(format "(s/union\n %s\n [%s])"
(cider-browse-spec--pprint keyset)
(string-join
(thread-last
selection
(mapcar (lambda (s) (cider-browse-spec--pprint s))))
"\n "))))
(defun cider-browse-spec--render-vector (spec-form)
"Render SPEC-FORM as a vector."
(format "[%s]" (string-join (mapcar #'cider-browse-spec--pprint spec-form))))
(defun cider-browse-spec--render-map-entry (spec-form)
"Render SPEC-FORM as a map entry."
(let ((key (cl-first spec-form))
(value (cl-second spec-form)))
(format "%s %s" (cider-browse-spec--pprint key)
(if (listp value)
(cider-browse-spec--render-vector value)
(cider-browse-spec--pprint value)))))
(defun cider-browse-spec--render-map (spec-form)
"Render SPEC-FORM as a map."
(let ((map-entries (cl-rest spec-form)))
(format "{%s}" (thread-last
(seq-partition map-entries 2)
(seq-map #'cider-browse-spec--render-map-entry)
(string-join)))))
(defun cider-browse-spec--pprint (form)
"Given a spec FORM builds a multi line string with a pretty render of that FORM."
(cond ((stringp form)
(if (cider--qualified-keyword-p form)
(with-temp-buffer
(thread-first
form
(insert-text-button 'type 'cider-browse-spec--spec)
(button-put 'spec-name form))
(buffer-string))
;; to make it easier to read replace all clojure.spec ns with s/
;; and remove all clojure.core ns
(thread-last
form
(replace-regexp-in-string "^\\(clojure.spec\\|clojure.spec.alpha\\|clojure.alpha.spec\\)/" "s/")
(replace-regexp-in-string "^\\(clojure.core\\)/" ""))))
((and (listp form) (stringp (cl-first form)))
(let ((form-tag (cl-first form)))
(cond
;; prettier fns #()
((string-equal form-tag "clojure.core/fn")
(if (equal (cl-second form) '("%"))
(format "#%s" (cl-reduce #'concat (mapcar #'cider-browse-spec--pprint (cl-rest (cl-rest form)))))
(format "(fn [%%] %s)" (cl-reduce #'concat (mapcar #'cider-browse-spec--pprint (cl-rest (cl-rest form)))))))
;; prettier (s/and )
((cider--spec-fn-p form-tag "and")
(format "(s/and\n%s)" (string-join (thread-last
(cl-rest form)
(mapcar #'cider-browse-spec--pprint)
(mapcar (lambda (x) (format "%s" x))))
"\n")))
;; prettier (s/or )
((cider--spec-fn-p form-tag "or")
(let ((name-spec-pair (seq-partition (cl-rest form) 2)))
(format "(s/or\n%s)" (string-join
(thread-last
name-spec-pair
(mapcar (lambda (s) (format "%s %s" (cl-first s) (cider-browse-spec--pprint (cl-second s))))))
"\n"))))
;; prettier (s/merge )
((cider--spec-fn-p form-tag "merge")
(format "(s/merge\n%s)" (string-join (thread-last
(cl-rest form)
(mapcar #'cider-browse-spec--pprint)
(mapcar (lambda (x) (format "%s" x))))
"\n")))
;; prettier (s/keys )
((cider--spec-fn-p form-tag "keys")
(let ((keys-args (seq-partition (cl-rest form) 2)))
(format "(s/keys%s)" (thread-last
keys-args
(mapcar (lambda (s)
(let ((key-type (cl-first s))
(specs-vec (cl-second s)))
(concat "\n" key-type
" ["
(string-join (thread-last
specs-vec
(mapcar #'cider-browse-spec--pprint)
(mapcar (lambda (x) (format "%s" x))))
"\n")
"]"))))
(cl-reduce #'concat)))))
;; prettier (s/multi-spec)
((cider--spec-fn-p form-tag "multi-spec")
(let ((multi-method (cl-second form))
(retag (cl-third form))
(sub-specs (cl-rest (cl-rest (cl-rest form)))))
(format "(s/multi-spec %s %s\n%s)"
multi-method
retag
(string-join
(thread-last
sub-specs
(mapcar (lambda (s)
(concat "\n\n" (cl-first s) " " (cider-browse-spec--pprint (cl-second s))))))
"\n"))))
;; prettier (s/cat )
((cider--spec-fn-p form-tag "cat")
(let ((name-spec-pairs (seq-partition (cl-rest form) 2)))
(format "(s/cat %s)"
(thread-last
name-spec-pairs
(mapcar (lambda (s)
(concat "\n" (cl-first s) " " (cider-browse-spec--pprint (cl-second s)))))
(cl-reduce #'concat)))))
;; prettier (s/alt )
((cider--spec-fn-p form-tag "alt")
(let ((name-spec-pairs (seq-partition (cl-rest form) 2)))
(format "(s/alt %s)"
(thread-last
name-spec-pairs
(mapcar (lambda (s)
(concat "\n" (cl-first s) " " (cider-browse-spec--pprint (cl-second s)))))
(cl-reduce #'concat)))))
;; prettier (s/fspec )
((cider--spec-fn-p form-tag "fspec")
(thread-last
(seq-partition (cl-rest form) 2)
(cl-remove-if (lambda (s) (and (stringp (cl-second s))
(string-empty-p (cl-second s)))))
(mapcar (lambda (s)
(format "\n%-11s: %s" (pcase (cl-first s)
(":args" "arguments")
(":ret" "returns")
(":fn" "invariants"))
(cider-browse-spec--pprint (cl-second s)))))
(cl-reduce #'concat)
(format "%s")))
;; prettier (s/schema )
((cider--spec-fn-p form-tag "schema")
(cider-browse-spec--render-schema form))
;; prettier (s/select )
((cider--spec-fn-p form-tag "select")
(cider-browse-spec--render-select form))
;; prettier (s/union )
((cider--spec-fn-p form-tag "union")
(cider-browse-spec--render-union form))
;; every other with no special management
(t (format "(%s %s)"
(cider-browse-spec--pprint form-tag)
(string-join (mapcar #'cider-browse-spec--pprint (cl-rest form)) " "))))))
((nrepl-dict-p form)
(cider-browse-spec--render-map form))
(t (format "%s" form))))
(defun cider-browse-spec--pprint-indented (spec-form)
"Indent (pretty-print) and font-lock SPEC-FORM.
Return the result as a string."
(with-temp-buffer
(clojure-mode)
(insert (cider-browse-spec--pprint spec-form))
(indent-region (point-min) (point-max))
(font-lock-ensure)
(buffer-string)))
(defun cider-browse-spec--draw-spec-buffer (buffer spec spec-form)
"Reset contents of BUFFER and draws everything needed to browse the SPEC-FORM.
Display SPEC as a title and uses `cider-browse-spec--pprint' to display
a more user friendly representation of SPEC-FORM."
(with-current-buffer buffer
(let ((inhibit-read-only t))
(cider--help-setup-xref (list #'cider-browse-spec spec) nil buffer)
(goto-char (point-max))
(insert (cider-font-lock-as-clojure spec) "\n\n")
(insert (cider-browse-spec--pprint-indented spec-form))
(cider--make-back-forward-xrefs)
(current-buffer))))
(defun cider-browse-spec--browse (spec)
"Browse SPEC."
(cider-ensure-connected)
(cider-ensure-op-supported "spec-form")
;; Expand auto-resolved keywords
(when-let* ((val (and (string-match-p "^::.+" spec)
(nrepl-dict-get (cider-sync-tooling-eval spec (cider-current-ns)) "value"))))
(setq spec val))
(with-current-buffer (cider-popup-buffer cider-browse-spec-buffer 'select #'cider-browse-spec-view-mode 'ancillary)
(setq-local cider-browse-spec--current-spec spec)
(cider-browse-spec--draw-spec-buffer (current-buffer)
spec
(cider-sync-request:spec-form spec))
(goto-char (point-min))
(current-buffer)))
(defun cider-browse-spec--browse-at (&optional pos)
"View the definition of a spec.
Optional argument POS is the position of a spec, defaulting to point. POS
may also be a button, so this function can be used a the button's `action'
property."
(interactive)
(let ((pos (or pos (point))))
(when-let* ((spec (button-get pos 'spec-name)))
(cider-browse-spec--browse spec))))
;; Interactive Functions
(defun cider-browse-spec--print-curr-spec-example ()
"Generate and print an example of the current spec."
(interactive)
(cider-ensure-connected)
(cider-ensure-op-supported "spec-example")
(if-let* ((spec cider-browse-spec--current-spec))
(if-let* ((example (cider-sync-request:spec-example spec)))
(with-current-buffer (cider-popup-buffer cider-browse-spec-example-buffer 'select #'cider-browse-spec-example-mode 'ancillary)
(setq-local cider-browse-spec--current-spec spec)
(let ((inhibit-read-only t))
(insert "Example of " (cider-font-lock-as-clojure spec))
(insert "\n\n")
(insert (cider-font-lock-as-clojure example))
(goto-char (point-min))))
(error (format "No example for spec %s" spec)))
(error "No current spec")))
(defun cider-browse-spec--example-revert-buffer-function (&rest _)
"`revert-buffer' function for `cider-browse-spec-example-mode'.
Generates a new example for the current spec."
(cider-browse-spec--print-curr-spec-example))
;;;###autoload
(defun cider-browse-spec (spec)
"Browse SPEC definition."
(interactive (list (completing-read "Browse spec: "
(cider-sync-request:spec-list)
nil nil
(cider-symbol-at-point))))
(cider-browse-spec--browse spec))
(defun cider-browse-spec-regex (regex)
"Open the list of specs that matches REGEX in a popup buffer.
Displays all specs when REGEX is nil."
(cider-ensure-connected)
(cider-ensure-op-supported "spec-list")
(let ((filter-regex (or regex "")))
(with-current-buffer (cider-popup-buffer cider-browse-spec-buffer 'select nil 'ancillary)
(let ((specs (cider-sync-request:spec-list filter-regex)))
(cider-browse-spec--draw-list-buffer (current-buffer)
(if (string-empty-p filter-regex)
"All specs in registry"
(format "All specs matching regex `%s' in registry" filter-regex))
specs)))))
;;;###autoload
(defun cider-browse-spec-all (&optional arg)
"Open list of specs in a popup buffer.
With a prefix argument ARG, prompts for a regexp to filter specs.
No filter applied if the regexp is the empty string."
(interactive "P")
(cider-browse-spec-regex (if arg (read-string "Filter regex: ") "")))
(provide 'cider-browse-spec)
;;; cider-browse-spec.el ends here
;;; cider-cheatsheet.el --- Quick reference for Clojure -*- lexical-binding: t -*-
;; Copyright © 2019-2022 Kris Jenkins, Bozhidar Batsov and CIDER contributors
;;
;; Author: Kris Jenkins <krisajenkins@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; A quick reference system for Clojure. Fast, searchable & available offline.
;; Mostly taken from Kris Jenkins' `clojure-cheatsheet'
;; See: https://github.com/clojure-emacs/clojure-cheatsheet
;;; Code:
(require 'cider-doc)
(require 'seq)
(defconst cider-cheatsheet-hierarchy
'(("Primitives"
("Numbers"
("Arithmetic"
(clojure.core + - * / quot rem mod dec inc max min))
("Compare"
(clojure.core = == not= < > <= >= compare))
("Bitwise"
(clojure.core bit-and bit-and-not bit-clear bit-flip bit-not bit-or bit-set bit-shift-left bit-shift-right bit-test bit-xor unsigned-bit-shift-right))
("Cast"
(clojure.core byte short long int float double bigdec bigint biginteger num rationalize))
("Test"
(clojure.core nil? some? identical? zero? pos? neg? even? odd?))
("Random"
(clojure.core rand rand-int))
("BigDecimal"
(clojure.core with-precision))
("Ratios"
(clojure.core numerator denominator ratio?))
("Arbitrary Precision Arithmetic"
(clojure.core +\' -\' *\' inc\' dec\'))
("Unchecked"
(clojure.core *unchecked-math*
unchecked-add
unchecked-add-int
unchecked-byte
unchecked-char
unchecked-dec
unchecked-dec-int
unchecked-divide-int
unchecked-double
unchecked-float
unchecked-inc
unchecked-inc-int
unchecked-int
unchecked-long
unchecked-multiply
unchecked-multiply-int
unchecked-negate
unchecked-negate-int
unchecked-remainder-int
unchecked-short
unchecked-subtract
unchecked-subtract-int)))
("Strings"
("Create"
(clojure.core str format))
("Use"
(clojure.core count get subs compare)
(clojure.string join escape split split-lines replace replace-first reverse re-quote-replacement index-of last-index-of starts-with? ends-with? includes?))
("Regex"
(clojure.core re-find re-seq re-matches re-pattern re-matcher re-groups)
(clojure.string replace replace-first re-quote-replacement))
("Letters"
(clojure.string capitalize lower-case upper-case))
("Trim"
(clojure.string trim trim-newline triml trimr))
("Test"
(clojure.core char char? string?)
(clojure.string blank?)))
("Other"
("Characters"
(clojure.core char char-name-string char-escape-string))
("Keywords"
(clojure.core keyword keyword? find-keyword))
("Symbols"
(clojure.core symbol symbol? gensym))
("Data Readers"
(clojure.core *data-readers* default-data-readers *default-data-reader-fn*))))
("Collections"
("Generic Ops"
(clojure.core count bounded-count empty not-empty into conj))
("Tree Walking"
(clojure.walk walk prewalk prewalk-demo prewalk-replace postwalk postwalk-demo postwalk-replace keywordize-keys stringify-keys))
("Content tests"
(clojure.core distinct? empty? every? not-every? some not-any?))
("Capabilities"
(clojure.core sequential? associative? sorted? counted? reversible?))
("Type tests"
(clojure.core type class coll? list? vector? set? map? seq?
number? integer? float? decimal? class? rational? ratio?
chunked-seq? reduced? special-symbol? record?))
("Lists"
("Create"
(clojure.core list list*))
("Examine"
(clojure.core first nth peek))
("Change"
(clojure.core cons conj rest pop)))
("Vectors"
("Create"
(clojure.core vec vector vector-of))
("Examine"
(clojure.core get peek))
("Change"
(clojure.core assoc pop subvec replace conj rseq))
("Ops"
(clojure.core mapv filterv reduce-kv)))
("Sets"
("Create"
(clojure.core set hash-set sorted-set sorted-set-by))
("Examine"
(clojure.core get contains?))
("Change"
(clojure.core conj disj))
("Relational Algebra"
(clojure.set join select project union difference intersection))
("Get map"
(clojure.set index rename-keys rename map-invert))
("Test"
(clojure.set subset? superset?))
("Sorted Sets"
(clojure.core rseq subseq rsubseq)))
("Maps"
("Create"
(clojure.core hash-map array-map zipmap sorted-map sorted-map-by bean frequencies group-by))
("Examine"
(clojure.core get get-in contains? find keys vals map-entry?))
("Change"
(clojure.core assoc assoc-in dissoc merge merge-with select-keys update update-in))
("Entry"
(clojure.core key val))
("Sorted Maps"
(clojure.core rseq subseq rsubseq)))
("Hashes"
(clojure.core hash hash-ordered-coll hash-unordered-coll mix-collection-hash))
("Volatiles"
(clojure.core volatile! volatile? vreset! vswap!)))
("Functions"
("Create"
(clojure.core fn defn defn- definline identity constantly comp complement partial juxt memfn memoize fnil every-pred some-fn trampoline))
("Call"
(clojure.core -> ->> some-> some->> as-> cond-> cond->>))
("Test"
(clojure.core fn? ifn?)))
("Transducers"
("Create"
(clojure.core cat dedupe distinct drop drop-while filter halt-when interpose keep keep-indexed map map-indexed mapcat partition-all partition-by random-sample remove replace take take-nth take-while))
("Call"
(clojure.core ->Eduction eduction into sequence transduce completing run!))
("Early Termination"
(clojure.core deref reduced reduced? ensure-reduced unreduced)))
("Spec"
("Operations"
(clojure.spec.alpha valid? conform unform explain explain-data explain-str explain-out form describe assert check-asserts check-asserts?))
("Generator Ops"
(clojure.spec.alpha gen exercise exercise-fn))
("Defn & Registry"
(clojure.spec.alpha def fdef registry get-spec spec? spec with-gen))
("Logical"
(clojure.spec.alpha and or))
("Collection"
(clojure.spec.alpha coll-of map-of every every-kv keys merge))
("Regex "
(clojure.spec.alpha cat alt * + \? & keys*))
("Range"
(clojure.spec.alpha int-in inst-in double-in int-in-range? inst-in-range?))
("Custom Explain"
(clojure.spec.alpha explain-printer *explain-out*))
("Other"
(clojure.spec.alpha nilable multi-spec fspec conformer))
("Predicates with test.check generators"
("Numbers"
(clojure.core number? rational? integer? ratio? decimal? float? zero? double? int? nat-int? neg-int? pos-int?))
("Symbols & Keywords"
(clojure.core keyword? symbol? ident? qualified-ident? qualified-keyword? qualified-symbol? simple-ident? simple-keyword? simple-symbol?))
("Scalars"
(clojure.core string? true? false? nil? some? boolean? bytes? inst? uri? uuid?))
("Collections"
(clojure.core list? map? set? vector? associative? coll? sequential? seq? empty? indexed? seqable?))
("Other"
(clojure.core any?))))
("Other"
("XML"
(clojure.core xml-seq)
(clojure.xml parse))
("REPL"
(clojure.core *1 *2 *3 *e *print-dup* *print-length* *print-level* *print-meta* *print-readably*))
("EDN"
(clojure.edn read read-string))
("Compiling Code & Class Generation"
(clojure.core *compile-files* *compile-path* *file* *warn-on-reflection* compile gen-class gen-interface loaded-libs test))
("Misc"
(clojure.core eval force name *clojure-version* clojure-version *command-line-args*))
("Pretty Printing"
(clojure.pprint pprint print-table pp *print-right-margin*))
("Browser / Shell"
(clojure.java.browse browse-url)
(clojure.java.shell sh with-sh-dir with-sh-env)))
("Vars & Global Environment"
("Def Variants"
(:special def)
(clojure.core defn defn- definline defmacro defmethod defmulti defonce defrecord))
("Interned Vars"
(:special var)
(clojure.core declare intern binding find-var))
("Var Objects"
(clojure.core with-local-vars var-get var-set alter-var-root var?))
("Var Validators"
(clojure.core set-validator! get-validator)))
("Reader Conditionals"
(clojure.core reader-conditional reader-conditional? tagged-literal tagged-literal?))
("Abstractions"
("Protocols"
(clojure.core defprotocol extend extend-type extend-protocol reify extends? satisfies? extenders))
("Records & Types"
(clojure.core defrecord deftype))
("Multimethods"
("Define"
(clojure.core defmulti defmethod))
("Dispatch"
(clojure.core get-method methods))
("Remove"
(clojure.core remove-method remove-all-methods))
("Prefer"
(clojure.core prefer-method prefers))
("Relation"
(clojure.core derive isa? parents ancestors descendants make-hierarchy))))
("Macros"
("Create"
(clojure.core defmacro definline))
("Debug"
(clojure.core macroexpand-1 macroexpand)
(clojure.walk macroexpand-all))
("Branch"
(clojure.core and or when when-not when-let when-first if-not if-let cond condp case))
("Loop"
(clojure.core for doseq dotimes while))
("Arrange"
(clojure.core .. doto ->))
("Scope"
(clojure.core binding locking time)
(clojure.core with-in-str with-local-vars with-open with-out-str with-precision with-redefs with-redefs-fn))
("Lazy"
(clojure.core lazy-cat lazy-seq delay delay?))
("Doc"
(clojure.core assert comment)
(clojure.repl doc dir dir-fn source-fn)))
("Java Interop"
("General"
(:special new set!)
(clojure.core .. doto bean comparator enumeration-seq import iterator-seq memfn definterface supers bases))
("Cast"
(clojure.core boolean byte short char int long float double bigdec bigint num cast biginteger))
("Exceptions"
(:special throw try catch finally)
(clojure.core ex-info ex-data Throwable->map StackTraceElement->vec)
(clojure.repl pst))
("Arrays"
("Create"
(clojure.core boolean-array byte-array double-array char-array float-array int-array long-array make-array object-array short-array to-array))
("Manipulate"
(clojure.core aclone aget aset alength amap areduce aset-int aset-long aset-short aset-boolean aset-byte aset-char aset-double aset-float))
("Cast"
(clojure.core booleans bytes chars doubles floats ints longs shorts)))
("Proxy"
("Create"
(clojure.core proxy get-proxy-class construct-proxy init-proxy))
("Misc"
(clojure.core proxy-mappings proxy-super update-proxy))))
("Namespaces"
("Current"
(clojure.core *ns*))
("Create Switch"
(clojure.core ns in-ns create-ns))
("Add"
(clojure.core alias import intern refer refer-clojure))
("Find"
(clojure.core all-ns find-ns))
("Examine"
(clojure.core ns-aliases ns-imports ns-interns ns-map ns-name ns-publics ns-refers))
("From symbol"
(clojure.core resolve namespace ns-resolve the-ns))
("Remove"
(clojure.core ns-unalias ns-unmap remove-ns)))
("Loading"
("Load libs"
(clojure.core require use import refer))
("List Loaded"
(clojure.core loaded-libs))
("Load Misc"
(clojure.core load load-file load-reader load-string)))
("Concurrency"
("Atoms"
(clojure.core atom swap! swap-vals! reset! reset-vals! compare-and-set!))
("Futures"
(clojure.core future future-call future-cancel future-cancelled? future-done? future?))
("Threads"
(clojure.core bound-fn bound-fn* get-thread-bindings pop-thread-bindings push-thread-bindings))
("Misc"
(clojure.core locking pcalls pvalues pmap seque promise deliver))
("Refs & Transactions"
("Create"
(clojure.core ref))
("Examine"
(clojure.core deref))
("Transaction"
(clojure.core sync dosync io!))
("In Transaction"
(clojure.core ensure ref-set alter commute))
("Validators"
(clojure.core get-validator set-validator!))
("History"
(clojure.core ref-history-count ref-max-history ref-min-history)))
("Agents & Asynchronous Actions"
("Create"
(clojure.core agent))
("Examine"
(clojure.core agent-error))
("Change State"
(clojure.core send send-off restart-agent send-via set-agent-send-executor! set-agent-send-off-executor!))
("Block Waiting"
(clojure.core await await-for))
("Ref Validators"
(clojure.core get-validator set-validator!))
("Watchers"
(clojure.core add-watch remove-watch))
("Thread Handling"
(clojure.core shutdown-agents))
("Error"
(clojure.core error-handler set-error-handler! error-mode set-error-mode!))
("Misc"
(clojure.core *agent* release-pending-sends))))
("Sequences"
("Creating a Lazy Seq"
("From Collection"
(clojure.core seq sequence keys vals rseq subseq rsubseq))
("From Producer Fn"
(clojure.core lazy-seq repeatedly iterate))
("From Constant"
(clojure.core repeat range))
("From Other"
(clojure.core file-seq line-seq resultset-seq re-seq tree-seq xml-seq iterator-seq enumeration-seq))
("From Seq"
(clojure.core keep keep-indexed)))
("Seq in, Seq out"
("Get shorter"
(clojure.core distinct dedupe filter remove for))
("Get longer"
(clojure.core cons conj concat lazy-cat mapcat cycle interleave interpose)))
("Tail-items"
(clojure.core rest nthrest fnext nnext drop drop-while take-last for))
("Head-items"
(clojure.core take take-nth take-while butlast drop-last for))
("Change"
(clojure.core conj concat distinct flatten group-by partition partition-all partition-by split-at split-with filter remove replace shuffle random-sample))
("Rearrange"
(clojure.core reverse sort sort-by compare))
("Process items"
(clojure.core map pmap map-indexed mapcat for replace seque))
("Using a Seq"
("Extract item"
(clojure.core first second last rest next ffirst nfirst fnext nnext nth nthnext rand-nth when-first max-key min-key))
("Construct coll"
(clojure.core zipmap into reduce reductions set vec into-array to-array-2d))
("Pass to fn"
(clojure.core apply))
("Search"
(clojure.core some filter))
("Force evaluation"
(clojure.core doseq dorun doall))
("Check for forced"
(clojure.core realized?))))
("Zippers"
("Create"
(clojure.zip zipper seq-zip vector-zip xml-zip))
("Get loc"
(clojure.zip up down left right leftmost rightmost))
("Get seq"
(clojure.zip lefts rights path children))
("Change"
(clojure.zip make-node replace edit insert-child insert-left insert-right append-child remove))
("Move"
(clojure.zip next prev))
("XML"
(clojure.data.zip.xml attr attr= seq-test tag= text text= xml-> xml1->))
("Misc"
(clojure.zip root node branch? end?)))
("Documentation"
("REPL"
(clojure.repl doc find-doc apropos source pst)
(clojure.java.javadoc javadoc)))
("Transients"
("Create"
(clojure.core transient persistent!))
("Change"
(clojure.core conj! pop! assoc! dissoc! disj!)))
("Misc"
("Compare"
(clojure.core = == identical? not= not compare)
(clojure.data diff))
("Test"
(clojure.core true? false? nil? instance?)))
("IO"
("To/from ..."
(clojure.core spit slurp))
("To *out*"
(clojure.core pr prn print printf println newline)
(clojure.pprint print-table))
("To writer"
(clojure.pprint pprint cl-format))
("To string"
(clojure.core format with-out-str pr-str prn-str print-str println-str))
("From *in*"
(clojure.core read-line read))
("From reader"
(clojure.core line-seq read))
("From string"
(clojure.core read-string with-in-str))
("Open"
(clojure.core with-open)
(clojure.java.io reader writer input-stream output-stream))
("Interop"
(clojure.java.io make-writer make-reader make-output-stream make-input-stream))
("Misc"
(clojure.core flush file-seq *in* *out* *err*)
(clojure.java.io file copy delete-file resource as-file as-url as-relative-path make-parents)))
("Metadata"
(clojure.core meta with-meta alter-meta! reset-meta! vary-meta))
("Special Forms"
(:special def if do quote var recur throw try monitor-enter monitor-exit)
(clojure.core fn loop)
("Binding / Destructuring"
(clojure.core let fn letfn defn defmacro loop for doseq if-let if-some when-let when-some)))
("Async"
("Main"
(clojure.core.async go go-loop <! <!! >! >!! chan put! take take! close! timeout offer! poll! promise-chan))
("Choice"
(clojure.core.async alt! alt!! alts! alts!! do-alts))
("Buffering"
(clojure.core.async buffer dropping-buffer sliding-buffer unblocking-buffer?))
("Pipelines"
(clojure.core.async pipeline pipeline-async pipeline-blocking))
("Threading"
(clojure.core.async thread thread-call))
("Mixing"
(clojure.core.async admix solo-mode mix unmix unmix-all toggle merge pipe unique))
("Multiples"
(clojure.core.async mult tap untap untap-all))
("Publish/Subscribe"
(clojure.core.async pub sub unsub unsub-all))
("Higher Order"
(clojure.core.async filter< filter> map map< map> mapcat< mapcat> partition partition-by reduce remove< remove> split))
("Pre-Populate"
(clojure.core.async into onto-chan to-chan)))
("Unit Tests"
("Defining"
(clojure.test deftest deftest- testing is are))
("Running"
(clojure.test run-tests run-all-tests test-vars))
("Fixtures"
(clojure.test use-fixtures join-fixtures compose-fixtures))))
"A data structure for Clojure cheatsheet information.
It's a tree, where the head of each list determines the context of the rest
of the list. The head may be:
- A string, in which case it's a (sub)heading for the rest of the items.
- A symbol, in which case it's the Clojure namespace of the symbols that
follow it.
- The keyword :special, in which case it's a Clojure special form
- Any other keyword, in which case it's a typed item that will be passed
through.
Note that some Clojure symbols appear in more than once. This is entirely
intentional. For instance, `map` belongs in the sections on collections
and transducers.")
(defun cider-cheatsheet--expand-vars (list)
"Expand the symbols in LIST to fully-qualified var names.
This list is supposed to have the following format:
(my-ns var1 var2 var3)"
(let ((ns (car list))
(vars (cdr list)))
(if (eq ns :special)
(mapcar #'symbol-name vars)
(mapcar (lambda (var) (format "%s/%s" ns var)) vars))))
(defun cider-cheatsheet--select-var (var-list)
"Expand the symbols in VAR-LIST to fully-qualified var names.
The list can hold one or more lists inside - one per each namespace."
(let ((namespaced-vars (seq-mapcat #'cider-cheatsheet--expand-vars
(seq-remove (lambda (list)
(eq (car list) :url))
var-list))))
(cider-doc-lookup (completing-read "Select var: " namespaced-vars))))
;;;###autoload
(defun cider-cheatsheet ()
"Navigate `cider-cheatsheet-hierarchy' with `completing-read'.
When you make it to a Clojure var its doc buffer gets displayed."
(interactive)
(let ((cheatsheet-data cider-cheatsheet-hierarchy))
(while (stringp (caar cheatsheet-data))
(let* ((sections (mapcar #'car cheatsheet-data))
(sel-section (completing-read "Select cheatsheet section: " sections))
(section-data (seq-find (lambda (elem) (equal (car elem) sel-section)) cheatsheet-data)))
(setq cheatsheet-data (cdr section-data))))
(cider-cheatsheet--select-var cheatsheet-data)))
(provide 'cider-cheatsheet)
;;; cider-cheatsheet.el ends here
;;; cider-classpath.el --- Basic Java classpath browser -*- lexical-binding: t; -*-
;; Copyright © 2014-2022 Bozhidar Batsov and CIDER contributors
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Basic Java classpath browser for CIDER.
;;; Code:
(require 'cider-client)
(require 'cider-popup)
(require 'subr-x)
(defvar cider-classpath-buffer "*cider-classpath*")
(defvar cider-classpath-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map cider-popup-buffer-mode-map)
(define-key map (kbd "RET") #'cider-classpath-operate-on-point)
(define-key map "n" #'next-line)
(define-key map "p" #'previous-line)
map))
(defvar cider-classpath-mouse-map
(let ((map (make-sparse-keymap)))
(define-key map [mouse-1] #'cider-classpath-handle-mouse)
map))
(define-derived-mode cider-classpath-mode special-mode "classpath"
"Major mode for browsing the entries in Java's classpath.
\\{cider-classpath-mode-map}"
(setq-local electric-indent-chars nil)
(setq-local sesman-system 'CIDER)
(when cider-special-mode-truncate-lines
(setq-local truncate-lines t)))
(defun cider-classpath-list (buffer items)
"Populate BUFFER with ITEMS."
(with-current-buffer buffer
(cider-classpath-mode)
(let ((inhibit-read-only t))
(erase-buffer)
(dolist (item items)
(insert item "\n"))
(goto-char (point-min)))))
(defun cider-classpath-properties (text)
"Decorate TEXT with a clickable keymap and function face."
(let ((face (cond
((not (file-exists-p text)) 'font-lock-warning-face)
((file-directory-p text) 'dired-directory)
(t 'default))))
(propertize text
'font-lock-face face
'mouse-face 'highlight
'keymap cider-classpath-mouse-map)))
(defun cider-classpath-operate-on-point ()
"Expand browser according to thing at current point."
(interactive)
(let* ((bol (line-beginning-position))
(eol (line-end-position))
(line (buffer-substring-no-properties bol eol)))
(find-file-other-window line)))
(defun cider-classpath-handle-mouse (_event)
"Handle mouse click EVENT."
(interactive "e")
(cider-classpath-operate-on-point))
;;;###autoload
(defun cider-classpath ()
"List all classpath entries."
(interactive)
(cider-ensure-connected)
(with-current-buffer (cider-popup-buffer cider-classpath-buffer 'select nil 'ancillary)
(cider-classpath-list (current-buffer)
(mapcar (lambda (name)
(cider-classpath-properties name))
(cider-classpath-entries)))))
;;;###autoload
(defun cider-open-classpath-entry ()
"Open a classpath entry."
(interactive)
(cider-ensure-connected)
(when-let* ((entry (completing-read "Classpath entries: " (cider-classpath-entries))))
(find-file-other-window entry)))
(provide 'cider-classpath)
;;; cider-classpath.el ends here
;;; cider-client.el --- A layer of abstraction above low-level nREPL client code. -*- lexical-binding: t -*-
;; Copyright © 2013-2022 Bozhidar Batsov
;;
;; Author: Bozhidar Batsov <bozhidar@batsov.dev>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; A layer of abstraction above the low-level nREPL client code.
;;; Code:
(require 'map)
(require 'seq)
(require 'subr-x)
(require 'parseedn)
(require 'clojure-mode)
(require 'spinner)
(require 'cider-connection)
(require 'cider-common)
(require 'cider-util)
(require 'nrepl-client)
;;; Eval spinner
(defcustom cider-eval-spinner-type 'progress-bar
"Appearance of the evaluation spinner.
Value is a symbol. The possible values are the symbols in the
`spinner-types' variable."
:type 'symbol
:group 'cider
:package-version '(cider . "0.10.0"))
(defcustom cider-show-eval-spinner t
"When true, show the evaluation spinner in the mode line."
:type 'boolean
:group 'cider
:package-version '(cider . "0.10.0"))
(defcustom cider-eval-spinner-delay 1
"Amount of time, in seconds, after which the evaluation spinner will be shown."
:type 'integer
:group 'cider
:package-version '(cider . "0.10.0"))
(defcustom cider-enhanced-cljs-completion-p t
"This setting enables dynamic cljs completions.
That is, expressions at point are evaluated and the properties of the
resulting value are used to compute completions."
:type 'boolean
:group 'cider
:package-version '(cider . "0.23.0"))
(defcustom cider-before-eval-hook nil
"List of functions to call before eval request is sent to nrepl."
:type 'hook
:group 'cider
:package-version '(cider . "1.2.0"))
(defcustom cider-after-eval-done-hook nil
"List of functions to call after eval was responded by nrepl with done status."
:type 'hook
:group 'cider
:package-version '(cider . "1.2.0"))
(defun cider-spinner-start (buffer)
"Start the evaluation spinner in BUFFER.
Do nothing if `cider-show-eval-spinner' is nil."
(when cider-show-eval-spinner
(with-current-buffer buffer
(spinner-start cider-eval-spinner-type nil
cider-eval-spinner-delay))))
(defun cider-eval-spinner (eval-buffer response)
"Handle RESPONSE stopping the spinner.
EVAL-BUFFER is the buffer where the spinner was started."
;; buffer still exists and
;; we've got status "done" from nrepl
;; stop the spinner
(when (and (buffer-live-p eval-buffer)
(let ((status (nrepl-dict-get response "status")))
(or (member "done" status)
(member "eval-error" status)
(member "error" status))))
(with-current-buffer eval-buffer
(when spinner-current (spinner-stop)))))
;;; Evaluation helpers
(defun cider-ns-form-p (form)
"Check if FORM is an ns form."
(string-match-p "^[[:space:]]*\(ns\\([[:space:]]*$\\|[[:space:]]+\\)" form))
(defun cider-ns-from-form (ns-form)
"Get ns substring from NS-FORM."
(when (string-match "^[ \t\n]*\(ns[ \t\n]+\\([^][ \t\n(){}]+\\)" ns-form)
(match-string-no-properties 1 ns-form)))
(defvar-local cider-buffer-ns nil
"Current Clojure namespace of some buffer.
Useful for special buffers (e.g. REPL, doc buffers) that have to keep track
of a namespace. This should never be set in Clojure buffers, as there the
namespace should be extracted from the buffer's ns form.")
(defun cider-current-ns (&optional no-default)
"Return the current ns.
The ns is extracted from the ns form for Clojure buffers and from
`cider-buffer-ns' for all other buffers. If it's missing, use the current
REPL's ns, otherwise fall back to \"user\". When NO-DEFAULT is non-nil, it
will return nil instead of \"user\"."
(or cider-buffer-ns
(clojure-find-ns)
(when-let* ((repl (cider-current-repl)))
(buffer-local-value 'cider-buffer-ns repl))
(if no-default nil "user")))
(defun cider-path-to-ns (relpath)
"Transform RELPATH to Clojure namespace.
Remove extension and substitute \"/\" with \".\", \"_\" with \"-\"."
(thread-last
relpath
(file-name-sans-extension)
(replace-regexp-in-string "/" ".")
(replace-regexp-in-string "_" "-")))
(defun cider-expected-ns (&optional path)
"Return the namespace string matching PATH, or nil if not found.
If PATH is nil, use the path to the file backing the current buffer. The
command falls back to `clojure-expected-ns' in the absence of an active
nREPL connection."
(if (cider-connected-p)
(let* ((path (file-truename (or path buffer-file-name)))
(relpath (thread-last
(cider-classpath-entries)
(seq-filter #'file-directory-p)
(seq-map (lambda (dir)
(when (file-in-directory-p path dir)
(file-relative-name path dir))))
(seq-filter #'identity)
(seq-sort (lambda (a b)
(< (length a) (length b))))
(car))))
(if relpath
(cider-path-to-ns relpath)
(clojure-expected-ns path)))
(clojure-expected-ns path)))
(defun cider-nrepl-op-supported-p (op &optional connection skip-ensure)
"Check whether the CONNECTION supports the nREPL middleware OP.
Skip check if repl is active if SKIP-ENSURE is non nil."
(nrepl-op-supported-p op (or connection (cider-current-repl nil (if skip-ensure
nil
'ensure)))))
(defun cider-ensure-op-supported (op)
"Check for support of middleware op OP.
Signal an error if it is not supported."
(unless (cider-nrepl-op-supported-p op)
(user-error "`%s' requires the nREPL op \"%s\" (provided by cider-nrepl)" this-command op)))
(defun cider-nrepl-send-request (request callback &optional connection tooling)
"Send REQUEST and register response handler CALLBACK.
REQUEST is a pair list of the form (\"op\" \"operation\" \"par1-name\"
\"par1\" ... ).
If CONNECTION is provided dispatch to that connection instead of
the current connection. Return the id of the sent message.
If TOOLING is truthy then the tooling session is used."
(nrepl-send-request request callback (or connection (cider-current-repl 'any 'ensure)) tooling))
(defun cider-nrepl-send-sync-request (request &optional connection abort-on-input)
"Send REQUEST to the nREPL server synchronously using CONNECTION.
Hold till final \"done\" message has arrived and join all response messages
of the same \"op\" that came along and return the accumulated response.
If ABORT-ON-INPUT is non-nil, the function will return nil
at the first sign of user input, so as not to hang the
interface."
(nrepl-send-sync-request request
(or connection (cider-current-repl 'any 'ensure))
abort-on-input))
(defun cider-nrepl-send-unhandled-request (request &optional connection)
"Send REQUEST to the nREPL CONNECTION and ignore any responses.
Immediately mark the REQUEST as done. Return the id of the sent message."
(let* ((conn (or connection (cider-current-repl 'any 'ensure)))
(id (nrepl-send-request request #'ignore conn)))
(with-current-buffer conn
(nrepl--mark-id-completed id))
id))
(defun cider-nrepl-request:eval (input callback &optional ns line column additional-params connection)
"Send the request INPUT and register the CALLBACK as the response handler.
If NS is non-nil, include it in the request. LINE and COLUMN, if non-nil,
define the position of INPUT in its buffer. ADDITIONAL-PARAMS is a plist
to be appended to the request message. CONNECTION is the connection
buffer, defaults to (cider-current-repl)."
(let ((connection (or connection (cider-current-repl nil 'ensure)))
(eval-buffer (current-buffer)))
(run-hooks 'cider-before-eval-hook)
(nrepl-request:eval input
(lambda (response)
(when cider-show-eval-spinner
(cider-eval-spinner connection response))
(when (and (buffer-live-p eval-buffer)
(member "done" (nrepl-dict-get response "status")))
(with-current-buffer eval-buffer
(run-hooks 'cider-after-eval-done-hook)))
(funcall callback response))
connection
ns line column additional-params)
(cider-spinner-start connection)))
(defun cider-nrepl-sync-request:eval (input &optional connection ns)
"Send the INPUT to the nREPL CONNECTION synchronously.
If NS is non-nil, include it in the eval request."
(nrepl-sync-request:eval input (or connection (cider-current-repl nil 'ensure)) ns))
(defcustom cider-format-code-options nil
"A map of options that will be passed to `cljfmt' to format code.
Assuming this is the Clojure map you want to use as `cljfmt' options:
{:indents {org.me/foo [[:inner 0]]}
:alias-map {\"me\" \"org.me\"}}
you need to encode it as the following plist:
'((\"indents\" ((\"org.me/foo\" ((\"inner\" 0))))) (\"alias-map\" ((\"me\" \"org.me\"))))"
:type 'list
:group 'cider
:package-version '(cider . "1.1.0"))
(defun cider--nrepl-format-code-request-map (&optional format-options)
"Map to merge into requests that require code formatting.
If non-nil, FORMAT-OPTIONS specifies the options cljfmt will use to format
the code. See `cider-format-code-options` for details."
(when format-options
(let* ((indents-dict (when (assoc "indents" format-options)
(thread-last
(cadr (assoc "indents" format-options))
(map-pairs)
(seq-mapcat #'identity)
(apply #'nrepl-dict))))
(alias-map-dict (when (assoc "alias-map" format-options)
(thread-last
(cadr (assoc "alias-map" format-options))
(map-pairs)
(seq-mapcat #'identity)
(apply #'nrepl-dict)))))
(thread-last
(map-merge 'list
(when indents-dict
`(("indents" ,indents-dict)))
(when alias-map-dict
`(("alias-map" ,alias-map-dict))))
(map-pairs)
(seq-mapcat #'identity)
(apply #'nrepl-dict)))))
(defcustom cider-print-fn 'pprint
"Sets the function to use for printing.
nil – to defer to nREPL to choose the printing function. This will use
the bound value of \\=`nrepl.middleware.print/*print-fn*\\=`, which
defaults to the equivalent of \\=`clojure.core/pr\\=`.
`pr' – to use the equivalent of \\=`clojure.core/pr\\=`.
`pprint' – to use \\=`clojure.pprint/pprint\\=` (this is the default).
`fipp' – to use the Fast Idiomatic Pretty Printer, approximately 5-10x
faster than \\=`clojure.core/pprint\\=`.
`puget' – to use Puget, which provides canonical serialization of data on
top of fipp, but at a slight performance cost.
`zprint' – to use zprint, a fast and flexible alternative to the libraries
mentioned above.
Alternatively can be the namespace-qualified name of a Clojure var whose
function takes three arguments: the object to print, the
\\=`java.io.PrintWriter\\=` to print on, and a (possibly nil) map of
options. If the function cannot be resolved, will behave as if set to
nil."
:type '(choice (const nil)
(const pr)
(const pprint)
(const fipp)
(const puget)
(const zprint)
string)
:group 'cider
:package-version '(cider . "0.21.0"))
(defcustom cider-print-options nil
"A map of options that will be passed to `cider-print-fn'.
Here's an example for `pprint':
'((\"length\" 50) (\"right-margin\" 70))"
:type 'list
:group 'cider
:package-version '(cider . "0.21.0"))
(make-obsolete-variable 'cider-pprint-fn 'cider-print-fn "0.21")
(make-obsolete-variable 'cider-pprint-options 'cider-print-options "0.21")
(defcustom cider-print-quota (* 1024 1024)
"A hard limit on the number of bytes to return from any printing operation.
Set to nil for no limit."
:type 'integer
:group 'cider
:package-version '(cider . "0.21.0"))
(defcustom cider-print-buffer-size (* 4 1024)
"The size in bytes of each value/output chunk when using print streaming.
Smaller values mean smaller data chunks and faster feedback, but they also mean
smaller results that can be font-locked as Clojure in the REPL buffers, as only
a single chunk result can be font-locked.
The default value in nREPL is 1024."
:type 'integer
:group 'cider
:package-version '(cider . "0.25.0"))
(defun cider--print-fn ()
"Return the value to send in the nrepl.middleware.print/print slot."
(pcase cider-print-fn
(`pr "cider.nrepl.pprint/pr")
(`pprint "cider.nrepl.pprint/pprint")
(`fipp "cider.nrepl.pprint/fipp-pprint")
(`puget "cider.nrepl.pprint/puget-pprint")
(`zprint "cider.nrepl.pprint/zprint-pprint")
(_ cider-print-fn)))
(defvar cider--print-options-mapping
'((right-margin
((fipp . width) (puget . width) (zprint . width)))
(length
((fipp . print-length) (puget . print-length) (zprint . max-length)))
(level
((fipp . print-level) (puget . print-level) (zprint . max-depth))))
"A mapping of print option for the various supported print engines.")
(defun cider--print-option (name printer)
"Convert the generic NAME to its PRINTER specific variant.
E.g. pprint's right-margin would become width for fipp.
The function is useful when you want to generate dynamically
print options.
NAME can be a string or a symbol. PRINTER has to be a symbol.
The result will be a string."
(let* ((name (cider-maybe-intern name))
(result (cdr (assoc printer (cadr (assoc name cider--print-options-mapping))))))
(symbol-name (or result name))))
(defun cider--nrepl-print-request-map (&optional right-margin)
"Map to merge into requests that require pretty-printing.
RIGHT-MARGIN specifies the maximum column-width of the printed result, and
is included in the request if non-nil."
(let* ((width-option (cider--print-option "right-margin" cider-print-fn))
(print-options (thread-last
(map-merge 'hash-table
`((,width-option ,right-margin))
cider-print-options)
(map-pairs)
(seq-mapcat #'identity)
(apply #'nrepl-dict))))
(map-merge 'list
`(("nrepl.middleware.print/stream?" "1"))
(when cider-print-fn
`(("nrepl.middleware.print/print" ,(cider--print-fn))))
(when cider-print-quota
`(("nrepl.middleware.print/quota" ,cider-print-quota)))
(when cider-print-buffer-size
`(("nrepl.middleware.print/buffer-size" ,cider-print-buffer-size)))
(unless (nrepl-dict-empty-p print-options)
`(("nrepl.middleware.print/options" ,print-options))))))
(defun cider--nrepl-pr-request-map ()
"Map to merge into requests that do not require pretty printing."
(let ((print-options (thread-last
cider-print-options
(map-pairs)
(seq-mapcat #'identity)
(apply #'nrepl-dict))))
(map-merge 'list
`(("nrepl.middleware.print/print" "cider.nrepl.pprint/pr")
("nrepl.middleware.print/stream?" nil))
(unless (nrepl-dict-empty-p print-options)
`(("nrepl.middleware.print/options" ,print-options)))
(when cider-print-quota
`(("nrepl.middleware.print/quota" ,cider-print-quota))))))
(defun cider--nrepl-content-type-map ()
"Map to be merged into an eval request to make it use content-types."
'(("content-type" "true")))
(defun cider-tooling-eval (input callback &optional ns connection)
"Send the request INPUT to CONNECTION and register the CALLBACK.
NS specifies the namespace in which to evaluate the request. Requests
evaluated in the tooling nREPL session don't affect the thread-local
bindings of the primary eval nREPL session (e.g. this is not going to
clobber *1/2/3)."
;; namespace forms are always evaluated in the "user" namespace
(nrepl-request:eval input
callback
(or connection (cider-current-repl nil 'ensure))
ns nil nil nil 'tooling))
(defun cider-sync-tooling-eval (input &optional ns connection)
"Send the request INPUT to CONNECTION and evaluate in synchronously.
NS specifies the namespace in which to evaluate the request. Requests
evaluated in the tooling nREPL session don't affect the thread-local
bindings of the primary eval nREPL session (e.g. this is not going to
clobber *1/2/3)."
;; namespace forms are always evaluated in the "user" namespace
(nrepl-sync-request:eval input
(or connection (cider-current-repl nil 'ensure))
ns
'tooling))
(defun cider-library-present-p (lib-ns)
"Check whether LIB-NS is present.
If a certain well-known ns in a library is present we assume that library
itself is present."
(nrepl-dict-get (cider-sync-tooling-eval (format "(require '%s)" lib-ns)) "value"))
;;; Interrupt evaluation
(defun cider-interrupt-handler (buffer)
"Create an interrupt response handler for BUFFER."
(nrepl-make-response-handler buffer nil nil nil nil))
(defun cider-interrupt ()
"Interrupt any pending evaluations."
(interactive)
;; FIXME: does this work correctly in cljc files?
(with-current-buffer (cider-current-repl nil 'ensure)
(let ((pending-request-ids (cider-util--hash-keys nrepl-pending-requests)))
(dolist (request-id pending-request-ids)
(nrepl-request:interrupt
request-id
(cider-interrupt-handler (current-buffer))
(cider-current-repl))))))
(defun cider-nrepl-eval-session ()
"Return the eval nREPL session id of the current connection."
(with-current-buffer (cider-current-repl)
nrepl-session))
(defun cider-nrepl-tooling-session ()
"Return the tooling nREPL session id of the current connection."
(with-current-buffer (cider-current-repl)
nrepl-tooling-session))
(defun cider--var-choice (var-info)
"Prompt to choose from among multiple VAR-INFO candidates, if required.
This is needed only when the symbol queried is an unqualified host platform
method, and multiple classes have a so-named member. If VAR-INFO does not
contain a `candidates' key, it is returned as is."
(let ((candidates (nrepl-dict-get var-info "candidates")))
(if candidates
(let* ((classes (nrepl-dict-keys candidates))
(choice (completing-read "Member in class: " classes nil t))
(info (nrepl-dict-get candidates choice)))
info)
var-info)))
;; FIXME: Now that nREPL supports a lookup op natively, we should
;; remove this eval-based hack at some point.
(defconst cider-info-form "
(do
(require 'clojure.java.io)
(require 'clojure.walk)
(if-let [var (resolve '%s)]
(let [info (meta var)]
(-> info
(update :ns str)
(update :name str)
(update :file (comp str clojure.java.io/resource))
(cond-> (:macro info) (update :macro str))
(cond-> (:special-form info) (update :special-form str))
(cond-> (:protocol info) (update :protocol str))
(cond-> (:arglists info) (update :arglists str))
(assoc :arglists-str (str (:arglists info)))
(clojure.walk/stringify-keys)))))
")
(defun cider-fallback-eval:info (var)
"Obtain VAR metadata via a regular eval.
Used only when the info nREPL middleware is not available."
(let* ((response (cider-sync-tooling-eval (format cider-info-form var)))
(var-info (nrepl-dict-from-hash (parseedn-read-str (nrepl-dict-get response "value")))))
var-info))
(defun cider-var-info (var &optional all)
"Return VAR's info as an alist with list cdrs.
When multiple matching vars are returned you'll be prompted to select one,
unless ALL is truthy."
(when (and var (not (string= var "")))
(let ((var-info (cond
((cider-nrepl-op-supported-p "info") (cider-sync-request:info var))
((cider-nrepl-op-supported-p "lookup") (cider-sync-request:lookup var))
(t (cider-fallback-eval:info var)))))
(if all var-info (cider--var-choice var-info)))))
(defun cider-member-info (class member)
"Return the CLASS MEMBER's info as an alist with list cdrs."
(when (and class member)
(cider-sync-request:info nil class member)))
;;; Requests
(declare-function cider-load-file-handler "cider-eval")
(defun cider-request:load-file (file-contents file-path file-name &optional connection callback)
"Perform the nREPL \"load-file\" op.
FILE-CONTENTS, FILE-PATH and FILE-NAME are details of the file to be
loaded. If CONNECTION is nil, use `cider-current-repl'. If CALLBACK
is nil, use `cider-load-file-handler'."
(cider-nrepl-send-request `("op" "load-file"
"file" ,file-contents
"file-path" ,file-path
"file-name" ,file-name)
(or callback
(cider-load-file-handler (current-buffer)))
connection))
;;; Sync Requests
(defcustom cider-filtered-namespaces-regexps
'("^cider.nrepl" "^refactor-nrepl" "^nrepl")
"List of regexps used to filter out some vars/symbols/namespaces.
When nil, nothing is filtered out. Otherwise, all namespaces matching any
regexp from this list are dropped out of the \"ns-list\" op. Also,
\"apropos\" won't include vars from such namespaces. This list is passed
on to the nREPL middleware without any pre-processing. So the regexps have
to be in Clojure format (with twice the number of backslashes) and not
Emacs Lisp."
:type '(repeat string)
:safe #'listp
:group 'cider
:package-version '(cider . "0.13.0"))
(defun cider-sync-request:apropos (query &optional search-ns docs-p privates-p case-sensitive-p)
"Send \"apropos\" request for regexp QUERY.
Optional arguments include SEARCH-NS, DOCS-P, PRIVATES-P, CASE-SENSITIVE-P."
(let* ((query (replace-regexp-in-string "[ \t]+" ".+" query))
(response (cider-nrepl-send-sync-request
`("op" "apropos"
"ns" ,(cider-current-ns)
"query" ,query
,@(when search-ns `("search-ns" ,search-ns))
,@(when docs-p '("docs?" "t"))
,@(when privates-p '("privates?" "t"))
,@(when case-sensitive-p '("case-sensitive?" "t"))
"exclude-regexps" ,cider-filtered-namespaces-regexps))))
(if (member "apropos-regexp-error" (nrepl-dict-get response "status"))
(user-error "Invalid regexp: %s" (nrepl-dict-get response "error-msg"))
(nrepl-dict-get response "apropos-matches"))))
(defun cider-sync-request:classpath ()
"Return a list of classpath entries."
(cider-ensure-op-supported "classpath")
(thread-first
'("op" "classpath")
(cider-nrepl-send-sync-request)
(nrepl-dict-get "classpath")))
(defun cider--get-abs-path (path project)
"Resolve PATH to an absolute path relative to PROJECT.
Do nothing if PATH is already absolute."
(if (not (file-name-absolute-p path))
(expand-file-name path project)
path))
(defun cider-fallback-eval:classpath ()
"Return a list of classpath entries using eval.
Sometimes the classpath contains entries like src/main and we need to
resolve those to absolute paths."
(when (cider-runtime-clojure-p)
(let ((classpath (thread-first
"(seq (.split (System/getProperty \"java.class.path\") \":\"))"
(cider-sync-tooling-eval)
(nrepl-dict-get "value")
read))
(project (clojure-project-dir)))
(mapcar (lambda (path) (cider--get-abs-path path project)) classpath))))
(defun cider-classpath-entries ()
"Return a list of classpath entries."
(seq-map #'expand-file-name ; normalize filenames for e.g. Windows
(if (cider-nrepl-op-supported-p "classpath")
(cider-sync-request:classpath)
(cider-fallback-eval:classpath))))
(defun cider-sync-request:completion (prefix)
"Return a list of completions for PREFIX using nREPL's \"completion\" op."
(when-let* ((dict (thread-first `("op" "completions"
"ns" ,(cider-current-ns)
"prefix" ,prefix)
(cider-nrepl-send-sync-request (cider-current-repl)
'abort-on-input))))
(nrepl-dict-get dict "completions")))
(defun cider-sync-request:complete (prefix context)
"Return a list of completions for PREFIX using nREPL's \"complete\" op.
CONTEXT represents a completion context for compliment."
(when-let* ((dict (thread-first `("op" "complete"
"ns" ,(cider-current-ns)
"prefix" ,prefix
"context" ,context
,@(when cider-enhanced-cljs-completion-p '("enhanced-cljs-completion?" "t")))
(cider-nrepl-send-sync-request (cider-current-repl)
'abort-on-input))))
(nrepl-dict-get dict "completions")))
(defun cider-sync-request:complete-flush-caches ()
"Send \"complete-flush-caches\" op to flush Compliment's caches."
(cider-nrepl-send-sync-request (list "op" "complete-flush-caches"
"session" (cider-nrepl-eval-session))
nil
'abort-on-input))
(defun cider-sync-request:info (symbol &optional class member)
"Send \"info\" op with parameters SYMBOL or CLASS and MEMBER."
(let ((var-info (thread-first `("op" "info"
"ns" ,(cider-current-ns)
,@(when symbol `("sym" ,symbol))
,@(when class `("class" ,class))
,@(when member `("member" ,member)))
(cider-nrepl-send-sync-request (cider-current-repl)))))
(if (member "no-info" (nrepl-dict-get var-info "status"))
nil
var-info)))
(defun cider-sync-request:lookup (symbol &optional lookup-fn)
"Send \"lookup\" op request with parameters SYMBOL and LOOKUP-FN."
(let ((var-info (thread-first `("op" "lookup"
"ns" ,(cider-current-ns)
,@(when symbol `("sym" ,symbol))
,@(when lookup-fn `("lookup-fn" ,lookup-fn)))
(cider-nrepl-send-sync-request (cider-current-repl)))))
(if (member "lookup-error" (nrepl-dict-get var-info "status"))
nil
(nrepl-dict-get var-info "info"))))
(defun cider-sync-request:eldoc (symbol &optional class member)
"Send \"eldoc\" op with parameters SYMBOL or CLASS and MEMBER."
(when-let* ((eldoc (thread-first `("op" "eldoc"
"ns" ,(cider-current-ns)
,@(when symbol `("sym" ,symbol))
,@(when class `("class" ,class))
,@(when member `("member" ,member)))
(cider-nrepl-send-sync-request (cider-current-repl)
'abort-on-input))))
(if (member "no-eldoc" (nrepl-dict-get eldoc "status"))
nil
eldoc)))
(defun cider-sync-request:eldoc-datomic-query (symbol)
"Send \"eldoc-datomic-query\" op with parameter SYMBOL."
(when-let* ((eldoc (thread-first `("op" "eldoc-datomic-query"
"ns" ,(cider-current-ns)
,@(when symbol `("sym" ,symbol)))
(cider-nrepl-send-sync-request nil 'abort-on-input))))
(if (member "no-eldoc" (nrepl-dict-get eldoc "status"))
nil
eldoc)))
(defun cider-sync-request:spec-list (&optional filter-regex)
"Get a list of the available specs in the registry.
Optional argument FILTER-REGEX filters specs. By default, all specs are
returned."
(setq filter-regex (or filter-regex ""))
(thread-first `("op" "spec-list"
"filter-regex" ,filter-regex
"ns" ,(cider-current-ns))
(cider-nrepl-send-sync-request)
(nrepl-dict-get "spec-list")))
(defun cider-sync-request:spec-form (spec)
"Get SPEC's form from registry."
(thread-first `("op" "spec-form"
"spec-name" ,spec
"ns" ,(cider-current-ns))
(cider-nrepl-send-sync-request)
(nrepl-dict-get "spec-form")))
(defun cider-sync-request:spec-example (spec)
"Get an example for SPEC."
(thread-first `("op" "spec-example"
"spec-name" ,spec)
(cider-nrepl-send-sync-request)
(nrepl-dict-get "spec-example")))
(defun cider-sync-request:ns-list ()
"Get a list of the available namespaces."
(thread-first `("op" "ns-list"
"exclude-regexps" ,cider-filtered-namespaces-regexps)
(cider-nrepl-send-sync-request)
(nrepl-dict-get "ns-list")))
(defun cider-sync-request:ns-vars (ns)
"Get a list of the vars in NS."
(thread-first `("op" "ns-vars"
"ns" ,ns)
(cider-nrepl-send-sync-request)
(nrepl-dict-get "ns-vars")))
(defun cider-sync-request:ns-path (ns)
"Get the path to the file containing NS."
(thread-first `("op" "ns-path"
"ns" ,ns)
(cider-nrepl-send-sync-request)
(nrepl-dict-get "path")))
(defun cider-sync-request:ns-vars-with-meta (ns)
"Get a map of the vars in NS to its metadata information."
(thread-first `("op" "ns-vars-with-meta"
"ns" ,ns)
(cider-nrepl-send-sync-request)
(nrepl-dict-get "ns-vars-with-meta")))
(defun cider-sync-request:private-ns-vars-with-meta (ns)
"Get a map of the vars in NS to its metadata information."
(thread-first `("op" "ns-vars-with-meta"
"ns" ,ns
"var-query" ,(nrepl-dict "private?" "t"
"include-meta-key" '("private")))
(cider-nrepl-send-sync-request)
(nrepl-dict-get "ns-vars-with-meta")))
(defun cider-sync-request:ns-load-all ()
"Load all project namespaces."
(thread-first '("op" "ns-load-all")
(cider-nrepl-send-sync-request)
(nrepl-dict-get "loaded-ns")))
(defun cider-sync-request:resource (name)
"Perform nREPL \"resource\" op with resource name NAME."
(thread-first `("op" "resource"
"name" ,name)
(cider-nrepl-send-sync-request)
(nrepl-dict-get "resource-path")))
(defun cider-sync-request:resources-list ()
"Return a list of all resources on the classpath.
The result entries are relative to the classpath."
(when-let* ((resources (thread-first '("op" "resources-list")
(cider-nrepl-send-sync-request)
(nrepl-dict-get "resources-list"))))
(seq-map (lambda (resource) (nrepl-dict-get resource "relpath")) resources)))
(defun cider-sync-request:fn-refs (ns sym)
"Return a list of functions that reference the function identified by NS and SYM."
(cider-ensure-op-supported "fn-refs")
(thread-first `("op" "fn-refs"
"ns" ,ns
"sym" ,sym)
(cider-nrepl-send-sync-request)
(nrepl-dict-get "fn-refs")))
(defun cider-sync-request:fn-deps (ns sym)
"Return a list of function deps for the function identified by NS and SYM."
(cider-ensure-op-supported "fn-deps")
(thread-first `("op" "fn-deps"
"ns" ,ns
"sym" ,sym)
(cider-nrepl-send-sync-request)
(nrepl-dict-get "fn-deps")))
(defun cider-sync-request:format-code (code &optional format-options)
"Perform nREPL \"format-code\" op with CODE.
FORMAT-OPTIONS is an optional configuration map for cljfmt."
(let* ((request `("op" "format-code"
"options" ,(cider--nrepl-format-code-request-map format-options)
"code" ,code))
(response (cider-nrepl-send-sync-request request))
(err (nrepl-dict-get response "err")))
(when err
;; err will be a stacktrace with a first line that looks like:
;; "clojure.lang.ExceptionInfo: Unmatched delimiter ]"
(error (car (split-string err "\n"))))
(nrepl-dict-get response "formatted-code")))
(defun cider-sync-request:format-edn (edn right-margin)
"Perform \"format-edn\" op with EDN and RIGHT-MARGIN."
(let* ((request (thread-last
(map-merge 'list
`(("op" "format-edn")
("edn" ,edn))
(cider--nrepl-print-request-map right-margin))
(seq-mapcat #'identity)))
(response (cider-nrepl-send-sync-request request))
(err (nrepl-dict-get response "err")))
(when err
;; err will be a stacktrace with a first line that looks like:
;; "clojure.lang.ExceptionInfo: Unmatched delimiter ]"
(error (car (split-string err "\n"))))
(nrepl-dict-get response "formatted-edn")))
;;; Dealing with input
;; TODO: Replace this with some nil handler.
(defun cider-stdin-handler (&optional _buffer)
"Make a stdin response handler for _BUFFER."
(nrepl-make-response-handler (current-buffer)
(lambda (_buffer _value))
(lambda (_buffer _out))
(lambda (_buffer _err))
nil))
(defun cider-need-input (buffer)
"Handle an need-input request from BUFFER."
(with-current-buffer buffer
(let ((map (make-sparse-keymap)))
(set-keymap-parent map minibuffer-local-map)
(define-key map (kbd "C-c C-c") #'abort-recursive-edit)
(let ((stdin (condition-case nil
(concat (read-from-minibuffer "Stdin: " nil map) "\n")
(quit nil))))
(nrepl-request:stdin stdin
(cider-stdin-handler buffer)
(cider-current-repl))))))
(provide 'cider-client)
;;; cider-client.el ends here
;;; cider-clojuredocs.el --- ClojureDocs integration -*- lexical-binding: t -*-
;; Copyright © 2014-2022 Bozhidar Batsov and CIDER contributors
;;
;; Author: Bozhidar Batsov <bozhidar@batsov.dev>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; A few commands for ClojureDocs documentation lookup.
;;; Code:
(require 'cider-client)
(require 'cider-common)
(require 'subr-x)
(require 'cider-popup)
(require 'nrepl-dict)
(require 'url-vars)
(defconst cider-clojuredocs-url "https://clojuredocs.org/")
(defconst cider-clojuredocs-buffer "*cider-clojuredocs*")
(defun cider-sync-request:clojuredocs-lookup (ns sym)
"Perform nREPL \"resource\" op with NS and SYM."
(thread-first `("op" "clojuredocs-lookup"
"ns" ,ns
"sym" ,sym)
(cider-nrepl-send-sync-request)
(nrepl-dict-get "clojuredocs")))
(defun cider-sync-request:clojuredocs-refresh ()
"Refresh the ClojureDocs cache."
(thread-first '("op" "clojuredocs-refresh-cache")
(cider-nrepl-send-sync-request)
(nrepl-dict-get "status")))
(defun cider-clojuredocs-replace-special (name)
"Convert the dashes in NAME to a ClojureDocs friendly format.
We need to handle \"?\", \".\", \"..\" and \"/\"."
(thread-last
name
(replace-regexp-in-string "\\?" "_q")
(replace-regexp-in-string "\\(\\.+\\)" "_\\1")
(replace-regexp-in-string "/" "fs")))
(defun cider-clojuredocs-url (name ns)
"Generate a ClojureDocs url from NAME and NS."
(let ((base-url cider-clojuredocs-url))
(when (and name ns)
(concat base-url ns "/" (cider-clojuredocs-replace-special name)))))
(defun cider-clojuredocs-web-lookup (sym)
"Open the ClojureDocs documentation for SYM in a web browser."
(if-let* ((var-info (cider-var-info sym)))
(let ((name (nrepl-dict-get var-info "name"))
(ns (nrepl-dict-get var-info "ns")))
(browse-url (cider-clojuredocs-url name ns)))
(error "Symbol %s not resolved" sym)))
;;;###autoload
(defun cider-clojuredocs-web (&optional arg)
"Open ClojureDocs documentation in the default web browser.
Prompts for the symbol to use, or uses the symbol at point, depending on
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
opposite of what that option dictates."
(interactive "P")
(funcall (cider-prompt-for-symbol-function arg)
"ClojureDocs doc for"
#'cider-clojuredocs-web-lookup))
;;;###autoload
(defun cider-clojuredocs-refresh-cache ()
"Refresh the ClojureDocs cache."
(interactive)
(let ((result (cider-sync-request:clojuredocs-refresh)))
(if (member "ok" result)
(message "ClojureDocs cache refreshed successfully")
(message "An error occurred while trying to refresh the ClojureDocs cache"))))
(defun cider-create-clojuredocs-buffer (content)
"Create a new ClojureDocs buffer with CONTENT."
(with-current-buffer (cider-popup-buffer cider-clojuredocs-buffer t)
(read-only-mode -1)
(set-syntax-table clojure-mode-syntax-table)
(local-set-key (kbd "C-c C-d C-c") 'cider-clojuredocs)
(insert content)
(cider-popup-buffer-mode 1)
(view-mode 1)
(goto-char (point-min))
(current-buffer)))
(defun cider-clojuredocs--content (dict)
"Generate a nice string from DICT."
(with-temp-buffer
(insert "= " (nrepl-dict-get dict "ns") "/" (nrepl-dict-get dict "name") "\n\n")
(let ((arglists (nrepl-dict-get dict "arglists")))
(dolist (arglist arglists)
(insert (format " [%s]\n" arglist)))
(insert "\n")
(insert (nrepl-dict-get dict "doc"))
(insert "\n"))
(insert "\n== See Also\n\n")
(if-let ((see-alsos (nrepl-dict-get dict "see-alsos")))
(dolist (see-also see-alsos)
(insert-text-button (format "* %s\n" see-also)
'sym see-also
'action (lambda (btn)
(cider-clojuredocs-lookup (button-get btn 'sym)))
'help-echo (format "Press Enter or middle click to jump to %s" see-also)))
(insert "Not available\n"))
(insert "\n== Examples\n\n")
(if-let ((examples (nrepl-dict-get dict "examples")))
(dolist (example examples)
(insert (cider-font-lock-as-clojure example))
(insert "\n-------------------------------------------------\n"))
(insert "Not available\n"))
(insert "\n== Notes\n\n")
(if-let ((notes (nrepl-dict-get dict "notes")))
(dolist (note notes)
(insert note)
(insert "\n-------------------------------------------------\n"))
(insert "Not available\n"))
(buffer-string)))
(defun cider-clojuredocs-lookup (sym)
"Look up the ClojureDocs documentation for SYM."
(let ((docs (cider-sync-request:clojuredocs-lookup (cider-current-ns) sym)))
(pop-to-buffer (cider-create-clojuredocs-buffer (cider-clojuredocs--content docs)))
;; highlight the symbol in question in the docs buffer
(highlight-regexp
(regexp-quote
(or (cadr (split-string sym "/"))
sym))
'bold)))
;;;###autoload
(defun cider-clojuredocs (&optional arg)
"Open ClojureDocs documentation in a popup buffer.
Prompts for the symbol to use, or uses the symbol at point, depending on
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
opposite of what that option dictates."
(interactive "P")
(when (derived-mode-p 'clojurescript-mode)
(user-error "`cider-clojuredocs' doesn't support ClojureScript"))
(funcall (cider-prompt-for-symbol-function arg)
"ClojureDocs doc for"
#'cider-clojuredocs-lookup))
(provide 'cider-clojuredocs)
;;; cider-clojuredocs.el ends here
;;; cider-common.el --- Common use functions -*- lexical-binding: t; -*-
;; Copyright © 2015-2022 Artur Malabarba
;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Common functions that are useful in both Clojure buffers and REPL
;; buffers.
;;; Code:
(require 'subr-x)
(require 'nrepl-dict)
(require 'cider-util)
(require 'xref)
(require 'tramp)
(defcustom cider-prompt-for-symbol nil
"Controls when to prompt for symbol when a command requires one.
When non-nil, always prompt, and use the symbol at point as the default
value at the prompt.
When nil, attempt to use the symbol at point for the command, and only
prompt if that throws an error."
:type '(choice (const :tag "always" t)
(const :tag "dwim" nil))
:group 'cider
:package-version '(cider . "0.9.0"))
(defcustom cider-special-mode-truncate-lines t
"If non-nil, contents of CIDER's special buffers will be line-truncated.
Should be set before loading CIDER."
:type 'boolean
:group 'cider
:package-version '(cider . "0.15.0"))
(defun cider--should-prompt-for-symbol (&optional invert)
"Return the value of the variable `cider-prompt-for-symbol'.
Optionally invert the value, if INVERT is truthy."
(if invert (not cider-prompt-for-symbol) cider-prompt-for-symbol))
(defun cider-prompt-for-symbol-function (&optional invert)
"Prompt for symbol if funcall `cider--should-prompt-for-symbol' is truthy.
Otherwise attempt to use the symbol at point for the command, and only
prompt if that throws an error.
INVERT inverts the semantics of the function `cider--should-prompt-for-symbol'."
(if (cider--should-prompt-for-symbol invert)
#'cider-read-symbol-name
#'cider-try-symbol-at-point))
(defun cider--kw-to-symbol (kw)
"Convert the keyword KW to a symbol."
(when kw
(replace-regexp-in-string "\\`:+" "" kw)))
;;; Minibuffer
(defvar cider-minibuffer-history '()
"History list of expressions read from the minibuffer.")
(defvar cider-minibuffer-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map minibuffer-local-map)
(define-key map (kbd "TAB") #'complete-symbol)
(define-key map (kbd "M-TAB") #'complete-symbol)
map)
"Minibuffer keymap used for reading Clojure expressions.")
(declare-function cider-complete-at-point "cider-completion")
(declare-function cider-eldoc "cider-eldoc")
(defun cider-read-from-minibuffer (prompt &optional value)
"Read a string from the minibuffer, prompting with PROMPT.
If VALUE is non-nil, it is inserted into the minibuffer as initial-input.
PROMPT need not end with \": \". If it doesn't, VALUE is displayed on the
prompt as a default value (used if the user doesn't type anything) and is
not used as initial input (input is left empty)."
(minibuffer-with-setup-hook
(lambda ()
(set-syntax-table clojure-mode-syntax-table)
(add-hook 'completion-at-point-functions
#'cider-complete-at-point nil t)
(setq-local eldoc-documentation-function #'cider-eldoc)
(run-hooks 'eval-expression-minibuffer-setup-hook))
(let* ((has-colon (string-match ": \\'" prompt))
(input (read-from-minibuffer (cond
(has-colon prompt)
(value (format "%s (default %s): " prompt value))
(t (format "%s: " prompt)))
(when has-colon value) ; initial-input
cider-minibuffer-map nil
'cider-minibuffer-history
(unless has-colon value)))) ; default-value
(if (and (equal input "") value (not has-colon))
value
input))))
(defun cider-read-symbol-name (prompt callback)
"Read a symbol name using PROMPT with a default of the one at point.
Use CALLBACK as the completing read var callback."
(funcall callback (cider-read-from-minibuffer
prompt
;; if the thing at point is a keyword we treat it as symbol
(cider--kw-to-symbol (cider-symbol-at-point 'look-back)))))
(defun cider-try-symbol-at-point (prompt callback)
"Call CALLBACK with symbol at point.
On failure, read a symbol name using PROMPT and call CALLBACK with that."
(condition-case nil (funcall callback (cider--kw-to-symbol (cider-symbol-at-point 'look-back)))
('error (funcall callback (cider-read-from-minibuffer prompt)))))
(declare-function cider-mode "cider-mode")
(defcustom cider-jump-to-pop-to-buffer-actions
'((display-buffer-reuse-window display-buffer-same-window))
"Determines what window `cider-jump-to` uses.
The value is passed as the `action` argument to `pop-to-buffer`.
The default value means:
- If the target file is already visible in a window, reuse it (switch to it).
- Otherwise, open the target buffer in the current window.
For further details, see https://docs.cider.mx/cider/config/basic_config.html#control-what-window-to-use-when-jumping-to-a-definition"
:type 'sexp
:group 'cider
:package-version '(cider . "0.24.0"))
(defun cider-jump-to (buffer &optional pos other-window)
"Push current point onto marker ring, and jump to BUFFER and POS.
POS can be either a number, a cons, or a symbol.
If a number, it is the character position (the point).
If a cons, it specifies the position as (LINE . COLUMN). COLUMN can be nil.
If a symbol, `cider-jump-to' searches for something that looks like the
symbol's definition in the file.
If OTHER-WINDOW is non-nil don't reuse current window."
(with-no-warnings
(xref-push-marker-stack))
(if other-window
(pop-to-buffer buffer 'display-buffer-pop-up-window)
(pop-to-buffer buffer cider-jump-to-pop-to-buffer-actions))
(with-current-buffer buffer
(widen)
(goto-char (point-min))
(cider-mode +1)
(let ((status
(cond
;; Line-column specification.
((consp pos)
(forward-line (1- (or (car pos) 1)))
(if (cdr pos)
(move-to-column (cdr pos))
(back-to-indentation)))
;; Point specification.
((numberp pos)
(goto-char pos))
;; Symbol or string.
(pos
;; Try to find (def full-name ...).
(if (or (save-excursion
(search-forward-regexp (format "(def.*\\s-\\(%s\\)" (regexp-quote pos))
nil 'noerror))
(let ((name (replace-regexp-in-string ".*/" "" pos)))
;; Try to find (def name ...).
(or (save-excursion
(search-forward-regexp (format "(def.*\\s-\\(%s\\)" (regexp-quote name))
nil 'noerror))
;; Last resort, just find the first occurrence of `name'.
(save-excursion
(search-forward name nil 'noerror)))))
(goto-char (match-beginning 0))
(message "Can't find %s in %s" pos (buffer-file-name))
'not-found))
(t 'not-found))))
(unless (eq status 'not-found)
;; Make sure the location we jump to is centered within the target window
(recenter)))))
(defun cider--find-buffer-for-file (file)
"Return a buffer visiting FILE.
If FILE is a temp buffer name, return that buffer."
(if (string-prefix-p "*" file)
file
(and file
(not (cider--tooling-file-p file))
(cider-find-file file))))
(defun cider--jump-to-loc-from-info (info &optional other-window)
"Jump to location give by INFO.
INFO object is returned by `cider-var-info' or `cider-member-info'.
OTHER-WINDOW is passed to `cider-jump-to'."
(let* ((line (nrepl-dict-get info "line"))
(file (nrepl-dict-get info "file"))
(name (nrepl-dict-get info "name"))
;; the filename might actually be a REPL buffer name
(buffer (cider--find-buffer-for-file file)))
(if buffer
(cider-jump-to buffer (if line (cons line nil) name) other-window)
(error "No source location"))))
(declare-function url-filename "url-parse" (cl-x) t)
(defun cider--url-to-file (url)
"Return the filename from the resource URL.
Uses `url-generic-parse-url' to parse the url. The filename is extracted and
then url decoded. If the decoded filename has a Windows device letter followed
by a colon immediately after the leading '/' then the leading '/' is dropped to
create a valid path."
(let ((filename (url-unhex-string (url-filename (url-generic-parse-url url)))))
(if (string-match "^/\\([a-zA-Z]:/.*\\)" filename)
(match-string 1 filename)
filename)))
(defun cider-make-tramp-prefix (method user host)
"Constructs a Tramp file prefix from METHOD, USER, HOST.
It originated from Tramp's `tramp-make-tramp-file-name'. The original be
forced to make full file name with `with-parsed-tramp-file-name', not providing
prefix only option."
(concat tramp-prefix-format
(unless (zerop (length method))
(concat method tramp-postfix-method-format))
(unless (zerop (length user))
(concat user tramp-postfix-user-format))
(when host
(if (string-match tramp-ipv6-regexp host)
(concat tramp-prefix-ipv6-format host tramp-postfix-ipv6-format)
host))
tramp-postfix-host-format))
(defun cider-tramp-prefix (&optional buffer)
"Use the filename for BUFFER to determine a tramp prefix.
Defaults to the current buffer. Return the tramp prefix, or nil
if BUFFER is local."
(let* ((buffer (or buffer (current-buffer)))
(name (or (buffer-file-name buffer)
(with-current-buffer buffer
default-directory))))
(when (tramp-tramp-file-p name)
(with-parsed-tramp-file-name name v
(with-no-warnings
(cider-make-tramp-prefix v-method v-user v-host))))))
(defun cider--client-tramp-filename (name &optional buffer)
"Return the tramp filename for path NAME relative to BUFFER.
If BUFFER has a tramp prefix, it will be added as a prefix to NAME.
If the resulting path is an existing tramp file, it returns the path,
otherwise, nil."
(let* ((buffer (or buffer (current-buffer)))
(name (replace-regexp-in-string "^file:" "" name))
(name (concat (cider-tramp-prefix buffer) name)))
(if (and (tramp-tramp-file-p name)
(tramp-handle-file-exists-p name))
name)))
(defun cider--server-filename (name)
"Return the nREPL server-relative filename for NAME."
(if (tramp-tramp-file-p name)
(with-parsed-tramp-file-name name nil
localname)
name))
(defcustom cider-path-translations nil
"Alist of path prefixes to path prefixes.
Useful to intercept the location of a path in a container (or virtual
machine) and translate to the original location. If your project is located
at \"~/projects/foo\" and the src directory of foo is mounted at \"/src\"
in the container, the alist would be `((\"/src\" \"~/projects/foo/src\"))."
:type '(alist :key-type string :value-type string)
:group 'cider
:package-version '(cider . "0.23.0"))
(defun cider--translate-path (path direction)
"Attempt to translate the PATH in the given DIRECTION.
Looks at `cider-path-translations' for (container . host) alist of path
prefixes and translates PATH from container to host or vice-versa depending on
whether DIRECTION is 'from-nrepl or 'to-nrepl."
(seq-let [from-fn to-fn path-fn] (cond ((eq direction 'from-nrepl) '(car cdr identity))
((eq direction 'to-nrepl) '(cdr car expand-file-name)))
(let ((path (funcall path-fn path)))
(seq-some (lambda (translation)
(let ((prefix (file-name-as-directory (expand-file-name (funcall from-fn translation)))))
(when (string-prefix-p prefix path)
(replace-regexp-in-string (format "^%s" (regexp-quote prefix))
(file-name-as-directory
(expand-file-name (funcall to-fn translation)))
path))))
cider-path-translations))))
(defun cider--translate-path-from-nrepl (path)
"Attempt to translate the nREPL PATH to a local path."
(cider--translate-path path 'from-nrepl))
(defun cider--translate-path-to-nrepl (path)
"Attempt to translate the local PATH to an nREPL path."
(cider--translate-path (expand-file-name path) 'to-nrepl))
(defvar cider-from-nrepl-filename-function
(with-no-warnings
(lambda (path)
(let ((path* (if (eq system-type 'cygwin)
(cygwin-convert-file-name-from-windows path)
path)))
(or (cider--translate-path-from-nrepl path*) path*))))
"Function to translate nREPL namestrings to Emacs filenames.")
(defcustom cider-prefer-local-resources nil
"Prefer local resources to remote (tramp) ones when both are available."
:type 'boolean
:group 'cider)
(defun cider--file-path (path)
"Return PATH's local or tramp path using `cider-prefer-local-resources'.
If no local or remote file exists, return nil."
(let* ((local-path (funcall cider-from-nrepl-filename-function path))
(tramp-path (and local-path (cider--client-tramp-filename local-path))))
(cond ((equal local-path "") "")
((and cider-prefer-local-resources (file-exists-p local-path))
local-path)
((and tramp-path (file-exists-p tramp-path))
tramp-path)
((and local-path (file-exists-p local-path))
local-path))))
(declare-function archive-extract "arc-mode")
(declare-function archive-zip-extract "arc-mode")
(defun cider-find-file (url)
"Return a buffer visiting the file URL if it exists, or nil otherwise.
If URL has a scheme prefix, it must represent a fully-qualified file path
or an entry within a zip/jar archive. If AVFS (archive virtual file
system; see online docs) is mounted the archive entry is opened inside the
AVFS directory, otherwise the entry is archived into a temporary read-only
buffer. If URL doesn't contain a scheme prefix and is an absolute path, it
is treated as such. Finally, if URL is relative, it is expanded within each
of the open Clojure buffers till an existing file ending with URL has been
found."
(require 'arc-mode)
(cond ((string-match "^file:\\(.+\\)" url)
(when-let* ((file (cider--url-to-file (match-string 1 url)))
(path (cider--file-path file)))
(find-file-noselect path)))
((string-match "^\\(jar\\|zip\\):\\(file:.+\\)!/\\(.+\\)" url)
(when-let* ((entry (match-string 3 url))
(file (cider--url-to-file (match-string 2 url)))
(path (cider--file-path file))
(name (format "%s:%s" path entry))
(avfs (format "%s%s#uzip/%s"
(expand-file-name (or (getenv "AVFSBASE") "~/.avfs/"))
path entry)))
(cond
;; 1) use avfs
((file-exists-p avfs)
(find-file-noselect avfs))
;; 2) already uncompressed
((find-buffer-visiting name))
;; 3) on remotes use Emacs built-in archiving
((tramp-tramp-file-p path)
(find-file path)
(goto-char (point-min))
;; anchor to eol to prevent eg. clj matching cljs.
(re-search-forward (concat entry "$"))
(let ((archive-buffer (current-buffer)))
(archive-extract)
(kill-buffer archive-buffer))
(current-buffer))
;; 4) Use external zip program to extract a single file
(t
(with-current-buffer (generate-new-buffer
(file-name-nondirectory entry))
;; Use appropriate coding system for bytes read from unzip cmd to
;; display Emacs native newlines regardless of whether the file
;; uses unix LF or dos CRLF line endings.
;; It's important to avoid spurious CR characters, which may
;; appear as `^M', because they can confuse clojure-mode's symbol
;; detection, e.g. `clojure-find-ns', and break `cider-find-var'.
;; `clojure-find-ns' uses Emacs' (thing-at-point 'symbol) as
;; part of identifying a file's namespace, and when a file
;; isn't decoded properly, namespaces can be reported as
;; `my.lib^M' which `cider-find-var' won't know what to do with.
(let ((coding-system-for-read 'prefer-utf-8))
(archive-zip-extract path entry))
(set-visited-file-name name)
(setq-local default-directory (file-name-directory path))
(setq-local buffer-read-only t)
(set-buffer-modified-p nil)
(set-auto-mode)
(current-buffer))))))
(t (if-let* ((path (cider--file-path url)))
(find-file-noselect path)
(unless (file-name-absolute-p url)
(let ((cider-buffers (cider-util--clojure-buffers))
(url (file-name-nondirectory url)))
(or (cl-loop for bf in cider-buffers
for path = (with-current-buffer bf
(expand-file-name url))
if (and path (file-exists-p path))
return (find-file-noselect path))
(cl-loop for bf in cider-buffers
if (string= (buffer-name bf) url)
return bf))))))))
(defun cider--open-other-window-p (arg)
"Test prefix value ARG to see if it indicates displaying results in other window."
(let ((narg (prefix-numeric-value arg)))
(pcase narg
(-1 t) ; -
(16 t) ; empty empty
(_ nil))))
(defun cider-abbreviate-ns (namespace)
"Return a string that abbreviates NAMESPACE."
(when namespace
(let* ((names (reverse (split-string namespace "\\.")))
(lastname (car names)))
(concat (mapconcat (lambda (s) (concat (substring s 0 1) "."))
(reverse (cdr names))
"")
lastname))))
(defun cider-last-ns-segment (namespace)
"Return the last segment of NAMESPACE."
(when namespace
(car (reverse (split-string namespace "\\.")))))
(provide 'cider-common)
;;; cider-common.el ends here
;;; cider-completion.el --- Smart REPL-powered code completion -*- lexical-binding: t -*-
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Smart REPL-powered code completion and integration with company-mode.
;;; Code:
(require 'subr-x)
(require 'thingatpt)
(require 'cider-client)
(require 'cider-common)
(require 'cider-doc)
(require 'cider-eldoc)
(require 'nrepl-dict)
(defcustom cider-completion-use-context t
"When true, uses context at point to improve completion suggestions."
:type 'boolean
:group 'cider
:package-version '(cider . "0.7.0"))
(defcustom cider-annotate-completion-candidates t
"When true, annotate completion candidates with some extra information."
:type 'boolean
:group 'cider
:package-version '(cider . "0.8.0"))
(defcustom cider-annotate-completion-function
#'cider-default-annotate-completion-function
"Controls how the annotations for completion candidates are formatted.
Must be a function that takes two arguments: the abbreviation of the
candidate type according to `cider-completion-annotations-alist' and the
candidate's namespace."
:type 'function
:group 'cider
:package-version '(cider . "0.9.0"))
(defcustom cider-completion-annotations-alist
'(("class" "c")
("field" "fi")
("function" "f")
("import" "i")
("keyword" "k")
("local" "l")
("macro" "m")
("method" "me")
("namespace" "n")
("protocol" "p")
("protocol-function" "pf")
("record" "r")
("special-form" "s")
("static-field" "sf")
("static-method" "sm")
("type" "t")
("var" "v"))
"Controls the abbreviations used when annotating completion candidates.
Must be a list of elements with the form (TYPE . ABBREVIATION), where TYPE
is a possible value of the candidate's type returned from the completion
backend, and ABBREVIATION is a short form of that type."
:type '(alist :key-type string :value-type string)
:group 'cider
:package-version '(cider . "0.9.0"))
(defconst cider-completion-kind-alist
'(("class" class)
("field" field)
("function" function)
("import" class)
("keyword" keyword)
("local" variable)
("macro" macro)
("method" method)
("namespace" module)
("protocol" enum)
("protocol-function" enum-member)
("record" struct)
("special-form" keyword)
("static-field" field)
("static-method" interface)
("type" parameter)
("var" variable))
"Icon mapping for company-mode.")
(defcustom cider-completion-annotations-include-ns 'unqualified
"Controls passing of namespaces to `cider-annotate-completion-function'.
When set to 'always, the candidate's namespace will always be passed if it
is available. When set to 'unqualified, the namespace will only be passed
if the candidate is not namespace-qualified."
:type '(choice (const always)
(const unqualified)
(const :tag "never" nil))
:group 'cider
:package-version '(cider . "0.9.0"))
(defvar cider-completion-last-context nil)
(defun cider-completion-symbol-start-pos ()
"Find the starting position of the symbol at point, unless inside a string."
(let ((sap (symbol-at-point)))
(when (and sap (not (nth 3 (syntax-ppss))))
(car (bounds-of-thing-at-point 'symbol)))))
(defun cider-completion-get-context-at-point ()
"Extract the context at point.
If point is not inside the list, returns nil; otherwise return \"top-level\"
form, with symbol at point replaced by __prefix__."
(when (save-excursion
(condition-case _
(progn
(up-list)
(check-parens)
t)
(scan-error nil)
(user-error nil)))
(save-excursion
(let* ((pref-end (point))
(pref-start (cider-completion-symbol-start-pos))
(context (cider-defun-at-point))
(_ (beginning-of-defun))
(expr-start (point)))
(concat (when pref-start (substring context 0 (- pref-start expr-start)))
"__prefix__"
(substring context (- pref-end expr-start)))))))
(defun cider-completion-get-context ()
"Extract context depending on `cider-completion-use-context' and major mode."
(let ((context (if (and cider-completion-use-context
;; Important because `beginning-of-defun' and
;; `ending-of-defun' work incorrectly in the REPL
;; buffer, so context extraction fails there.
(derived-mode-p 'clojure-mode))
;; We use ignore-errors here since grabbing the context
;; might fail because of unbalanced parens, or other
;; technical reasons, yet we don't want to lose all
;; completions and throw error to user because of that.
(or (ignore-errors (cider-completion-get-context-at-point))
"nil")
"nil")))
(if (string= cider-completion-last-context context)
":same"
(setq cider-completion-last-context context)
context)))
(defun cider-completion--parse-candidate-map (candidate-map)
"Get \"candidate\" from CANDIDATE-MAP.
Put type and ns properties on the candidate"
(let ((candidate (nrepl-dict-get candidate-map "candidate"))
(type (nrepl-dict-get candidate-map "type"))
(ns (nrepl-dict-get candidate-map "ns")))
(put-text-property 0 1 'type type candidate)
(put-text-property 0 1 'ns ns candidate)
candidate))
(defun cider-complete (prefix)
"Complete PREFIX with context at point.
Completion relies on nREPL middleware. First
we check if cider-nrepl's complete op is available
and afterward we fallback on nREPL's built-in
completion functionality."
(cond
;; if we don't have a connection, end early
((not (cider-connected-p)) nil)
;; next we try if cider-nrepl's completion is available
((cider-nrepl-op-supported-p "complete")
(let* ((context (cider-completion-get-context))
(candidates (cider-sync-request:complete prefix context)))
(mapcar #'cider-completion--parse-candidate-map candidates)))
;; then we fallback to nREPL's built-in op (available in nREPL 0.8+)
((cider-nrepl-op-supported-p "completions")
(mapcar #'cider-completion--parse-candidate-map (cider-sync-request:completion prefix)))
(t nil)))
(defun cider-completion--get-candidate-type (symbol)
"Get candidate type for SYMBOL."
(let ((type (get-text-property 0 'type symbol)))
(or (cadr (assoc type cider-completion-annotations-alist))
type)))
(defun cider-completion--get-candidate-ns (symbol)
"Get candidate ns for SYMBOL."
(when (or (eq 'always cider-completion-annotations-include-ns)
(and (eq 'unqualified cider-completion-annotations-include-ns)
(not (cider-namespace-qualified-p symbol))))
(get-text-property 0 'ns symbol)))
(defun cider-default-annotate-completion-function (type ns)
"Get completion function based on TYPE and NS."
(concat (when ns (format " (%s)" ns))
(when type (format " <%s>" type))))
(defun cider-company-symbol-kind (symbol)
"Get SYMBOL kind for company-mode."
(let ((type (get-text-property 0 'type symbol)))
(or (cadr (assoc type cider-completion-kind-alist))
type)))
(defun cider-annotate-symbol (symbol)
"Return a string suitable for annotating SYMBOL.
If SYMBOL has a text property `type` whose value is recognised, its
abbreviation according to `cider-completion-annotations-alist' will be
used. If `type` is present but not recognised, its value will be used
unaltered. If SYMBOL has a text property `ns`, then its value will be used
according to `cider-completion-annotations-include-ns'. The formatting is
performed by `cider-annotate-completion-function'."
(when cider-annotate-completion-candidates
(let* ((type (cider-completion--get-candidate-type symbol))
(ns (cider-completion--get-candidate-ns symbol)))
(funcall cider-annotate-completion-function type ns))))
(defun cider-complete-at-point ()
"Complete the symbol at point."
(when-let* ((bounds (bounds-of-thing-at-point 'symbol)))
(when (and (cider-connected-p)
(not (or (cider-in-string-p) (cider-in-comment-p))))
(list (car bounds) (cdr bounds)
(lambda (prefix pred action)
;; When the 'action is 'metadata, this lambda returns metadata about this
;; capf, when action is (boundaries . suffix), it returns nil. With every
;; other value of 'action (t, nil, or lambda), 'action is forwarded to
;; (complete-with-action), together with (cider-complete), prefix and pred.
;; And that function performs the completion based on those arguments.
;;
;; This api is better described in the section
;; '21.6.7 Programmed Completion' of the elisp manual.
(cond ((eq action 'metadata) `(metadata (category . cider)))
((eq (car-safe action) 'boundaries) nil)
(t (with-current-buffer (current-buffer)
(complete-with-action action
(cider-complete prefix) prefix pred)))))
:annotation-function #'cider-annotate-symbol
:company-kind #'cider-company-symbol-kind
:company-doc-buffer #'cider-create-doc-buffer
:company-location #'cider-company-location
:company-docsig #'cider-company-docsig))))
(defun cider-completion-flush-caches ()
"Force Compliment to refill its caches.
This command should be used if Compliment fails to pick up new classnames
and methods from dependencies that were loaded dynamically after the REPL
has started."
(interactive)
(cider-sync-request:complete-flush-caches))
(defun cider-company-location (var)
"Open VAR's definition in a buffer.
Returns the cons of the buffer itself and the location of VAR's definition
in the buffer."
(when-let* ((info (cider-var-info var))
(file (nrepl-dict-get info "file"))
(line (nrepl-dict-get info "line"))
(buffer (cider-find-file file)))
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(forward-line (1- line))
(cons buffer (point))))))
(defun cider-company-docsig (thing)
"Return signature for THING."
(let* ((eldoc-info (cider-eldoc-info thing))
(ns (lax-plist-get eldoc-info "ns"))
(symbol (lax-plist-get eldoc-info "symbol"))
(arglists (lax-plist-get eldoc-info "arglists")))
(when eldoc-info
(format "%s: %s"
(cider-eldoc-format-thing ns symbol thing
(cider-eldoc-thing-type eldoc-info))
(cider-eldoc-format-arglist arglists 0)))))
;; Fuzzy completion for company-mode
(defun cider-company-unfiltered-candidates (string &rest _)
"Return CIDER completion candidates for STRING as is, unfiltered."
(cider-complete string))
(add-to-list 'completion-styles-alist
'(cider
cider-company-unfiltered-candidates
cider-company-unfiltered-candidates
"CIDER backend-driven completion style."))
(defun cider-company-enable-fuzzy-completion ()
"Enable backend-driven fuzzy completion in the current buffer."
(setq-local completion-styles '(cider)))
(provide 'cider-completion)
;;; cider-completion.el ends here
;;; cider-connection.el --- Connection and session life-cycle management for CIDER -*- lexical-binding: t -*-
;;
;; Copyright © 2019-2022 Artur Malabarba, Bozhidar Batsov, Vitalie Spinu and CIDER contributors
;;
;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Vitalie Spinu <spinuvit@gmail.com>
;;
;; Keywords: languages, clojure, cider
;;
;; This program 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.
;;
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;
;; This file is not part of GNU Emacs.
;;
;;
;;; Commentary:
;;
;;
;;; Code:
(require 'nrepl-client)
(require 'cl-lib)
(require 'format-spec)
(require 'sesman)
(require 'sesman-browser)
(require 'spinner)
(require 'cider-popup)
(require 'cider-util)
(defcustom cider-session-name-template "%J:%h:%p"
"Format string to use for session names.
See `cider-format-connection-params' for available format characters."
:type 'string
:group 'cider
:package-version '(cider . "0.18.0"))
(defcustom cider-redirect-server-output-to-repl t
"Controls whether nREPL server output would be redirected to the REPL.
When non-nil the output would end up in both the nrepl-server buffer (when
available) and the matching REPL buffer."
:type 'boolean
:group 'cider
:safe #'booleanp
:package-version '(cider . "0.17.0"))
(defcustom cider-auto-mode t
"When non-nil, automatically enable cider mode for all Clojure buffers."
:type 'boolean
:group 'cider
:safe #'booleanp
:package-version '(cider . "0.9.0"))
;;;###autoload
(defcustom cider-merge-sessions nil
"Controls session combination behaviour.
Symbol `host' combines all sessions of a project associated with the same host.
Symbol `project' combines all sessions of a project.
All other values do not combine any sessions."
:type 'symbol
:group 'cider
:safe #'symbolp
:package-version '(cider . "1.5"))
(defconst cider-required-nrepl-version "0.6.0"
"The minimum nREPL version that's known to work properly with CIDER.")
;;; Connect
(defun cider-nrepl-connect (params)
"Start nrepl client and create the REPL.
PARAMS is a plist containing :host, :port, :server and other parameters for
`cider-repl-create'."
(process-buffer
(nrepl-start-client-process
(plist-get params :host)
(plist-get params :port)
(plist-get params :server)
(lambda (_)
(cider-repl-create params))
(plist-get params :socket-file))))
(defun cider-sessions ()
"Return a list of all active CIDER sessions."
(sesman-sessions 'CIDER))
(defun cider-connected-p ()
"Return t if CIDER is currently connected, nil otherwise."
(process-live-p (get-buffer-process (cider-current-repl))))
(defun cider-ensure-connected ()
"Ensure there is a linked CIDER session."
(sesman-ensure-session 'CIDER))
(defun cider--session-server (session)
"Return server buffer for SESSION or nil if there is no server."
(seq-some (lambda (r)
(buffer-local-value 'nrepl-server-buffer r))
(cdr session)))
(defun cider--gather-session-params (session)
"Gather all params for a SESSION."
(let (params)
(dolist (repl (cdr session))
(setq params (cider--gather-connect-params params repl)))
(when-let* ((server (cider--session-server session)))
(setq params (cider--gather-connect-params params server)))
params))
(defun cider--gather-connect-params (&optional params proc-buffer)
"Gather all relevant connection parameters into PARAMS plist.
PROC-BUFFER is either server or client buffer, defaults to current buffer."
(let ((proc-buffer (or proc-buffer (current-buffer))))
(with-current-buffer proc-buffer
(unless nrepl-endpoint
(error "This is not a REPL or SERVER buffer; is there an active REPL?"))
(let ((server-buf (if (nrepl-server-p proc-buffer)
proc-buffer
nrepl-server-buffer)))
(cl-loop for l on nrepl-endpoint by #'cddr
do (setq params (plist-put params (car l) (cadr l))))
(setq params (thread-first
params
(plist-put :project-dir nrepl-project-dir)))
(when (buffer-live-p server-buf)
(setq params (thread-first
params
(plist-put :server (get-buffer-process server-buf))
(plist-put :server-command nrepl-server-command))))
;; repl-specific parameters (do not pollute server params!)
(unless (nrepl-server-p proc-buffer)
(setq params (thread-first
params
(plist-put :session-name cider-session-name)
(plist-put :repl-type cider-repl-type)
(plist-put :cljs-repl-type cider-cljs-repl-type)
(plist-put :repl-init-function cider-repl-init-function))))
params))))
(defun cider--close-buffer (buffer)
"Close the BUFFER and kill its associated process (if any)."
(when (buffer-live-p buffer)
(when-let* ((proc (get-buffer-process buffer)))
(when (process-live-p proc)
(delete-process proc)))
(kill-buffer buffer)))
(declare-function cider-repl-emit-interactive-stderr "cider-repl")
(defun cider--close-connection (repl &optional no-kill)
"Close connection associated with REPL.
When NO-KILL is non-nil stop the connection but don't kill the REPL
buffer."
(when (buffer-live-p repl)
(with-current-buffer repl
(when spinner-current (spinner-stop))
(when nrepl-tunnel-buffer
(cider--close-buffer nrepl-tunnel-buffer))
(when no-kill
;; inform sentinel not to kill the server, if any
(thread-first
(get-buffer-process repl)
(process-plist)
(plist-put :keep-server t))))
(let ((proc (get-buffer-process repl)))
(when (and (process-live-p proc)
(or (not nrepl-server-buffer)
;; Sync request will hang if the server is dead.
(process-live-p (get-buffer-process nrepl-server-buffer))))
(nrepl-sync-request:close repl)
(delete-process proc)))
(when-let* ((messages-buffer (and nrepl-log-messages
(nrepl-messages-buffer repl))))
(kill-buffer messages-buffer))
(unless no-kill
(kill-buffer repl)))
(when repl
(sesman-remove-object 'CIDER nil repl (not no-kill) t)))
(defun cider-emit-manual-warning (section-id format &rest args)
"Emit a warning to the REPL and link to the online manual.
SECTION-ID is the section to link to. The link is added on the last line.
FORMAT is a format string to compile with ARGS and display on the REPL."
(let ((message (apply #'format format args)))
(cider-repl-emit-interactive-stderr
(concat "WARNING: " message " ("
(cider--manual-button "More information" section-id)
")\n"))))
(defvar cider-version)
(defun cider--check-required-nrepl-version ()
"Check whether we're using a compatible nREPL version."
(if-let* ((nrepl-version (cider--nrepl-version)))
(when (version< nrepl-version cider-required-nrepl-version)
(cider-emit-manual-warning "troubleshooting.html#warning-saying-you-have-to-use-newer-nrepl"
"CIDER requires nREPL %s (or newer) to work properly"
cider-required-nrepl-version))))
(defvar cider-minimum-clojure-version)
(defun cider--check-clojure-version-supported ()
"Ensure that we are meeting the minimum supported version of Clojure."
(if-let* ((clojure-version (cider--clojure-version))
;; drop all qualifiers from the version string
;; e.g. 1.10.0-master-SNAPSHOT becomes simply 1.10.0
(clojure-version (car (split-string clojure-version "-"))))
(when (version< clojure-version cider-minimum-clojure-version)
(cider-emit-manual-warning "basics/installation.html#prerequisites"
"Clojure version (%s) is not supported (minimum %s). CIDER will not work."
clojure-version cider-minimum-clojure-version))))
(defun cider--strip-version-patch (v)
"Strips everything but major.minor from the version, returning a version list.
V is the version string to strip the patch from."
(seq-take (version-to-list v) 2))
(defun cider--compatible-middleware-version-p (required-ver ver)
"Checks that the available middleware version is compatible with the required.
We look only at the major and minor components. When the major
version is 0, only check that the minor versions match. When the major version
is > 0, first check that the major version matches, then that the minor
version is >= the required minor version.
VER the 'installed' version,
REQUIRED-VER the version required by cider."
(let ((ver* (cider--strip-version-patch ver))
(required-ver* (cider--strip-version-patch required-ver)))
(cond ((= 0 (car required-ver*)) (= (cadr required-ver*)
(cadr ver*)))
(t (and (= (car required-ver*)
(car ver*))
(version-list-<= required-ver* ver*))))))
(defvar cider-required-middleware-version)
(defun cider--check-middleware-compatibility ()
"CIDER frontend/backend compatibility check.
Retrieve the underlying connection's CIDER-nREPL version and checks if the
middleware used is compatible with CIDER. If not, will display a warning
message in the REPL area."
(let* ((version-dict (nrepl-aux-info "cider-version" (cider-current-repl)))
(middleware-version (nrepl-dict-get version-dict "version-string")))
(cond
((null middleware-version)
(cider-emit-manual-warning "troubleshooting.html#cider-complains-of-the-cider-nrepl-version"
"CIDER requires cider-nrepl to be fully functional. Some features will not be available without it!"))
((not (cider--compatible-middleware-version-p cider-required-middleware-version middleware-version))
(cider-emit-manual-warning "troubleshooting.html#cider-complains-of-the-cider-nrepl-version"
"CIDER %s requires cider-nrepl %s, but you're currently using cider-nrepl %s. The version mismatch might break some functionality!"
cider-version cider-required-middleware-version middleware-version)))))
(declare-function cider-interactive-eval-handler "cider-eval")
(declare-function cider-nrepl-send-request "cider-client")
;; TODO: Use some null handler here
(defun cider--subscribe-repl-to-server-out ()
"Subscribe to the nREPL server's *out*."
(cider-nrepl-send-request '("op" "out-subscribe")
(cider-interactive-eval-handler (current-buffer))))
(declare-function cider-mode "cider-mode")
(defun cider-enable-on-existing-clojure-buffers ()
"Enable CIDER's minor mode on existing Clojure buffers.
See command `cider-mode'."
(interactive)
(add-hook 'clojure-mode-hook #'cider-mode)
(dolist (buffer (cider-util--clojure-buffers))
(with-current-buffer buffer
(cider-mode +1)
;; In global-eldoc-mode, a new file-visiting buffer calls
;; `turn-on-eldoc-mode' which enables eldoc-mode if it's supported in that
;; buffer as determined by `eldoc--supported-p'. Cider's eldoc support
;; allows new buffers in cider-mode to enable eldoc-mode. As of 2021-04,
;; however, clojure-mode itself has no eldoc support, so old clojure
;; buffers opened before cider started aren't necessarily in eldoc-mode.
;; Here, we've enabled cider-mode for this old clojure buffer, and now, if
;; global-eldoc-mode is enabled, try to enable eldoc-mode as if the buffer
;; had just been created with cider-mode.
(when global-eldoc-mode
(turn-on-eldoc-mode)))))
(declare-function cider--debug-mode "cider-debug")
(defun cider-disable-on-existing-clojure-buffers ()
"Disable `cider-mode' and related commands on existing Clojure buffers."
(interactive)
(dolist (buffer (cider-util--clojure-buffers))
(with-current-buffer buffer
(cider--debug-mode -1)
(cider-mode -1))))
(defun cider-possibly-disable-on-existing-clojure-buffers ()
"Disable `cider-mode' in all Clojure buffers if all CIDER sessions are closed."
(unless (cider-sessions)
(cider-disable-on-existing-clojure-buffers)))
(declare-function cider--debug-init-connection "cider-debug")
(declare-function cider-repl-init "cider-repl")
(declare-function cider-nrepl-op-supported-p "cider-client")
(defun cider--connected-handler ()
"Handle CIDER initialization after nREPL connection has been established.
This function is appended to `nrepl-connected-hook' in the client process
buffer."
;; `nrepl-connected-hook' is run in the connection buffer
;; `cider-enlighten-mode' changes eval to include the debugger, so we inhibit
;; it here as the debugger isn't necessarily initialized yet
(let ((cider-enlighten-mode nil))
;; after initialization, set mode-line and buffer name.
(cider-set-repl-type cider-repl-type)
(cider-repl-init
(current-buffer)
(lambda ()
;; Init logic that's specific to Clojure's nREPL and cider-nrepl
(when (cider-runtime-clojure-p)
(cider--check-required-nrepl-version)
(cider--check-clojure-version-supported)
(cider--check-middleware-compatibility)
;; Redirect the nREPL's terminal output to a REPL buffer.
;; If we don't do this the server's output will end up
;; in the *nrepl-server* buffer.
(when (and cider-redirect-server-output-to-repl
(cider-nrepl-op-supported-p "out-subscribe"))
(cider--subscribe-repl-to-server-out))
;; Middleware on cider-nrepl's side is deferred until first usage, but
;; loading middleware concurrently can lead to occasional "require" issues
;; (likely a Clojure bug). Thus, we load the heavy debug middleware towards
;; the end, allowing for the faster "server-out" middleware to load
;; first.
(cider--debug-init-connection))
(when cider-repl-init-function
(funcall cider-repl-init-function))
(when cider-auto-mode
(cider-enable-on-existing-clojure-buffers))
(run-hooks 'cider-connected-hook)))))
(defun cider--disconnected-handler ()
"Cleanup after nREPL connection has been lost or closed.
This function is appended to `nrepl-disconnected-hook' in the client
process buffer."
;; `nrepl-connected-hook' is run in the connection buffer
(cider-possibly-disable-on-existing-clojure-buffers)
(run-hooks 'cider-disconnected-hook))
;;; Connection Info
(defun cider--java-version ()
"Retrieve the underlying connection's Java version."
(with-current-buffer (cider-current-repl)
(when nrepl-versions
(thread-first
nrepl-versions
(nrepl-dict-get "java")
(nrepl-dict-get "version-string")))))
(defun cider--clojure-version ()
"Retrieve the underlying connection's Clojure version."
(with-current-buffer (cider-current-repl)
(when nrepl-versions
(thread-first
nrepl-versions
(nrepl-dict-get "clojure")
(nrepl-dict-get "version-string")))))
(defun cider--nrepl-version ()
"Retrieve the underlying connection's nREPL version."
(with-current-buffer (cider-current-repl)
(when nrepl-versions
(thread-first
nrepl-versions
(nrepl-dict-get "nrepl")
(nrepl-dict-get "version-string")))))
(defun cider--babashka-version ()
"Retrieve the underlying connection's Babashka version."
(with-current-buffer (cider-current-repl)
(when nrepl-versions
(nrepl-dict-get nrepl-versions "babashka"))))
(defun cider--babashka-nrepl-version ()
"Retrieve the underlying connection's babashka.nrepl version."
(with-current-buffer (cider-current-repl)
(when nrepl-versions
(nrepl-dict-get nrepl-versions "babashka.nrepl"))))
(defun cider-runtime ()
"Return the runtime of the nREPl server."
(cond
((cider--clojure-version) 'clojure)
((cider--babashka-version) 'babashka)
(t 'generic)))
(defun cider-runtime-clojure-p ()
"Check if the current runtime is Clojure."
(eq (cider-runtime) 'clojure))
(defun cider--connection-info (connection-buffer &optional genericp)
"Return info about CONNECTION-BUFFER.
Info contains project name, current REPL namespace, host:port endpoint and
runtime details. When GENERICP is non-nil, don't provide specific info
about this buffer (like variable `cider-repl-type')."
(with-current-buffer connection-buffer
(cond
((cider--clojure-version)
(format "%s%s@%s:%s (Java %s, Clojure %s, nREPL %s)"
(if genericp "" (upcase (concat (symbol-name cider-repl-type) " ")))
(or (cider--project-name nrepl-project-dir) "<no project>")
(plist-get nrepl-endpoint :host)
(plist-get nrepl-endpoint :port)
(cider--java-version)
(cider--clojure-version)
(cider--nrepl-version)))
((cider--babashka-version)
(format "%s%s@%s:%s (Babashka %s, babashka.nrepl %s)"
(if genericp "" (upcase (concat (symbol-name cider-repl-type) " ")))
(or (cider--project-name nrepl-project-dir) "<no project>")
(plist-get nrepl-endpoint :host)
(plist-get nrepl-endpoint :port)
(cider--babashka-version)
(cider--babashka-nrepl-version)))
(t
(format "%s%s@%s:%s"
(if genericp "" (upcase (concat (symbol-name cider-repl-type) " ")))
(or (cider--project-name nrepl-project-dir) "<no project>")
(plist-get nrepl-endpoint :host)
(plist-get nrepl-endpoint :port))))))
;;; Connection Management Commands
(defun cider-quit (&optional repl)
"Quit the CIDER connection associated with REPL.
REPL defaults to the current REPL."
(interactive)
(let ((repl (or repl
(sesman-browser-get 'object)
(cider-current-repl nil 'ensure))))
(cider--close-connection repl))
;; if there are no more sessions we can kill all ancillary buffers
(unless (cider-sessions)
(cider-close-ancillary-buffers))
;; need this to refresh sesman browser
(run-hooks 'sesman-post-command-hook))
(defun cider-restart (&optional repl)
"Restart CIDER connection associated with REPL.
REPL defaults to the current REPL. Don't restart the server or other
connections within the same session. Use `sesman-restart' to restart the
entire session."
(interactive)
(let* ((repl (or repl
(sesman-browser-get 'object)
(cider-current-repl nil 'ensure)))
(params (thread-first
()
(cider--gather-connect-params repl)
(plist-put :session-name (sesman-session-name-for-object 'CIDER repl))
(plist-put :repl-buffer repl))))
(cider--close-connection repl 'no-kill)
(cider-nrepl-connect params)
;; need this to refresh sesman browser
(run-hooks 'sesman-post-command-hook)))
(defun cider-close-ancillary-buffers ()
"Close buffers that are shared across connections."
(interactive)
(dolist (buf-name cider-ancillary-buffers)
(when (get-buffer buf-name)
(kill-buffer buf-name))))
(defun cider-describe-connection (&optional repl)
"Display information about the connection associated with REPL.
REPL defaults to the current REPL."
(interactive)
(let ((repl (or repl
(sesman-browser-get 'object)
(cider-current-repl nil 'ensure))))
(message "%s" (cider--connection-info repl))))
(defconst cider-nrepl-session-buffer "*cider-nrepl-session*")
(declare-function cider-nrepl-eval-session "cider-client")
(declare-function cider-nrepl-tooling-session "cider-client")
(defun cider-describe-nrepl-session ()
"Describe an nREPL session."
(interactive)
(cider-ensure-connected)
(let* ((repl (cider-current-repl nil 'ensure))
(selected-session (completing-read "Describe nREPL session: " (nrepl-sessions repl))))
(when (and selected-session (not (equal selected-session "")))
(let* ((session-info (nrepl-sync-request:describe repl))
(ops (nrepl-dict-keys (nrepl-dict-get session-info "ops")))
(session-id (nrepl-dict-get session-info "session"))
(session-type (cond
((equal session-id (cider-nrepl-eval-session)) "Active eval")
((equal session-id (cider-nrepl-tooling-session)) "Active tooling")
(t "Unknown"))))
(with-current-buffer (cider-popup-buffer cider-nrepl-session-buffer 'select nil 'ancillary)
(read-only-mode -1)
(insert (format "Session: %s\n" session-id)
(format "Type: %s session\n" session-type)
(format "Supported ops:\n"))
(mapc (lambda (op) (insert (format " * %s\n" op))) ops)))
(display-buffer cider-nrepl-session-buffer))))
(defun cider-list-nrepl-middleware ()
"List the loaded nREPL middleware."
(interactive)
(cider-ensure-connected)
(let* ((repl (cider-current-repl nil 'ensure))
(middleware (nrepl-middleware repl)))
(with-current-buffer (cider-popup-buffer "*cider-nrepl-middleware*" 'select nil 'ancillary)
(read-only-mode -1)
(insert (format "Currently loaded middleware:\n"))
(mapc (lambda (mw) (insert (format " * %s\n" mw))) middleware))
(display-buffer "*cider-nrepl-middleware*")))
;;; Sesman's Session-Wise Management UI
(cl-defmethod sesman-project ((_system (eql CIDER)))
"Find project directory."
(clojure-project-dir (cider-current-dir)))
(cl-defmethod sesman-more-relevant-p ((_system (eql CIDER)) session1 session2)
"Figure out if SESSION1 or SESSION2 is more relevant."
(sesman-more-recent-p (cdr session1) (cdr session2)))
(declare-function cider-classpath-entries "cider-client")
(cl-defmethod sesman-friendly-session-p ((_system (eql CIDER)) session)
"Check if SESSION is a friendly session."
(setcdr session (seq-filter #'buffer-live-p (cdr session)))
(when-let* ((repl (cadr session))
(proc (get-buffer-process repl))
(file (file-truename (or (buffer-file-name) default-directory))))
;; With avfs paths look like /path/to/.avfs/path/to/some.jar#uzip/path/to/file.clj
(when (string-match-p "#uzip" file)
(let ((avfs-path (directory-file-name (expand-file-name (or (getenv "AVFSBASE") "~/.avfs/")))))
(setq file (replace-regexp-in-string avfs-path "" file t t))))
(when (process-live-p proc)
(let* ((classpath (or (process-get proc :cached-classpath)
(let ((cp (with-current-buffer repl
(cider-classpath-entries))))
(process-put proc :cached-classpath cp)
cp)))
(classpath-roots (or (process-get proc :cached-classpath-roots)
(let ((cp (thread-last
classpath
(seq-filter (lambda (path) (not (string-match-p "\\.jar$" path))))
(mapcar #'file-name-directory)
(seq-remove #'null)
(seq-uniq))))
(process-put proc :cached-classpath-roots cp)
cp))))
(or (seq-find (lambda (path) (string-prefix-p path file))
classpath)
(seq-find (lambda (path) (string-prefix-p path file))
classpath-roots))))))
(defvar cider-sesman-browser-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "j q") #'cider-quit)
(define-key map (kbd "j k") #'cider-quit)
(define-key map (kbd "j r") #'cider-restart)
(define-key map (kbd "j d") #'cider-describe-connection)
(define-key map (kbd "j i") #'cider-describe-connection)
(define-key map (kbd "C-c C-q") #'cider-quit)
(define-key map (kbd "C-c C-q") #'cider-quit)
(define-key map (kbd "C-c C-r") #'cider-restart)
(define-key map (kbd "C-c M-r") #'cider-restart)
(define-key map (kbd "C-c C-d") #'cider-describe-connection)
(define-key map (kbd "C-c M-d") #'cider-describe-connection)
(define-key map (kbd "C-c C-i") #'cider-describe-connection)
map)
"Map active on REPL objects in sesman browser.")
(cl-defmethod sesman-session-info ((_system (eql CIDER)) session)
"Obtain info for a CIDER SESSION."
(list :objects (cdr session)
:map cider-sesman-browser-map))
(declare-function cider "cider")
(cl-defmethod sesman-start-session ((_system (eql CIDER)))
"Start a connection of any type interactively.
Fallback on `cider' command."
(call-interactively #'cider))
(cl-defmethod sesman-quit-session ((_system (eql CIDER)) session)
"Quit a CIDER SESSION."
(mapc #'cider--close-connection (cdr session))
;; if there are no more session we can kill all ancillary buffers
(unless (cider-sessions)
(cider-close-ancillary-buffers)))
(cl-defmethod sesman-restart-session ((_system (eql CIDER)) session)
"Restart a CIDER SESSION."
(let* ((ses-name (car session))
(repls (cdr session))
(srv-buf (cider--session-server session)))
(if srv-buf
;; session with a server
(let ((s-params (cider--gather-connect-params nil srv-buf)))
;; 1) kill all connections, but keep the buffers
(mapc (lambda (conn)
(cider--close-connection conn 'no-kill))
repls)
;; 2) kill the server
(nrepl-kill-server-buffer srv-buf)
;; 3) start server
(nrepl-start-server-process
(plist-get s-params :project-dir)
(plist-get s-params :server-command)
(lambda (server-buf)
;; 4) restart the repls reusing the buffer
(dolist (r repls)
(cider-nrepl-connect
(thread-first
()
(cider--gather-connect-params r)
;; server params (:port, :project-dir etc) have precedence
(cider--gather-connect-params server-buf)
(plist-put :session-name ses-name)
(plist-put :repl-buffer r))))
(sesman-browser-revert-all 'CIDER)
(message "Restarted CIDER %s session" ses-name))))
;; server-less session
(dolist (r repls)
(cider--close-connection r 'no-kill)
(cider-nrepl-connect
(thread-first
()
(cider--gather-connect-params r)
(plist-put :session-name ses-name)
(plist-put :repl-buffer r)))))))
(defun cider-format-connection-params (template params)
"Format PARAMS with TEMPLATE string.
The following formats can be used in TEMPLATE string:
%h - host
%H - remote host, empty for local hosts
%p - port
%j - short project name, or directory name if no project
%J - long project name including parent dir name
%r - REPL type (clj or cljs)
%S - type of the ClojureScript runtime (Browser, Node, Figwheel etc.)
%s - session name as defined by `cider-session-name-template'.
In case some values are empty, extra separators (: and -) are automatically
removed."
(let* ((dir (directory-file-name
(abbreviate-file-name
(or (plist-get params :project-dir)
(clojure-project-dir (cider-current-dir))
default-directory))))
(short-proj (file-name-nondirectory (directory-file-name dir)))
(parent-dir (ignore-errors
(thread-first dir file-name-directory
directory-file-name file-name-nondirectory
file-name-as-directory)))
(long-proj (format "%s%s" (or parent-dir "") short-proj))
;; use `dir` if it is shorter than `long-proj` or `short-proj`
(short-proj (if (>= (length short-proj) (length dir))
dir
short-proj))
(long-proj (if (>= (length long-proj) (length dir))
dir
long-proj))
(port (or (plist-get params :port) ""))
(host (or (plist-get params :host) "localhost"))
(remote-host (if (member host '("localhost" "127.0.0.1"))
""
host))
(repl-type (or (plist-get params :repl-type) "unknown"))
(cljs-repl-type (or (and (eq repl-type 'cljs)
(plist-get params :cljs-repl-type))
""))
(specs `((?h . ,host)
(?H . ,remote-host)
(?p . ,port)
(?j . ,short-proj)
(?J . ,long-proj)
(?r . ,repl-type)
(?S . ,cljs-repl-type)))
(ses-name (or (plist-get params :session-name)
(format-spec cider-session-name-template specs)))
(specs (append `((?s . ,ses-name)) specs)))
(thread-last
(format-spec template specs)
;; remove extraneous separators
(replace-regexp-in-string "\\([:-]\\)[:-]+" "\\1")
(replace-regexp-in-string "\\(^[:-]\\)\\|\\([:-]$\\)" "")
(replace-regexp-in-string "[:-]\\([])*]\\)" "\\1"))))
(defun cider-make-session-name (params)
"Create new session name given plist of connection PARAMS.
Session name can be customized with `cider-session-name-template'."
(let* ((root-name (cider-format-connection-params cider-session-name-template params))
(other-names (mapcar #'car (sesman-sessions 'CIDER)))
(name root-name)
(i 2))
(while (member name other-names)
(setq name (concat root-name "#" (number-to-string i))
i (+ i 1)))
name))
;;; REPL Buffer Init
(defvar-local cider-cljs-repl-type nil
"The type of the ClojureScript runtime (Browser, Node, Figwheel, etc.).")
(defvar-local cider-repl-type nil
"The type of this REPL buffer, usually either clj or cljs.")
(defun cider-repl-type (repl-buffer)
"Get REPL-BUFFER's type."
(buffer-local-value 'cider-repl-type repl-buffer))
(defun cider-repl-type-for-buffer (&optional buffer)
"Return the matching connection type (clj or cljs) for BUFFER.
BUFFER defaults to the `current-buffer'. In cljc buffers return
multi. This function infers connection type based on the major mode.
For the REPL type use the function `cider-repl-type'."
(with-current-buffer (or buffer (current-buffer))
(cond
((derived-mode-p 'clojurescript-mode) 'cljs)
((derived-mode-p 'clojurec-mode) 'multi)
((derived-mode-p 'clojure-mode) 'clj)
(cider-repl-type))))
(defun cider-set-repl-type (&optional type)
"Set REPL TYPE to clj or cljs.
Assume that the current buffer is a REPL."
(interactive)
(let ((type (cider-maybe-intern (or type (completing-read
(format "Set REPL type (currently `%s') to: "
cider-repl-type)
'(clj cljs))))))
(when (or (not (equal cider-repl-type type))
(null mode-name))
(setq cider-repl-type type)
(setq mode-name (format "REPL[%s]" type))
(let ((params (cider--gather-connect-params)))
;; We need to set current name to something else temporarily to avoid
;; false name duplication in `nrepl-repl-buffer-name`.
(rename-buffer (generate-new-buffer-name "*dummy-cider-repl-buffer*"))
(rename-buffer (nrepl-repl-buffer-name params))
(when (and nrepl-log-messages nrepl-messages-buffer)
(with-current-buffer nrepl-messages-buffer
(rename-buffer (nrepl-messages-buffer-name params))))))))
(defun cider--choose-reusable-repl-buffer (params)
"Find connection-less REPL buffer and ask the user for confirmation.
Return nil if no such buffers exists or the user has chosen not to reuse
the buffer. If multiple dead REPLs exist, ask the user to choose one.
PARAMS is a plist as received by `cider-repl-create'."
(when-let* ((repls (seq-filter (lambda (b)
(with-current-buffer b
(and (derived-mode-p 'cider-repl-mode)
(not (process-live-p (get-buffer-process b))))))
(buffer-list))))
(let* ((proj-dir (plist-get params :project-dir))
(host (plist-get params :host))
(port (plist-get params :port))
(cljsp (member (plist-get params :repl-type) '(cljs pending-cljs)))
(scored-repls
(delq nil
(mapcar (lambda (b)
(let ((bparams (cider--gather-connect-params nil b)))
(when (eq cljsp (member (plist-get bparams :repl-type)
'(cljs pending-cljs)))
(cons (buffer-name b)
(+
(if (equal proj-dir (plist-get bparams :project-dir)) 8 0)
(if (equal host (plist-get bparams :host)) 4 0)
(if (equal port (plist-get bparams :port)) 2 0))))))
repls))))
(when scored-repls
(if (> (length scored-repls) 1)
(when (y-or-n-p "Dead REPLs exist. Reuse? ")
(let ((sorted-repls (seq-sort (lambda (a b) (> (cdr a) (cdr b))) scored-repls)))
(get-buffer (completing-read "REPL to reuse: "
(mapcar #'car sorted-repls) nil t nil nil (caar sorted-repls)))))
(when (y-or-n-p (format "A dead REPL %s exists. Reuse? " (caar scored-repls)))
(get-buffer (caar scored-repls))))))))
(declare-function cider-default-err-handler "cider-eval")
(declare-function cider-repl-mode "cider-repl")
(declare-function cider-repl--state-handler "cider-repl")
(declare-function cider-repl-reset-markers "cider-repl")
(defvar-local cider-session-name nil)
(defvar-local cider-repl-init-function nil)
(defvar-local cider-launch-params nil)
(defun cider-repl-create (params)
"Create new repl buffer.
PARAMS is a plist which contains :repl-type, :host, :port, :project-dir,
:repl-init-function and :session-name. When non-nil, :repl-init-function
must be a function with no arguments which is called after repl creation
function with the repl buffer set as current."
;; Connection might not have been set as yet. Please don't send requests in
;; this function, but use cider--connected-handler instead.
(let ((buffer (or (plist-get params :repl-buffer)
(cider--choose-reusable-repl-buffer params)
(get-buffer-create (generate-new-buffer-name "*cider-uninitialized-repl*"))))
(ses-name (or (plist-get params :session-name)
(cider-make-session-name params))))
(with-current-buffer buffer
(setq-local sesman-system 'CIDER)
(setq-local default-directory (or (plist-get params :project-dir) default-directory))
;; creates a new session if session with ses-name doesn't already exist
(sesman-add-object 'CIDER ses-name buffer 'allow-new)
(unless (derived-mode-p 'cider-repl-mode)
(cider-repl-mode))
(setq nrepl-err-handler #'cider-default-err-handler
;; used as a new-repl marker in cider-set-repl-type
mode-name nil
cider-session-name ses-name
nrepl-project-dir (plist-get params :project-dir)
;; REPLs start with clj and then "upgrade" to a different type
cider-repl-type (plist-get params :repl-type)
;; ran at the end of cider--connected-handler
cider-repl-init-function (plist-get params :repl-init-function)
cider-launch-params params)
(cider-repl-reset-markers)
(add-hook 'nrepl-response-handler-functions #'cider-repl--state-handler nil 'local)
(add-hook 'nrepl-connected-hook #'cider--connected-handler nil 'local)
(add-hook 'nrepl-disconnected-hook #'cider--disconnected-handler nil 'local)
(current-buffer))))
;;; Current/other REPLs
(defun cider--no-repls-user-error (type)
"Throw \"No REPL\" user error customized for TYPE."
(let ((type (cond
((or (eq type 'multi) (eq type 'any))
"clj or cljs")
((listp type)
(mapconcat #'identity type " or "))
(type))))
(user-error "No %s REPLs in current session \"%s\""
type (car (sesman-current-session 'CIDER)))))
(defun cider-current-repl (&optional type ensure)
"Get the most recent REPL of TYPE from the current session.
TYPE is either clj, cljs, multi or any.
When nil, infer the type from the current buffer.
If ENSURE is non-nil, throw an error if either there is
no linked session or there is no REPL of TYPE within the current session."
(let ((type (cider-maybe-intern type)))
(if (and (derived-mode-p 'cider-repl-mode)
(or (null type)
(eq 'any type)
(eq cider-repl-type type)))
;; shortcut when in REPL buffer
(current-buffer)
(let* ((type (or type (cider-repl-type-for-buffer)))
(repls (cider-repls type ensure))
(repl (if (<= (length repls) 1)
(car repls)
;; pick the most recent one
(seq-find (lambda (b)
(member b repls))
(buffer-list)))))
(if (and ensure (null repl))
(cider--no-repls-user-error type)
repl)))))
(defun cider--match-repl-type (type buffer)
"Return non-nil if TYPE matches BUFFER's REPL type."
(let ((buffer-repl-type (cider-repl-type buffer)))
(cond ((null buffer-repl-type) nil)
((or (null type) (eq type 'multi) (eq type 'any)) t)
((listp type) (member buffer-repl-type type))
(t (string= type buffer-repl-type)))))
(defun cider--get-host-from-session (session)
"Returns the host associated with SESSION."
(plist-get (cider--gather-session-params session)
:host))
(defun cider--make-sessions-list-with-hosts (sessions)
"Makes a list of SESSIONS and their hosts.
Returns a list of the form ((session1 host1) (session2 host2) ...)."
(mapcar (lambda (session)
(list session (cider--get-host-from-session session)))
sessions))
(defun cider--get-sessions-with-same-host (session sessions)
"Returns a list of SESSIONS with the same host as SESSION."
(mapcar #'car
(seq-filter (lambda (x)
(string-equal (cadr x)
(cider--get-host-from-session session)))
(cider--make-sessions-list-with-hosts sessions))))
(defun cider--extract-connections (sessions)
"Returns a flattened list of all session buffers in SESSIONS."
(cl-reduce (lambda (x y)
(append x (cdr y)))
sessions
:initial-value '()))
(defun cider-repls (&optional type ensure)
"Return cider REPLs of TYPE from the current session.
If TYPE is nil or multi, return all REPLs. If TYPE is a list of types,
return only REPLs of type contained in the list. If ENSURE is non-nil,
throw an error if no linked session exists."
(let ((type (cond
((listp type)
(mapcar #'cider-maybe-intern type))
((cider-maybe-intern type))))
(repls (pcase cider-merge-sessions
('host
(if ensure
(or (cider--extract-connections (cider--get-sessions-with-same-host
(sesman-current-session 'CIDER)
(sesman-current-sessions 'CIDER)))
(user-error "No linked %s sessions" 'CIDER))
(cider--extract-connections (cider--get-sessions-with-same-host
(sesman-current-session 'CIDER)
(sesman-current-sessions 'CIDER)))))
('project
(if ensure
(or (cider--extract-connections (sesman-current-sessions 'CIDER))
(user-error "No linked %s sessions" 'CIDER))
(cider--extract-connections (sesman-current-sessions 'CIDER))))
(_ (cdr (if ensure
(sesman-ensure-session 'CIDER)
(sesman-current-session 'CIDER)))))))
(or (seq-filter (lambda (b)
(cider--match-repl-type type b))
repls)
(when ensure
(cider--no-repls-user-error type)))))
(defun cider-map-repls (which function)
"Call FUNCTION once for each appropriate REPL as indicated by WHICH.
The function is called with one argument, the REPL buffer. The appropriate
connections are found by inspecting the current buffer. WHICH is one of
the following keywords:
:auto - Act on the connections whose type matches the current buffer. In
`cljc' files, mapping happens over both types of REPLs.
:clj (:cljs) - Map over clj (cljs)) REPLs only.
:clj-strict (:cljs-strict) - Map over clj (cljs) REPLs but signal a
`user-error' in `clojurescript-mode' (`clojure-mode'). Use this for
commands only supported in Clojure (ClojureScript).
Error is signaled if no REPL buffers of specified type exist in current
session."
(declare (indent 1))
(let ((cur-type (cider-repl-type-for-buffer)))
(cl-case which
(:clj-strict (when (eq cur-type 'cljs)
(user-error "Clojure-only operation requested in a ClojureScript buffer")))
(:cljs-strict (when (eq cur-type 'clj)
(user-error "ClojureScript-only operation requested in a Clojure buffer"))))
(let* ((type (cl-case which
((:clj :clj-strict) 'clj)
((:cljs :cljs-strict) 'cljs)
(:auto (if (eq cur-type 'multi)
'(clj cljs)
cur-type))))
(ensure (cl-case which
(:auto nil)
(t 'ensure)))
(repls (cider-repls type ensure)))
(mapcar function repls))))
;; REPLs double as connections in CIDER, so it's useful to be able to refer to
;; them as connections in certain contexts.
(defalias 'cider-current-connection #'cider-current-repl)
(defalias 'cider-connections #'cider-repls)
(defalias 'cider-map-connections #'cider-map-repls)
(defalias 'cider-connection-type-for-buffer #'cider-repl-type-for-buffer)
;; Deprecated after #2324 (introduction of sesman)
(define-obsolete-function-alias 'cider-current-repl-buffer #'cider-current-repl "0.18")
(define-obsolete-function-alias 'cider-repl-buffers #'cider-repls "0.18")
(define-obsolete-function-alias 'cider-current-session #'cider-nrepl-eval-session "0.18")
(define-obsolete-function-alias 'cider-current-tooling-session #'cider-nrepl-tooling-session "0.18")
(define-obsolete-function-alias 'nrepl-connection-buffer-name #'nrepl-repl-buffer-name "0.18")
(provide 'cider-connection)
;;; cider-connection.el ends here
;;; cider-debug.el --- CIDER interaction with the cider.debug nREPL middleware -*- lexical-binding: t; -*-
;; Copyright © 2015-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Instrument code with `cider-debug-defun-at-point', and when the code is
;; executed cider-debug will kick in. See this function's doc for more
;; information.
;;; Code:
(require 'map)
(require 'seq)
(require 'subr-x)
(require 'spinner)
(require 'cider-browse-ns)
(require 'cider-client)
(require 'cider-eval)
(require 'cider-inspector)
(require 'cider-util)
(require 'cider-common)
(require 'nrepl-client) ; `nrepl--mark-id-completed'
(require 'nrepl-dict)
;;; Customization
(defgroup cider-debug nil
"Presentation and behavior of the cider debugger."
:prefix "cider-debug-"
:group 'cider
:package-version '(cider . "0.10.0"))
(defface cider-debug-code-overlay-face
'((((class color) (background light)) :background "grey80")
(((class color) (background dark)) :background "grey30"))
"Face used to mark code being debugged."
:package-version '(cider . "0.9.1"))
(defface cider-debug-prompt-face
'((t :underline t :inherit font-lock-builtin-face))
"Face used to highlight keys in the debug prompt."
:package-version '(cider . "0.10.0"))
(defface cider-enlightened-face
'((((class color) (background light)) :inherit cider-result-overlay-face
:box (:color "darkorange" :line-width -1))
(((class color) (background dark)) :inherit cider-result-overlay-face
;; "#dd0" is a dimmer yellow.
:box (:color "#990" :line-width -1)))
"Face used to mark enlightened sexps and their return values."
:package-version '(cider . "0.11.0"))
(defface cider-enlightened-local-face
'((((class color) (background light)) :weight bold :foreground "darkorange")
(((class color) (background dark)) :weight bold :foreground "yellow"))
"Face used to mark enlightened locals (not their values)."
:package-version '(cider . "0.11.0"))
(defcustom cider-debug-prompt 'overlay
"If and where to show the keys while debugging.
If `minibuffer', show it in the minibuffer along with the return value.
If `overlay', show it in an overlay above the current function.
If t, do both.
If nil, don't list available keys at all."
:type '(choice (const :tag "Show in minibuffer" minibuffer)
(const :tag "Show above function" overlay)
(const :tag "Show in both places" t)
(const :tag "Don't list keys" nil))
:package-version '(cider . "0.10.0"))
(defcustom cider-debug-use-overlays t
"Whether to highlight debugging information with overlays.
Takes the same possible values as `cider-use-overlays', but only applies to
values displayed during debugging sessions.
To control the overlay that lists possible keys above the current function,
configure `cider-debug-prompt' instead."
:type '(choice (const :tag "End of line" t)
(const :tag "Bottom of screen" nil)
(const :tag "Both" both))
:package-version '(cider . "0.9.1"))
(make-obsolete 'cider-debug-print-length 'cider-debug-print-options "0.20")
(make-obsolete 'cider-debug-print-level 'cider-debug-print-options "0.20")
(make-obsolete-variable 'cider-debug-print-options 'cider-print-options "0.21")
;;; Implementation
(declare-function cider-browse-ns--combined-vars-with-meta "cider-browse-ns")
(defun cider-browse-instrumented-defs ()
"List all instrumented definitions."
(interactive)
(if-let* ((all (thread-first (cider-nrepl-send-sync-request '("op" "debug-instrumented-defs"))
(nrepl-dict-get "list"))))
(with-current-buffer (cider-popup-buffer cider-browse-ns-buffer t)
(let ((inhibit-read-only t))
(dolist (list all)
(let* ((ns (car list))
(ns-vars-with-meta (cider-browse-ns--combined-vars-with-meta ns))
(instrumented-meta (nrepl-dict-filter (lambda (k _)
(member k list))
ns-vars-with-meta)))
(cider-browse-ns--list (current-buffer) ns
instrumented-meta
ns)))))
(message "No currently instrumented definitions")))
(defun cider--debug-response-handler (response)
"Handles RESPONSE from the cider.debug middleware."
(nrepl-dbind-response response (status id causes)
(when (member "enlighten" status)
(cider--handle-enlighten response))
(when (or (member "eval-error" status)
(member "stack" status))
;; TODO: Make the error buffer a bit friendlier when we're just printing
;; the stack.
(cider--render-stacktrace-causes causes))
(when (member "need-debug-input" status)
(cider--handle-debug response))
(when (member "done" status)
(nrepl--mark-id-completed id))))
(defun cider--debug-init-connection ()
"Initialize a connection with the cider.debug middleware."
(cider-nrepl-send-request
(thread-last
(map-merge 'list
'(("op" "init-debugger"))
(cider--nrepl-print-request-map fill-column))
(seq-mapcat #'identity))
#'cider--debug-response-handler))
;;; Debugging overlays
(defconst cider--fringe-arrow-string
#("." 0 1 (display (left-fringe right-triangle)))
"Used as an overlay's before-string prop to place a fringe arrow.")
(defun cider--debug-display-result-overlay (value)
"Place an overlay at point displaying VALUE."
(when cider-debug-use-overlays
;; This is cosmetic, let's ensure it doesn't break the session no matter what.
(ignore-errors
;; Result
(cider--make-result-overlay (cider-font-lock-as-clojure value)
:where (point-marker)
:type 'debug-result
'before-string cider--fringe-arrow-string)
;; Code
(cider--make-overlay (save-excursion (clojure-backward-logical-sexp 1) (point))
(point) 'debug-code
'face 'cider-debug-code-overlay-face
;; Higher priority than `show-paren'.
'priority 2000))))
;;; Minor mode
(defvar-local cider--debug-mode-response nil
"Response that triggered current debug session.
Set by `cider--turn-on-debug-mode'.")
(defcustom cider-debug-display-locals nil
"If non-nil, local variables are displayed while debugging.
Can be toggled at any time with `\\[cider-debug-toggle-locals]'."
:type 'boolean
:package-version '(cider . "0.10.0"))
(defcustom cider-debug-prompt-commands
'((?c "continue" "continue")
(?C "continue-all" nil)
(?n "next" "next")
(?i "in" "in")
(?o "out" "out")
(?O "force-out" nil)
(?h "here" "here")
(?e "eval" "eval")
(?p "inspect" "inspect")
(?P "inspect-prompt" nil)
(?l "locals" "locals")
(?j "inject" "inject")
(?s "stacktrace" "stacktrace")
(?t "trace" "trace")
(?q "quit" "quit"))
"A list of debugger command specs.
Specs are in the format (KEY COMMAND-NAME DISPLAY-NAME?) where KEY is a
character which is mapped to the command COMMAND-NAME is a valid debug
command to be passed to the cider-nrepl middleware DISPLAY-NAME is the
string displayed in the debugger overlay
If DISPLAY-NAME is nil, that command is hidden from the overlay but still
callable. The rest of the commands are displayed in the same order as this
list."
:type '(alist :key-type character
:value-type (list
(string :tag "command name")
(choice (string :tag "display name") nil)))
:package-version '(cider . "0.24.0"))
(defun cider--debug-format-locals-list (locals)
"Return a string description of list LOCALS.
Each element of LOCALS should be a list of at least two elements."
(if locals
(let ((left-col-width
;; To right-indent the variable names.
(apply #'max (mapcar (lambda (l) (string-width (car l))) locals))))
;; A format string to build a format string. :-P
(mapconcat (lambda (l) (format (format " %%%ds: %%s\n" left-col-width)
(propertize (car l) 'face 'font-lock-variable-name-face)
(cider-font-lock-as-clojure (cadr l))))
locals ""))
""))
(defun cider--debug-propertize-prompt-commands ()
"In-place format the command display names for the `cider-debug-prompt' overlay."
(mapc (lambda (spec)
(cl-destructuring-bind (char _cmd disp-name) spec
(when-let* ((pos (cl-position char disp-name)))
(put-text-property pos (1+ pos) 'face 'cider-debug-prompt-face disp-name))))
cider-debug-prompt-commands))
(defun cider--debug-prompt (commands)
"Return prompt to display for COMMANDS."
;; Force `default' face, otherwise the overlay "inherits" the face of the text
;; after it.
(format (propertize "%s\n" 'face 'default)
(cl-reduce
(lambda (prompt spec)
(cl-destructuring-bind (_char cmd disp) spec
(if (and disp (cl-find cmd commands :test 'string=))
(concat prompt " " disp)
prompt)))
cider-debug-prompt-commands
:initial-value "")))
(defvar-local cider--debug-prompt-overlay nil)
(defun cider--debug-mode-redisplay ()
"Display the input prompt to the user."
(nrepl-dbind-response cider--debug-mode-response (debug-value input-type locals)
;; input-type is an unsorted collection of command names,
;; as sent by `cider.nrepl.middleware.debug/read-debug-input`
(when (or (eq cider-debug-prompt t)
(eq cider-debug-prompt 'overlay))
(if (overlayp cider--debug-prompt-overlay)
(overlay-put cider--debug-prompt-overlay
'before-string (cider--debug-prompt input-type))
(setq cider--debug-prompt-overlay
(cider--make-overlay
(max (car (cider-defun-at-point 'bounds))
(window-start))
nil 'debug-prompt
'before-string (cider--debug-prompt input-type)))))
(let* ((value (concat " " cider-eval-result-prefix
(cider-font-lock-as-clojure
(or debug-value "#unknown#"))))
(to-display
(concat (when cider-debug-display-locals
(cider--debug-format-locals-list locals))
(when (or (eq cider-debug-prompt t)
(eq cider-debug-prompt 'minibuffer))
(cider--debug-prompt input-type))
(when (or (not cider-debug-use-overlays)
(eq cider-debug-use-overlays 'both))
value))))
(if (> (string-width to-display) 0)
(message "%s" to-display)
;; If there's nothing to display in the minibuffer. Just send the value
;; to the Messages buffer.
(message "%s" value)
(message nil)))))
(defun cider-debug-toggle-locals ()
"Toggle display of local variables."
(interactive)
(setq cider-debug-display-locals (not cider-debug-display-locals))
(cider--debug-mode-redisplay))
(defun cider--debug-lexical-eval (key form &optional callback _point)
"Eval FORM in the lexical context of debug session given by KEY.
Do nothing if CALLBACK is provided.
Designed to be used as `cider-interactive-eval-override' and called instead
of `cider-interactive-eval' in debug sessions."
;; The debugger uses its own callback, so if the caller is passing a callback
;; we return nil and let `cider-interactive-eval' do its thing.
(unless callback
(cider-debug-mode-send-reply (format "{:response :eval, :code %s}" form)
key)
t))
(defvar cider--debug-mode-tool-bar-map
(let ((tool-bar-map (make-sparse-keymap)))
(tool-bar-add-item "right-arrow" #'cider-debug-mode-send-reply :next :label "Next step")
(tool-bar-add-item "next-node" #'cider-debug-mode-send-reply :continue :label "Continue")
(tool-bar-add-item "jump-to" #'cider-debug-mode-send-reply :out :label "Out of sexp")
(tool-bar-add-item "exit" #'cider-debug-mode-send-reply :quit :label "Quit")
tool-bar-map))
(defvar cider--debug-mode-map
(let ((map (make-sparse-keymap)))
;; Bind the `:here` command to both h and H, because it behaves differently
;; if invoked with an uppercase letter.
(define-key map "h" #'cider-debug-move-here)
(define-key map "H" #'cider-debug-move-here)
(define-key map "L" #'cider-debug-toggle-locals)
map)
"The active keymap during a debugging session.")
(define-minor-mode cider--debug-mode
"Mode active during debug sessions.
In order to work properly, this mode must be activated by
`cider--turn-on-debug-mode'."
:init-value nil :lighter " DEBUG" :keymap '()
(if cider--debug-mode
(if cider--debug-mode-response
(nrepl-dbind-response cider--debug-mode-response (input-type)
;; A debug session is an ongoing eval, but it's annoying to have the
;; spinner spinning while you debug.
(when spinner-current (spinner-stop))
(setq-local tool-bar-map cider--debug-mode-tool-bar-map)
(add-hook 'kill-buffer-hook #'cider--debug-quit nil 'local)
(add-hook 'before-revert-hook #'cider--debug-quit nil 'local)
(unless (consp input-type)
(error "Activated debug-mode on a message not asking for commands: %s" cider--debug-mode-response))
;; Integrate with eval commands.
(setq cider-interactive-eval-override
(apply-partially #'cider--debug-lexical-eval
(nrepl-dict-get cider--debug-mode-response "key")))
;; Map over the key->command alist and set the keymap
(mapc
(lambda (p)
(let ((char (car p)))
(unless (= char ?h) ; `here' needs a special command.
(define-key cider--debug-mode-map (string char) #'cider-debug-mode-send-reply))
(when (= char ?o)
(define-key cider--debug-mode-map (string (upcase ?o)) #'cider-debug-mode-send-reply))))
cider-debug-prompt-commands)
(cider--debug-propertize-prompt-commands)
;; Show the prompt.
(cider--debug-mode-redisplay)
;; If a sync request is ongoing, the user can't act normally to
;; provide input, so we enter `recursive-edit'.
(when nrepl-ongoing-sync-request
(recursive-edit)))
(cider--debug-mode -1)
(if (called-interactively-p 'any)
(user-error (substitute-command-keys "Don't call this mode manually, use `\\[universal-argument] \\[cider-eval-defun-at-point]' instead"))
(error "Attempt to activate `cider--debug-mode' without setting `cider--debug-mode-response' first")))
(setq cider-interactive-eval-override nil)
(setq cider--debug-mode-response nil)
;; We wait a moment before clearing overlays and the read-onlyness, so that
;; cider-nrepl has a chance to send the next message, and so that the user
;; doesn't accidentally hit `n' between two messages (thus editing the code).
(when-let* ((proc (unless nrepl-ongoing-sync-request
(get-buffer-process (cider-current-repl)))))
(accept-process-output proc 1))
(unless cider--debug-mode
(setq buffer-read-only nil)
(cider--debug-remove-overlays (current-buffer)))
(when nrepl-ongoing-sync-request
(ignore-errors (exit-recursive-edit)))))
(defun cider--debug-remove-overlays (&optional buffer)
"Remove CIDER debug overlays from BUFFER if variable `cider--debug-mode' is nil."
(when (or (not buffer) (buffer-live-p buffer))
(with-current-buffer (or buffer (current-buffer))
(unless cider--debug-mode
(kill-local-variable 'tool-bar-map)
(remove-overlays nil nil 'category 'debug-result)
(remove-overlays nil nil 'category 'debug-code)
(setq cider--debug-prompt-overlay nil)
(remove-overlays nil nil 'category 'debug-prompt)))))
(defun cider--debug-set-prompt (value)
"Set `cider-debug-prompt' to VALUE, then redisplay."
(setq cider-debug-prompt value)
(cider--debug-mode-redisplay))
(easy-menu-define cider-debug-mode-menu cider--debug-mode-map
"Menu for CIDER debug mode."
`("CIDER Debugger"
["Next step" (cider-debug-mode-send-reply ":next") :keys "n"]
["Continue" (cider-debug-mode-send-reply ":continue") :keys "c"]
["Continue non-stop" (cider-debug-mode-send-reply ":continue-all") :keys "C"]
["Move out of sexp" (cider-debug-mode-send-reply ":out") :keys "o"]
["Forced move out of sexp" (cider-debug-mode-send-reply ":out" nil true) :keys "O"]
["Move to current position" (cider-debug-mode-send-reply ":here") :keys "h"]
["Quit" (cider-debug-mode-send-reply ":quit") :keys "q"]
"--"
["Evaluate in current scope" (cider-debug-mode-send-reply ":eval") :keys "e"]
["Inject value" (cider-debug-mode-send-reply ":inject") :keys "i"]
["Inspect current value" (cider-debug-mode-send-reply ":inspect") :keys "p"]
["Inspect expression" (cider-debug-mode-send-reply ":inspect-prompt") :keys "P"]
["Inspect local variables" (cider-debug-mode-send-reply ":locals") :keys "l"]
"--"
("Configure keys prompt"
["Don't show keys" (cider--debug-set-prompt nil) :style toggle :selected (eq cider-debug-prompt nil)]
["Show in minibuffer" (cider--debug-set-prompt 'minibuffer) :style toggle :selected (eq cider-debug-prompt 'minibuffer)]
["Show above function" (cider--debug-set-prompt 'overlay) :style toggle :selected (eq cider-debug-prompt 'overlay)]
["Show in both places" (cider--debug-set-prompt t) :style toggle :selected (eq cider-debug-prompt t)]
"--"
["List locals" cider-debug-toggle-locals :style toggle :selected cider-debug-display-locals])
["Customize" (customize-group 'cider-debug)]))
(defun cider--uppercase-command-p ()
"Return non-nil if the last command was uppercase letter."
(ignore-errors
(let ((case-fold-search nil))
(string-match "[[:upper:]]" (string last-command-event)))))
(defun cider-debug-mode-send-reply (command &optional key force)
"Reply to the message that started current bufer's debugging session.
COMMAND is sent as the input option. KEY can be provided to reply to a
specific message. If FORCE is non-nil, send a \"force?\" argument in the
message."
(interactive (list
(if (symbolp last-command-event)
(symbol-name last-command-event)
(ignore-errors
(concat ":" (cadr (assoc last-command-event cider-debug-prompt-commands)))))
nil
(cider--uppercase-command-p)))
(when (and (string-prefix-p ":" command) force)
(setq command (format "{:response %s :force? true}" command)))
(cider-nrepl-send-unhandled-request
`("op" "debug-input"
"input" ,(or command ":quit")
"key" ,(or key (nrepl-dict-get cider--debug-mode-response "key"))))
(ignore-errors (cider--debug-mode -1)))
(defun cider--debug-quit ()
"Send a :quit reply to the debugger. Used in hooks."
(when cider--debug-mode
(cider-debug-mode-send-reply ":quit")
(message "Quitting debug session")))
;;; Movement logic
(defconst cider--debug-buffer-format "*cider-debug %s*")
(defun cider--debug-trim-code (code)
"Remove whitespace and reader macros from the start of the CODE.
Return trimmed CODE."
(replace-regexp-in-string "\\`#[a-z]+[\n\r[:blank:]]*" "" code))
(declare-function cider-set-buffer-ns "cider-mode")
(defun cider--initialize-debug-buffer (code ns id &optional reason)
"Create a new debugging buffer with CODE and namespace NS.
ID is the id of the message that instrumented CODE.
REASON is a keyword describing why this buffer was necessary."
(let ((buffer-name (format cider--debug-buffer-format id)))
(if-let* ((buffer (get-buffer buffer-name)))
(cider-popup-buffer-display buffer 'select)
(with-current-buffer (cider-popup-buffer buffer-name 'select
#'clojure-mode 'ancillary)
(cider-set-buffer-ns ns)
(setq buffer-undo-list nil)
(let ((inhibit-read-only t)
(buffer-undo-list t))
(erase-buffer)
(insert (format "%s" (cider--debug-trim-code code)))
(when code
(insert "\n\n\n;; We had to create this temporary buffer because we couldn't find the original definition. That probably happened because "
reason
".")
(fill-paragraph))
(font-lock-ensure)
(set-buffer-modified-p nil))))
(switch-to-buffer buffer-name)
(goto-char (point-min))))
(defun cider--debug-goto-keyval (key)
"Find KEY in current sexp or return nil."
(when-let* ((limit (ignore-errors (save-excursion (up-list) (point)))))
(search-forward-regexp (concat "\\_<" (regexp-quote key) "\\_>")
limit 'noerror)))
(defun cider--debug-skip-ignored-forms ()
"Skip past all forms ignored with #_ reader macro."
;; Logic taken from `clojure--search-comment-macro-internal'
(while (looking-at (concat "[ ,\r\t\n]*" clojure--comment-macro-regexp))
(let ((md (match-data))
(start (match-beginning 1)))
(goto-char start)
;; Count how many #_ we got and step by that many sexps
(clojure-forward-logical-sexp
(count-matches (rx "#_") (elt md 0) (elt md 1))))))
(defun cider--debug-move-point (coordinates)
"Place point on after the sexp specified by COORDINATES.
COORDINATES is a list of integers that specify how to navigate into the
sexp that is after point when this function is called.
As an example, a COORDINATES list of '(1 0 2) means:
- enter next sexp then `forward-sexp' once,
- enter next sexp,
- enter next sexp then `forward-sexp' twice.
In the following snippet, this takes us to the (* x 2) sexp (point is left
at the end of the given sexp).
(letfn [(twice [x]
(* x 2))]
(twice 15))
In addition to numbers, a coordinate can be a string. This string names the
key of a map, and it means \"go to the value associated with this key\"."
(condition-case-unless-debug nil
;; Navigate through sexps inside the sexp.
(let ((in-syntax-quote nil))
(while coordinates
(while (clojure--looking-at-non-logical-sexp)
(forward-sexp))
;; An `@x` is read as (deref x), so we pop coordinates once to account
;; for the extra depth, and move past the @ char.
(if (eq ?@ (char-after))
(progn (forward-char 1)
(pop coordinates))
(down-list)
;; Are we entering a syntax-quote?
(when (looking-back "`\\(#{\\|[{[(]\\)" (line-beginning-position))
;; If we are, this affects all nested structures until the next `~',
;; so we set this variable for all following steps in the loop.
(setq in-syntax-quote t))
(when in-syntax-quote
;; A `(. .) is read as (seq (concat (list .) (list .))). This pops
;; the `seq', since the real coordinates are inside the `concat'.
(pop coordinates)
;; Non-list seqs like `[] and `{} are read with
;; an extra (apply vector ...), so pop it too.
(unless (eq ?\( (char-before))
(pop coordinates)))
;; #(...) is read as (fn* ([] ...)), so we patch that here.
(when (looking-back "#(" (line-beginning-position))
(pop coordinates))
(if coordinates
(let ((next (pop coordinates)))
(when in-syntax-quote
;; We're inside the `concat' form, but we need to discard the
;; actual `concat' symbol from the coordinate.
(setq next (1- next)))
;; String coordinates are map keys.
(if (stringp next)
(cider--debug-goto-keyval next)
(clojure-forward-logical-sexp next)
(when in-syntax-quote
(clojure-forward-logical-sexp 1)
(forward-sexp -1)
;; Here a syntax-quote is ending.
(let ((match (when (looking-at "~@?")
(match-string 0))))
(when match
(setq in-syntax-quote nil))
;; A `~@' is read as the object itself, so we don't pop
;; anything.
(unless (equal "~@" match)
;; Anything else (including a `~') is read as a `list'
;; form inside the `concat', so we need to pop the list
;; from the coordinates.
(pop coordinates))))))
;; If that extra pop was the last coordinate, this represents the
;; entire #(...), so we should move back out.
(backward-up-list)))
;; Finally skip past all #_ forms
(cider--debug-skip-ignored-forms))
;; Place point at the end of instrumented sexp.
(clojure-forward-logical-sexp 1))
;; Avoid throwing actual errors, since this happens on every breakpoint.
(error (message "Can't find instrumented sexp, did you edit the source?"))))
(defun cider--debug-position-for-code (code)
"Return non-nil if point is roughly before CODE.
This might move point one line above."
(or (looking-at-p (regexp-quote code))
(let ((trimmed (regexp-quote (cider--debug-trim-code code))))
(or (looking-at-p trimmed)
;; If this is a fake #dbg injected by `C-u
;; C-M-x', then the sexp we want is actually on
;; the line above.
(progn (forward-line -1)
(looking-at-p trimmed))))))
(defun cider--debug-find-source-position (response &optional create-if-needed)
"Return a marker of the position after the sexp specified in RESPONSE.
This marker might be in a different buffer! If the sexp can't be
found (file that contains the code is no longer visited or has been
edited), return nil. However, if CREATE-IF-NEEDED is non-nil, a new buffer
is created in this situation and the return value is never nil.
Follow the \"line\" and \"column\" entries in RESPONSE, and check whether
the code at point matches the \"code\" entry in RESPONSE. If it doesn't,
assume that the code in this file has been edited, and create a temp buffer
holding the original code.
Either way, navigate inside the code by following the \"coor\" entry which
is a coordinate measure in sexps."
(nrepl-dbind-response response (code file line column ns original-id coor)
(when (or code (and file line column))
;; This is for restoring current-buffer.
(save-excursion
(let ((out))
;; We prefer in-source debugging.
(when-let* ((buf (and file line column
(ignore-errors
(cider--find-buffer-for-file file)))))
;; The logic here makes it hard to use `with-current-buffer'.
(with-current-buffer buf
;; This is for restoring point inside buf.
(save-excursion
;; Get to the proper line & column in the file
(forward-line (- line (line-number-at-pos)))
;; Column numbers in the response start from 1.
;; Convert to Emacs system which starts from 0
;; Inverse of `cider-column-number-at-pos'.
(move-to-column (max 0 (1- column)))
;; Check if it worked
(when (cider--debug-position-for-code code)
;; Find the desired sexp.
(cider--debug-move-point coor)
(setq out (point-marker))))))
;; But we can create a temp buffer if that fails.
(or out
(when create-if-needed
(cider--initialize-debug-buffer
code ns original-id
(if (and line column)
"you edited the code"
"your nREPL version is older than 0.2.11"))
(save-excursion
(cider--debug-move-point coor)
(point-marker)))))))))
(defun cider--handle-debug (response)
"Handle debugging notification.
RESPONSE is a message received from the nrepl describing the input
needed. It is expected to contain at least \"key\", \"input-type\", and
\"prompt\", and possibly other entries depending on the input-type."
(nrepl-dbind-response response (debug-value key input-type prompt inspect)
(condition-case-unless-debug e
(progn
(pcase input-type
("expression" (cider-debug-mode-send-reply
(condition-case nil
(cider-read-from-minibuffer
(or prompt "Expression: "))
(quit "nil"))
key))
((pred sequencep)
(let* ((marker (cider--debug-find-source-position response 'create-if-needed)))
(pop-to-buffer (marker-buffer marker))
(goto-char marker))
;; The overlay code relies on window boundaries, but point could have been
;; moved outside the window by some other code. Redisplay here to ensure the
;; visible window includes point.
(redisplay)
;; Remove overlays AFTER redisplaying! Otherwise there's a visible
;; flicker even if we immediately recreate the overlays.
(cider--debug-remove-overlays)
(when cider-debug-use-overlays
(cider--debug-display-result-overlay debug-value))
(setq cider--debug-mode-response response)
(cider--debug-mode 1)))
(when inspect
(setq cider-inspector--current-repl (cider-current-repl))
(cider-inspector--render-value inspect)))
;; If something goes wrong, we send a "quit" or the session hangs.
(error (cider-debug-mode-send-reply ":quit" key)
(message "Error encountered while handling the debug message: %S" e)))))
(defun cider--handle-enlighten (response)
"Handle an enlighten notification.
RESPONSE is a message received from the nrepl describing the value and
coordinates of a sexp. Create an overlay after the specified sexp
displaying its value."
(when-let* ((marker (cider--debug-find-source-position response)))
(with-current-buffer (marker-buffer marker)
(save-excursion
(goto-char marker)
(clojure-backward-logical-sexp 1)
(nrepl-dbind-response response (debug-value erase-previous)
(when erase-previous
(remove-overlays (point) marker 'category 'enlighten))
(when debug-value
(if (memq (char-before marker) '(?\) ?\] ?}))
;; Enlightening a sexp looks like a regular return value, except
;; for a different border.
(cider--make-result-overlay (cider-font-lock-as-clojure debug-value)
:where (cons marker marker)
:type 'enlighten
:prepend-face 'cider-enlightened-face)
;; Enlightening a symbol uses a more abbreviated format. The
;; result face is the same as a regular result, but we also color
;; the symbol with `cider-enlightened-local-face'.
(cider--make-result-overlay (cider-font-lock-as-clojure debug-value)
:format "%s"
:where (cons (point) marker)
:type 'enlighten
'face 'cider-enlightened-local-face))))))))
;;; Move here command
;; This is the inverse of `cider--debug-move-point'. However, that algorithm is
;; complicated, and trying to code its inverse would probably be insane.
;; Instead, we find the coordinate by trial and error.
(defun cider--debug-find-coordinates-for-point (target &optional list-so-far)
"Return the coordinates list for reaching TARGET.
Assumes that the next thing after point is a logical Clojure sexp and that
TARGET is inside it. The returned list is suitable for use in
`cider--debug-move-point'. LIST-SO-FAR is for internal use."
(when (looking-at (rx (or "(" "[" "#{" "{")))
(let ((starting-point (point)))
(unwind-protect
(let ((x 0))
;; Keep incrementing the last coordinate until we've moved
;; past TARGET.
(while (condition-case nil
(progn (goto-char starting-point)
(cider--debug-move-point (append list-so-far (list x)))
(< (point) target))
;; Not a valid coordinate. Move back a step and stop here.
(scan-error (setq x (1- x))
nil))
(setq x (1+ x)))
(setq list-so-far (append list-so-far (list x)))
;; We have moved past TARGET, now determine whether we should
;; stop, or if target is deeper inside the previous sexp.
(if (or (= target (point))
(progn (forward-sexp -1)
(<= target (point))))
list-so-far
(goto-char starting-point)
(cider--debug-find-coordinates-for-point target list-so-far)))
;; `unwind-protect' clause.
(goto-char starting-point)))))
(defun cider-debug-move-here (&optional force)
"Skip any breakpoints up to point.
The boolean value of FORCE will be sent in the reply."
(interactive (list (cider--uppercase-command-p)))
(unless cider--debug-mode
(user-error "`cider-debug-move-here' only makes sense during a debug session"))
(let ((here (point)))
(nrepl-dbind-response cider--debug-mode-response (line column)
(if (and line column (buffer-file-name))
(progn ;; Get to the proper line & column in the file
(forward-line (1- (- line (line-number-at-pos))))
(move-to-column column))
(beginning-of-defun))
;; Is HERE inside the sexp being debugged?
(when (or (< here (point))
(save-excursion
(forward-sexp 1)
(> here (point))))
(user-error "Point is outside the sexp being debugged"))
;; Move forward until start of sexp.
(comment-normalize-vars)
(comment-forward (point-max))
;; Find the coordinate and send it.
(cider-debug-mode-send-reply
(format "{:response :here, :coord %s :force? %s}"
(cider--debug-find-coordinates-for-point here)
(if force "true" "false"))))))
;;; User commands
;;;###autoload
(defun cider-debug-defun-at-point ()
"Instrument the \"top-level\" expression at point.
If it is a defn, dispatch the instrumented definition. Otherwise,
immediately evaluate the instrumented expression.
While debugged code is being evaluated, the user is taken through the
source code and displayed the value of various expressions. At each step,
a number of keys will be prompted to the user."
(interactive)
(cider-eval-defun-at-point 'debug-it))
(provide 'cider-debug)
;;; cider-debug.el ends here
;;; cider-doc.el --- CIDER documentation functionality -*- lexical-binding: t -*-
;; Copyright © 2014-2022 Bozhidar Batsov, Jeff Valk and CIDER contributors
;; Author: Jeff Valk <jv@jeffvalk.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Mode for formatting and presenting documentation
;;; Code:
(require 'cider-common)
(require 'subr-x)
(require 'cider-util)
(require 'cider-popup)
(require 'cider-client)
(require 'cider-clojuredocs)
(require 'nrepl-dict)
(require 'button)
(require 'easymenu)
(require 'cider-browse-spec)
;; we defer loading those, as org-table is a big library
(declare-function org-table-map-tables "org-table")
(declare-function org-table-align "org-table")
(declare-function org-table-begin "org-table")
(declare-function org-table-end "org-table")
;;; Variables
(defgroup cider-doc nil
"Documentation for CIDER."
:prefix "cider-doc-"
:group 'cider)
(defcustom cider-doc-auto-select-buffer t
"Controls whether to auto-select the doc popup buffer."
:type 'boolean
:group 'cider-doc
:package-version '(cider . "0.15.0"))
(declare-function cider-apropos "cider-apropos")
(declare-function cider-apropos-select "cider-apropos")
(declare-function cider-apropos-documentation "cider-apropos")
(declare-function cider-apropos-documentation-select "cider-apropos")
(defvar cider-doc-map
(let (cider-doc-map)
(define-prefix-command 'cider-doc-map)
(define-key cider-doc-map (kbd "a") #'cider-apropos)
(define-key cider-doc-map (kbd "C-a") #'cider-apropos)
(define-key cider-doc-map (kbd "s") #'cider-apropos-select)
(define-key cider-doc-map (kbd "C-s") #'cider-apropos-select)
(define-key cider-doc-map (kbd "f") #'cider-apropos-documentation)
(define-key cider-doc-map (kbd "C-f") #'cider-apropos-documentation)
(define-key cider-doc-map (kbd "e") #'cider-apropos-documentation-select)
(define-key cider-doc-map (kbd "C-e") #'cider-apropos-documentation-select)
(define-key cider-doc-map (kbd "d") #'cider-doc)
(define-key cider-doc-map (kbd "C-d") #'cider-doc)
(define-key cider-doc-map (kbd "c") #'cider-clojuredocs)
(define-key cider-doc-map (kbd "C-c") #'cider-clojuredocs)
(define-key cider-doc-map (kbd "w") #'cider-clojuredocs-web)
(define-key cider-doc-map (kbd "C-w") #'cider-clojuredocs-web)
(define-key cider-doc-map (kbd "j") #'cider-javadoc)
(define-key cider-doc-map (kbd "C-j") #'cider-javadoc)
cider-doc-map)
"CIDER documentation keymap.")
(defconst cider-doc-menu
'("Documentation"
["CiderDoc" cider-doc]
["JavaDoc in browser" cider-javadoc]
"--"
["Clojuredocs" cider-clojuredocs]
["Clojuredocs in browser" cider-clojuredocs-web]
["Refresh ClojureDocs cache" cider-clojuredocs-refresh-cache]
"--"
["Search symbols" cider-apropos]
["Search symbols & select" cider-apropos-select]
["Search documentation" cider-apropos-documentation]
["Search documentation & select" cider-apropos-documentation-select]
"--"
["Configure Doc buffer" (customize-group 'cider-docview-mode)])
"CIDER documentation submenu.")
;;; cider-docview-mode
(defgroup cider-docview-mode nil
"Formatting/fontifying documentation viewer."
:prefix "cider-docview-"
:group 'cider)
(defcustom cider-docview-fill-column fill-column
"Fill column for docstrings in doc buffer."
:type 'list
:group 'cider-docview-mode
:package-version '(cider . "0.7.0"))
;; Faces
(defface cider-docview-emphasis-face
'((t (:inherit default :underline t)))
"Face for emphasized text."
:group 'cider-docview-mode
:package-version '(cider . "0.7.0"))
(defface cider-docview-strong-face
'((t (:inherit default :underline t :weight bold)))
"Face for strongly emphasized text."
:group 'cider-docview-mode
:package-version '(cider . "0.7.0"))
(defface cider-docview-literal-face
'((t (:inherit font-lock-string-face)))
"Face for literal text."
:group 'cider-docview-mode
:package-version '(cider . "0.7.0"))
(defface cider-docview-table-border-face
'((t (:inherit shadow)))
"Face for table borders."
:group 'cider-docview-mode
:package-version '(cider . "0.7.0"))
;; Colors & Theme Support
(defvar cider-docview-code-background-color
(cider-scale-background-color)
"Background color for code blocks.")
(advice-add 'enable-theme :after #'cider--docview-adapt-to-theme)
(advice-add 'disable-theme :after #'cider--docview-adapt-to-theme)
(defun cider--docview-adapt-to-theme (&rest _)
"When theme is changed, update `cider-docview-code-background-color'."
(setq cider-docview-code-background-color (cider-scale-background-color)))
;; Mode & key bindings
(defvar cider-docview-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "q" #'cider-popup-buffer-quit-function)
(define-key map "g" #'cider-docview-clojuredocs)
(define-key map "G" #'cider-docview-clojuredocs-web)
(define-key map "j" #'cider-docview-javadoc)
(define-key map "s" #'cider-docview-source)
(define-key map (kbd "<backtab>") #'backward-button)
(define-key map (kbd "TAB") #'forward-button)
(easy-menu-define cider-docview-mode-menu map
"Menu for CIDER's doc mode"
`("CiderDoc"
["Look up in Clojuredocs" cider-docview-clojuredocs]
["Look up in Clojuredocs (browser)" cider-docview-clojuredocs-web]
["JavaDoc in browser" cider-docview-javadoc]
["Jump to source" cider-docview-source]
"--"
["Quit" cider-popup-buffer-quit-function]
))
map))
(defvar cider-docview-symbol)
(defvar cider-docview-javadoc-url)
(defvar cider-docview-file)
(defvar cider-docview-line)
(define-derived-mode cider-docview-mode help-mode "Doc"
"Major mode for displaying CIDER documentation.
\\{cider-docview-mode-map}"
(setq buffer-read-only t)
(setq-local sesman-system 'CIDER)
(when cider-special-mode-truncate-lines
(setq-local truncate-lines t))
(setq-local electric-indent-chars nil)
(setq-local cider-docview-symbol nil)
(setq-local cider-docview-javadoc-url nil)
(setq-local cider-docview-file nil)
(setq-local cider-docview-line nil))
;;; Interactive functions
(defun cider-docview-javadoc ()
"Open the Javadoc for the current class, if available."
(interactive)
(if cider-docview-javadoc-url
(browse-url cider-docview-javadoc-url)
(error "No Javadoc available for %s" cider-docview-symbol)))
(defun cider-javadoc-handler (symbol-name)
"Invoke the nREPL \"info\" op on SYMBOL-NAME if available."
(when symbol-name
(let* ((info (cider-var-info symbol-name))
(url (nrepl-dict-get info "javadoc")))
(if url
(browse-url url)
(user-error "No Javadoc available for %s" symbol-name)))))
(defun cider-javadoc (arg)
"Open Javadoc documentation in a popup buffer.
Prompts for the symbol to use, or uses the symbol at point, depending on
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
opposite of what that option dictates."
(interactive "P")
(cider-ensure-connected)
(cider-ensure-op-supported "info")
(funcall (cider-prompt-for-symbol-function arg)
"Javadoc for"
#'cider-javadoc-handler))
(defun cider-docview-source ()
"Open the source for the current symbol, if available."
(interactive)
(if cider-docview-file
(if-let* ((buffer (and (not (cider--tooling-file-p cider-docview-file))
(cider-find-file cider-docview-file))))
(cider-jump-to buffer (if cider-docview-line
(cons cider-docview-line nil)
cider-docview-symbol)
nil)
(user-error
(substitute-command-keys
"Can't find the source because it wasn't defined with `cider-eval-buffer'")))
(error "No source location for %s" cider-docview-symbol)))
(defvar cider-buffer-ns)
(declare-function cider-clojuredocs-lookup "cider-clojuredocs")
(defun cider-docview-clojuredocs ()
"Return the clojuredocs documentation for `cider-docview-symbol'."
(interactive)
(if cider-buffer-ns
(cider-clojuredocs-lookup cider-docview-symbol)
(error "%s cannot be looked up on ClojureDocs" cider-docview-symbol)))
(declare-function cider-clojuredocs-web-lookup "cider-clojuredocs")
(defun cider-docview-clojuredocs-web ()
"Open the clojuredocs documentation for `cider-docview-symbol' in a web browser."
(interactive)
(if cider-buffer-ns
(cider-clojuredocs-web-lookup cider-docview-symbol)
(error "%s cannot be looked up on ClojureDocs" cider-docview-symbol)))
(defconst cider-doc-buffer "*cider-doc*")
(defun cider-create-doc-buffer (symbol)
"Populates *cider-doc* with the documentation for SYMBOL."
(when-let* ((info (cider-var-info symbol)))
(cider-docview-render (cider-make-popup-buffer cider-doc-buffer nil 'ancillary) symbol info)))
(defun cider-doc-lookup (symbol)
"Look up documentation for SYMBOL."
(if-let* ((buffer (cider-create-doc-buffer symbol)))
(cider-popup-buffer-display buffer cider-doc-auto-select-buffer)
(user-error "Symbol %s not resolved" symbol)))
(defun cider-doc (&optional arg)
"Open Clojure documentation in a popup buffer.
Prompts for the symbol to use, or uses the symbol at point, depending on
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
opposite of what that option dictates."
(interactive "P")
(cider-ensure-connected)
(funcall (cider-prompt-for-symbol-function arg)
"Doc for"
#'cider-doc-lookup))
;;; Font Lock and Formatting
(defun cider-docview-fontify-code-blocks (buffer mode)
"Font lock BUFFER code blocks using MODE and remove markdown characters.
This processes the triple backtick GFM markdown extension. An overlay is used
to shade the background. Blocks are marked to be ignored by other fonification
and line wrap."
(with-current-buffer buffer
(save-excursion
(while (search-forward-regexp "```\n" nil t)
(replace-match "")
(let ((beg (point))
(bg `(:background ,cider-docview-code-background-color)))
(when (search-forward-regexp "```\n" nil t)
(replace-match "")
(cider-font-lock-region-as mode beg (point))
(overlay-put (make-overlay beg (point)) 'font-lock-face bg)
(put-text-property beg (point) 'block 'code)))))))
(defun cider-docview-fontify-literals (buffer)
"Font lock BUFFER literal text and remove backtick markdown characters.
Preformatted code text blocks are ignored."
(with-current-buffer buffer
(save-excursion
(while (search-forward "`" nil t)
(if (eq (get-text-property (point) 'block) 'code)
(forward-char)
(progn
(replace-match "")
(let ((beg (point)))
(when (search-forward "`" (line-end-position) t)
(replace-match "")
(put-text-property beg (point) 'font-lock-face 'cider-docview-literal-face)))))))))
(defun cider-docview-fontify-emphasis (buffer)
"Font lock BUFFER emphasized text and remove markdown characters.
One '*' represents emphasis, multiple '**'s represent strong emphasis.
Preformatted code text blocks are ignored."
(with-current-buffer buffer
(save-excursion
(while (search-forward-regexp "\\(*+\\)\\(\\w\\)" nil t)
(if (eq (get-text-property (point) 'block) 'code)
(forward-char)
(progn
(replace-match "\\2")
(let ((beg (1- (point)))
(face (if (> (length (match-string 1)) 1)
'cider-docview-strong-face
'cider-docview-emphasis-face)))
(when (search-forward-regexp "\\(\\w\\)\\*+" (line-end-position) t)
(replace-match "\\1")
(put-text-property beg (point) 'font-lock-face face)))))))))
(defun cider-docview-format-tables (buffer)
"Align BUFFER tables and dim borders.
This processes the GFM table markdown extension using `org-table'.
Tables are marked to be ignored by line wrap."
(require 'org-table)
(with-current-buffer buffer
(save-excursion
(let ((border 'cider-docview-table-border-face))
(org-table-map-tables
(lambda ()
(org-table-align)
(goto-char (org-table-begin))
(while (search-forward-regexp "[+|-]" (org-table-end) t)
(put-text-property (match-beginning 0) (match-end 0) 'font-lock-face border))
(put-text-property (org-table-begin) (org-table-end) 'block 'table)))))))
(defun cider-docview-wrap-text (buffer)
"For text in BUFFER not propertized as 'block', apply line wrap."
(with-current-buffer buffer
(save-excursion
(while (not (eobp))
(unless (get-text-property (point) 'block)
(fill-region (point) (line-end-position)))
(forward-line)))))
;;; Rendering
(defun cider-docview-render-java-doc (buffer text)
"Emit into BUFFER formatted doc TEXT for a Java class or member."
(with-current-buffer buffer
(let ((beg (point)))
(insert text)
(save-excursion
(goto-char beg)
(cider-docview-fontify-code-blocks buffer 'java-mode) ; left alone hereafter
(cider-docview-fontify-literals buffer)
(cider-docview-fontify-emphasis buffer)
(cider-docview-format-tables buffer) ; may contain literals, emphasis
(cider-docview-wrap-text buffer))))) ; ignores code, table blocks
(defun cider--abbreviate-file-protocol (file-with-protocol)
"Abbreviate the file-path in `file:/path/to/file' of FILE-WITH-PROTOCOL."
(if (string-match "\\`file:\\(.*\\)" file-with-protocol)
(let ((file (match-string 1 file-with-protocol))
(proj-dir (clojure-project-dir)))
(if (and proj-dir
(file-in-directory-p file proj-dir))
(file-relative-name file proj-dir)
file))
file-with-protocol))
(defun cider-docview-render-info (buffer info)
"Emit into BUFFER formatted INFO for the Clojure or Java symbol."
(let* ((ns (nrepl-dict-get info "ns"))
(name (nrepl-dict-get info "name"))
(added (nrepl-dict-get info "added"))
(depr (nrepl-dict-get info "deprecated"))
(macro (nrepl-dict-get info "macro"))
(special (nrepl-dict-get info "special-form"))
(builtin (nrepl-dict-get info "built-in")) ;; babashka specific
(forms (when-let* ((str (nrepl-dict-get info "forms-str")))
(split-string str "\n")))
(args (when-let* ((str (nrepl-dict-get info "arglists-str")))
(split-string str "\n")))
(doc (or (nrepl-dict-get info "doc")
"Not documented."))
(url (nrepl-dict-get info "url"))
(class (nrepl-dict-get info "class"))
(member (nrepl-dict-get info "member"))
(javadoc (nrepl-dict-get info "javadoc"))
(super (nrepl-dict-get info "super"))
(ifaces (nrepl-dict-get info "interfaces"))
(spec (nrepl-dict-get info "spec"))
(clj-name (if ns (concat ns "/" name) name))
(java-name (if member (concat class "/" member) class))
(see-also (nrepl-dict-get info "see-also")))
(cider--help-setup-xref (list #'cider-doc-lookup (format "%s/%s" ns name)) nil buffer)
(with-current-buffer buffer
(cl-flet ((emit (text &optional face)
(insert (if face
(propertize text 'font-lock-face face)
text)
"\n")))
(emit (if class java-name clj-name) 'font-lock-function-name-face)
(when super
(emit (concat " Extends: " (cider-font-lock-as 'java-mode super))))
(when ifaces
(emit (concat "Implements: " (cider-font-lock-as 'java-mode (car ifaces))))
(dolist (iface (cdr ifaces))
(emit (concat " "(cider-font-lock-as 'java-mode iface)))))
(when (or super ifaces)
(insert "\n"))
(when-let* ((forms (or forms args)))
(dolist (form forms)
(insert " ")
(emit (cider-font-lock-as-clojure form))))
(when special
(emit "Special Form" 'font-lock-keyword-face))
(when macro
(emit "Macro" 'font-lock-variable-name-face))
(when builtin
(emit "Built-in" 'font-lock-keyword-face))
(when added
(emit (concat "Added in " added) 'font-lock-comment-face))
(when depr
(emit (concat "Deprecated in " depr) 'font-lock-keyword-face))
(if class
(cider-docview-render-java-doc (current-buffer) doc)
(emit (concat " " doc)))
(when url
(insert "\n Please see ")
(insert-text-button url
'url url
'follow-link t
'action (lambda (x)
(browse-url (button-get x 'url))))
(insert "\n"))
(when javadoc
(insert "\n\nFor additional documentation, see the ")
(insert-text-button "Javadoc"
'url javadoc
'follow-link t
'action (lambda (x)
(browse-url (button-get x 'url))))
(insert ".\n"))
(insert "\n")
(when spec
(emit "Spec:" 'font-lock-function-name-face)
(insert (cider-browse-spec--pprint-indented spec))
(insert "\n\n")
(insert-text-button "Browse spec"
'follow-link t
'action (lambda (_)
(cider-browse-spec (format "%s/%s" ns name))))
(insert "\n\n"))
(if (and cider-docview-file (not (string= cider-docview-file "")))
(progn
(insert (propertize (if class java-name clj-name)
'font-lock-face 'font-lock-function-name-face)
" is defined in ")
(insert-text-button (cider--abbreviate-file-protocol cider-docview-file)
'follow-link t
'action (lambda (_x)
(cider-docview-source)))
(insert "."))
(insert "Definition location unavailable."))
(when see-also
(insert "\n\n Also see: ")
(mapc (lambda (ns-sym)
(let* ((ns-sym-split (split-string ns-sym "/"))
(see-also-ns (car ns-sym-split))
(see-also-sym (cadr ns-sym-split))
;; if the var belongs to the same namespace,
;; we omit the namespace to save some screen space
(symbol (if (equal ns see-also-ns) see-also-sym ns-sym)))
(insert-text-button symbol
'type 'help-xref
'help-function (apply-partially #'cider-doc-lookup symbol)))
(insert " "))
see-also))
(cider--doc-make-xrefs)
(let ((beg (point-min))
(end (point-max)))
(nrepl-dict-map (lambda (k v)
(put-text-property beg end k v))
info)))
(current-buffer))))
(declare-function cider-set-buffer-ns "cider-mode")
(defun cider-docview-render (buffer symbol info)
"Emit into BUFFER formatted documentation for SYMBOL's INFO."
(with-current-buffer buffer
(let ((javadoc (nrepl-dict-get info "javadoc"))
(file (nrepl-dict-get info "file"))
(line (nrepl-dict-get info "line"))
(ns (nrepl-dict-get info "ns"))
(inhibit-read-only t))
(cider-docview-mode)
(cider-set-buffer-ns ns)
(setq-local cider-docview-symbol symbol)
(setq-local cider-docview-javadoc-url javadoc)
(setq-local cider-docview-file file)
(setq-local cider-docview-line line)
(remove-overlays)
(cider-docview-render-info buffer info)
(goto-char (point-min))
(current-buffer))))
(provide 'cider-doc)
;;; cider-doc.el ends here
;;; cider-eldoc.el --- eldoc support for Clojure -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; eldoc support for Clojure.
;;; Code:
(require 'cider-client)
(require 'cider-common) ; for cider-symbol-at-point
(require 'subr-x)
(require 'cider-util)
(require 'nrepl-dict)
(require 'seq)
(require 'eldoc)
(defvar cider-extra-eldoc-commands '("yas-expand")
"Extra commands to be added to eldoc's safe commands list.")
(defcustom cider-eldoc-max-num-sexps-to-skip 30
"Max number of sexps to skip while searching the beginning of current sexp."
:type 'integer
:group 'cider
:package-version '(cider . "0.10.1"))
(defvar-local cider-eldoc-last-symbol nil
"The eldoc information for the last symbol we checked.")
(defcustom cider-eldoc-ns-function #'identity
"A function that returns a ns string to be used by eldoc.
Takes one argument, a namespace name.
For convenience, some functions are already provided for this purpose:
`cider-abbreviate-ns', and `cider-last-ns-segment'."
:type '(choice (const :tag "Full namespace" identity)
(const :tag "Abbreviated namespace" cider-abbreviate-ns)
(const :tag "Last name in namespace" cider-last-ns-segment)
(function :tag "Custom function"))
:group 'cider
:package-version '(cider . "0.13.0"))
(defcustom cider-eldoc-max-class-names-to-display 3
"The maximum number of classes to display in an eldoc string.
An eldoc string for Java interop forms can have a number of classes prefixed to
it, when the form belongs to more than 1 class. When, not nil we only display
the names of first `cider-eldoc-max-class-names-to-display' classes and add
a \"& x more\" suffix. Otherwise, all the classes are displayed."
:type 'integer
:safe #'integerp
:group 'cider
:package-version '(cider . "0.13.0"))
(defcustom cider-eldoc-display-for-symbol-at-point t
"When non-nil, display eldoc for symbol at point if available.
So in (map inc ...) when the cursor is over inc its eldoc would be
displayed. When nil, always display eldoc for first symbol of the sexp."
:type 'boolean
:safe #'booleanp
:group 'cider
:package-version '(cider . "0.13.0"))
(defcustom cider-eldoc-display-context-dependent-info nil
"When non-nil, display context dependent info in the eldoc where possible.
CIDER will try to add expected function arguments based on the current context,
for example for the datomic.api/q function where it will show the expected
inputs of the query at point."
:type 'boolean
:group 'cider
:package-version '(cider . "0.15.0"))
(defun cider--eldoc-format-class-names (class-names)
"Return a formatted CLASS-NAMES prefix string.
CLASS-NAMES is a list of classes to which a Java interop form belongs.
Only keep the first `cider-eldoc-max-class-names-to-display' names, and
add a \"& x more\" suffix. Return nil if the CLASS-NAMES list is empty or
mapping `cider-eldoc-ns-function' on it returns an empty list."
(when-let* ((eldoc-class-names (seq-remove #'null (mapcar (apply-partially cider-eldoc-ns-function) class-names)))
(eldoc-class-names-length (length eldoc-class-names)))
(cond
;; truncate class-names list and then format it
((and cider-eldoc-max-class-names-to-display
(> eldoc-class-names-length cider-eldoc-max-class-names-to-display))
(format "(%s & %s more)"
(thread-first
eldoc-class-names
(seq-take cider-eldoc-max-class-names-to-display)
(string-join " ")
(cider-propertize 'ns))
(- eldoc-class-names-length cider-eldoc-max-class-names-to-display)))
;; format the whole list but add surrounding parentheses
((> eldoc-class-names-length 1)
(format "(%s)"
(thread-first
eldoc-class-names
(string-join " ")
(cider-propertize 'ns))))
;; don't add the parentheses
(t (format "%s" (car eldoc-class-names))))))
(defun cider-eldoc-format-thing (ns symbol thing type)
"Format the eldoc subject defined by NS, SYMBOL, THING and TYPE.
THING represents the thing at point which triggered eldoc. Normally NS and
SYMBOL are used (they are derived from THING), but when empty we fallback to
THING (e.g. for Java methods). Format it as a function, if FUNCTION-P
is non-nil. Else format it as a variable."
(if-let* ((method-name (if (and symbol (not (string= symbol "")))
symbol
thing))
(propertized-method-name (cider-propertize method-name type))
(ns-or-class (if (and ns (stringp ns))
(funcall cider-eldoc-ns-function ns)
(cider--eldoc-format-class-names ns))))
(format "%s/%s"
;; we set font-lock properties of classes in `cider--eldoc-format-class-names'
;; to avoid font locking the parentheses and "& x more"
;; so we only propertize ns-or-class if not already done
(if (get-text-property 1 'face ns-or-class)
;; it is already propertized
ns-or-class
(cider-propertize ns-or-class 'ns))
propertized-method-name)
;; in case ns-or-class is nil
propertized-method-name))
(defun cider-eldoc-format-sym-doc (var ns docstring)
"Return the formatted eldoc string for VAR and DOCSTRING.
Consider the value of `eldoc-echo-area-use-multiline-p' while formatting.
If the entire line cannot fit in the echo area, the var name may be
truncated or eliminated entirely from the output to make room for the
description.
Try to truncate the var with various strategies, so that the var and
the docstring can be displayed in the minibuffer without resizing the window.
We start with `cider-abbreviate-ns' and `cider-last-ns-segment'.
Next, if the var is in current namespace, we remove NS from the eldoc string.
Otherwise, only the docstring is returned."
(let* ((ea-multi eldoc-echo-area-use-multiline-p)
;; Subtract 1 from window width since emacs will not write
;; any chars to the last column, or in later versions, will
;; cause a wraparound and resize of the echo area.
(ea-width (1- (window-width (minibuffer-window))))
(strip (- (+ (length var) (length docstring)) ea-width))
(newline (string-match-p "\n" docstring))
;; Truncated var can be ea-var long
;; Subtract 2 to account for the : and / added when including
;; the namespace prefixed form in eldoc string
(ea-var (- (- ea-width (length docstring)) 2)))
(cond
((or (eq ea-multi t)
(and (<= strip 0) (null newline))
(and ea-multi (or (> (length docstring) ea-width) newline)))
(format "%s: %s" var docstring))
;; Now we have to truncate either the docstring or the var
(newline (cider-eldoc-format-sym-doc var ns (substring docstring 0 newline)))
;; Only return the truncated docstring
((> (length docstring) ea-width)
(substring docstring 0 ea-width))
;; Try to truncate the var with cider-abbreviate-ns
((<= (length (cider-abbreviate-ns var)) ea-var)
(format "%s: %s" (cider-abbreviate-ns var) docstring))
;; Try to truncate var with cider-last-ns-segment
((<= (length (cider-last-ns-segment var)) ea-var)
(format "%s: %s" (cider-last-ns-segment var) docstring))
;; If the var is in current namespace, we try to truncate the var by
;; skipping the namespace from the returned eldoc string
((and (string-equal ns (cider-current-ns))
(<= (- (length var) (length ns)) ea-var))
(format "%s: %s"
(replace-regexp-in-string (format "%s/" ns) "" var)
docstring))
;; We couldn't fit the var and docstring in the available space,
;; so we just display the docstring
(t docstring))))
(defun cider-eldoc-format-variable (thing eldoc-info)
"Return the formatted eldoc string for a variable.
THING is the variable name. ELDOC-INFO is a p-list containing the eldoc
information."
(let* ((ns (lax-plist-get eldoc-info "ns"))
(symbol (lax-plist-get eldoc-info "symbol"))
(docstring (lax-plist-get eldoc-info "docstring"))
(formatted-var (cider-eldoc-format-thing ns symbol thing 'var)))
(when docstring
(cider-eldoc-format-sym-doc formatted-var ns docstring))))
(defun cider-eldoc-format-function (thing pos eldoc-info)
"Return the formatted eldoc string for a function.
THING is the function name. POS is the argument-index of the functions
arglists. ELDOC-INFO is a p-list containing the eldoc information."
(let ((ns (lax-plist-get eldoc-info "ns"))
(symbol (lax-plist-get eldoc-info "symbol"))
(arglists (lax-plist-get eldoc-info "arglists")))
(format "%s: %s"
(cider-eldoc-format-thing ns symbol thing 'fn)
(cider-eldoc-format-arglist arglists pos))))
(defun cider-highlight-args (arglist pos)
"Format the the function ARGLIST for eldoc.
POS is the index of the currently highlighted argument."
(let* ((rest-pos (cider--find-rest-args-position arglist))
(i 0))
(mapconcat
(lambda (arg)
(let ((argstr (format "%s" arg)))
(if (string= arg "&")
argstr
(prog1
(if (or (= (1+ i) pos)
(and rest-pos
(> (1+ i) rest-pos)
(> pos rest-pos)))
(propertize argstr 'face
'eldoc-highlight-function-argument)
argstr)
(setq i (1+ i)))))) arglist " ")))
(defun cider--find-rest-args-position (arglist)
"Find the position of & in the ARGLIST vector."
(seq-position arglist "&"))
(defun cider-highlight-arglist (arglist pos)
"Format the ARGLIST for eldoc.
POS is the index of the argument to highlight."
(concat "[" (cider-highlight-args arglist pos) "]"))
(defun cider-eldoc-format-arglist (arglist pos)
"Format all the ARGLIST for eldoc.
POS is the index of current argument."
(concat "("
(mapconcat (lambda (args) (cider-highlight-arglist args pos))
arglist
" ")
")"))
(defun cider-eldoc-beginning-of-sexp ()
"Move to the beginning of current sexp.
Return the number of nested sexp the point was over or after. Return nil
if the maximum number of sexps to skip is exceeded."
(let ((parse-sexp-ignore-comments t)
(num-skipped-sexps 0))
(condition-case _
(progn
;; First account for the case the point is directly over a
;; beginning of a nested sexp.
(condition-case _
(let ((p (point)))
(forward-sexp -1)
(forward-sexp 1)
(when (< (point) p)
(setq num-skipped-sexps 1)))
(error))
(while
(let ((p (point)))
(clojure-backward-logical-sexp 1)
(when (< (point) p)
(setq num-skipped-sexps
(unless (and cider-eldoc-max-num-sexps-to-skip
(>= num-skipped-sexps
cider-eldoc-max-num-sexps-to-skip))
;; Without the above guard,
;; `cider-eldoc-beginning-of-sexp' could traverse the
;; whole buffer when the point is not within a
;; list. This behavior is problematic especially with
;; a buffer containing a large number of
;; non-expressions like a REPL buffer.
(1+ num-skipped-sexps)))))))
(error))
num-skipped-sexps))
(defun cider-eldoc-thing-type (eldoc-info)
"Return the type of the ELDOC-INFO being displayed by eldoc.
It can be a function or var now."
(pcase (lax-plist-get eldoc-info "type")
("function" 'fn)
("special-form" 'special-form)
("macro" 'macro)
("method" 'method)
("variable" 'var)))
(defun cider-eldoc-info-at-point ()
"Return eldoc info at point.
First go to the beginning of the sexp and check if the eldoc is to be
considered (i.e sexp is a method call) and not a map or vector literal.
Then go back to the point and return its eldoc."
(save-excursion
(unless (cider-in-comment-p)
(let* ((current-point (point)))
(cider-eldoc-beginning-of-sexp)
(unless (member (or (char-before (point)) 0) '(?\" ?\{ ?\[))
(goto-char current-point)
(when-let* ((eldoc-info (cider-eldoc-info
(cider--eldoc-remove-dot (cider-symbol-at-point)))))
`("eldoc-info" ,eldoc-info
"thing" ,(cider-symbol-at-point)
"pos" 0)))))))
(defun cider-eldoc-info-at-sexp-beginning ()
"Return eldoc info for first symbol in the sexp."
(save-excursion
(when-let* ((beginning-of-sexp (cider-eldoc-beginning-of-sexp))
;; If we are at the beginning of function name, this will be -1
(argument-index (max 0 (1- beginning-of-sexp))))
(unless (or (memq (or (char-before (point)) 0)
'(?\" ?\{ ?\[))
(cider-in-comment-p))
(when-let* ((eldoc-info (cider-eldoc-info
(cider--eldoc-remove-dot (cider-symbol-at-point)))))
`("eldoc-info" ,eldoc-info
"thing" ,(cider-symbol-at-point)
"pos" ,argument-index))))))
(defun cider-eldoc-info-in-current-sexp ()
"Return eldoc information from the sexp.
If `cider-eldoc-display-for-symbol-at-point' is non-nil and
the symbol at point has a valid eldoc available, return that.
Otherwise return the eldoc of the first symbol of the sexp."
(or (when cider-eldoc-display-for-symbol-at-point
(cider-eldoc-info-at-point))
(cider-eldoc-info-at-sexp-beginning)))
(defun cider-eldoc--convert-ns-keywords (thing)
"Convert THING values that match ns macro keywords to function names."
(pcase thing
(":import" "clojure.core/import")
(":refer-clojure" "clojure.core/refer-clojure")
(":use" "clojure.core/use")
(":refer" "clojure.core/refer")
(_ thing)))
(defun cider-eldoc-info (thing)
"Return the info for THING.
This includes the arglist and ns and symbol name (if available)."
(let ((thing (cider-eldoc--convert-ns-keywords thing)))
(when (and (cider-nrepl-op-supported-p "eldoc")
thing
;; ignore blank things
(not (string-blank-p thing))
;; ignore string literals
(not (string-prefix-p "\"" thing))
;; ignore regular expressions
(not (string-prefix-p "#" thing))
;; ignore chars
(not (string-prefix-p "\\" thing))
;; ignore numbers
(not (string-match-p "^[0-9]" thing)))
;; check if we can used the cached eldoc info
(cond
;; handle keywords for map access
((string-prefix-p ":" thing) (list "symbol" thing
"type" "function"
"arglists" '(("map") ("map" "not-found"))))
;; handle Classname. by displaying the eldoc for new
((string-match-p "^[A-Z].+\\.$" thing) (list "symbol" thing
"type" "function"
"arglists" '(("args*"))))
;; generic case
(t (if (equal thing (car cider-eldoc-last-symbol))
(cadr cider-eldoc-last-symbol)
(when-let* ((eldoc-info (cider-sync-request:eldoc thing)))
(let* ((arglists (nrepl-dict-get eldoc-info "eldoc"))
(docstring (nrepl-dict-get eldoc-info "docstring"))
(type (nrepl-dict-get eldoc-info "type"))
(ns (nrepl-dict-get eldoc-info "ns"))
(class (nrepl-dict-get eldoc-info "class"))
(name (nrepl-dict-get eldoc-info "name"))
(member (nrepl-dict-get eldoc-info "member"))
(ns-or-class (if (and ns (not (string= ns "")))
ns
class))
(name-or-member (if (and name (not (string= name "")))
name
(format ".%s" member)))
(eldoc-plist (list "ns" ns-or-class
"symbol" name-or-member
"arglists" arglists
"docstring" docstring
"type" type)))
;; add context dependent args if requested by defcustom
;; do not cache this eldoc info to avoid showing info
;; of the previous context
(if cider-eldoc-display-context-dependent-info
(cond
;; add inputs of datomic query
((and (equal ns-or-class "datomic.api")
(equal name-or-member "q"))
(let ((arglists (lax-plist-get eldoc-plist "arglists")))
(lax-plist-put eldoc-plist "arglists"
(cider--eldoc-add-datomic-query-inputs-to-arglists arglists))))
;; if none of the clauses is successful, do cache the eldoc
(t (setq cider-eldoc-last-symbol (list thing eldoc-plist))))
;; middleware eldoc lookups are expensive, so we
;; cache the last lookup. This eliminates the need
;; for extra middleware requests within the same sexp.
(setq cider-eldoc-last-symbol (list thing eldoc-plist)))
eldoc-plist))))))))
(defun cider--eldoc-remove-dot (sym)
"Remove the preceding \".\" from a namespace qualified SYM and return sym.
Only useful for interop forms. Clojure forms would be returned unchanged."
(when sym (replace-regexp-in-string "/\\." "/" sym)))
(defun cider--eldoc-edn-file-p (file-name)
"Check whether FILE-NAME is representing an EDN file."
(and file-name (equal (file-name-extension file-name) "edn")))
(defun cider--eldoc-add-datomic-query-inputs-to-arglists (arglists)
"Add the expected inputs of the datomic query to the ARGLISTS."
(if (cider-second-sexp-in-list)
(let* ((query (cider-second-sexp-in-list))
(query-inputs (nrepl-dict-get
(cider-sync-request:eldoc-datomic-query query)
"inputs")))
(if query-inputs
(thread-first
(thread-last arglists
(car)
(remove "&")
(remove "inputs"))
(append (car query-inputs))
(list))
arglists))
arglists))
(defun cider-eldoc (&rest _ignored)
"Backend function for eldoc to show argument list in the echo area."
(when (and (cider-connected-p)
;; don't clobber an error message in the minibuffer
(not (member last-command '(next-error previous-error)))
;; don't try to provide eldoc in EDN buffers
(not (cider--eldoc-edn-file-p buffer-file-name)))
(let* ((sexp-eldoc-info (cider-eldoc-info-in-current-sexp))
(eldoc-info (lax-plist-get sexp-eldoc-info "eldoc-info"))
(pos (lax-plist-get sexp-eldoc-info "pos"))
(thing (lax-plist-get sexp-eldoc-info "thing")))
(when eldoc-info
(if (eq (cider-eldoc-thing-type eldoc-info) 'var)
(cider-eldoc-format-variable thing eldoc-info)
(cider-eldoc-format-function thing pos eldoc-info))))))
(defun cider-eldoc-setup ()
"Setup eldoc in the current buffer.
eldoc mode has to be enabled for this to have any effect."
;; Emacs 28.1 changes the way eldoc is setup.
;; There you can have multiple eldoc functions.
(if (boundp 'eldoc-documentation-functions)
(add-hook 'eldoc-documentation-functions #'cider-eldoc nil t)
(setq-local eldoc-documentation-function #'cider-eldoc))
(apply #'eldoc-add-command cider-extra-eldoc-commands))
(provide 'cider-eldoc)
;;; cider-eldoc.el ends here
;;; cider-eval.el --- Interactive evaluation (compilation) functionality -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; Arne Brasseur <arne@arnebraasseur.net>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; This file contains CIDER's interactive evaluation (compilation) functionality.
;; Although Clojure doesn't really have the concept of evaluation (only
;; compilation), we're using everywhere in the code the term evaluation for
;; brevity (and to be in line with the naming employed by other similar modes).
;;
;; This files also contains all the logic related to displaying errors and
;; evaluation warnings.
;;
;; Pretty much all of the commands here are meant to be used mostly from
;; `cider-mode', but some of them might make sense in other contexts as well.
;;; Code:
(require 'ansi-color)
(require 'cl-lib)
(require 'compile)
(require 'map)
(require 'seq)
(require 'subr-x)
(require 'clojure-mode)
(require 'cider-client)
(require 'cider-common)
(require 'cider-jar)
(require 'cider-overlays)
(require 'cider-popup)
(require 'cider-repl)
(require 'cider-stacktrace)
(require 'cider-util)
(defconst cider-read-eval-buffer "*cider-read-eval*")
(defconst cider-result-buffer "*cider-result*")
(defcustom cider-show-error-buffer t
"Control the popup behavior of cider stacktraces.
The following values are possible t or 'always, 'except-in-repl,
'only-in-repl. Any other value, including nil, will cause the stacktrace
not to be automatically shown.
Irrespective of the value of this variable, the `cider-error-buffer' is
always generated in the background. Use `cider-selector' to
navigate to this buffer."
:type '(choice (const :tag "always" t)
(const except-in-repl)
(const only-in-repl)
(const :tag "never" nil))
:group 'cider)
(defcustom cider-auto-jump-to-error t
"Control the cursor jump behavior in compilation error buffer.
When non-nil automatically jump to error location during interactive
compilation. When set to 'errors-only, don't jump to warnings.
When set to nil, don't jump at all."
:type '(choice (const :tag "always" t)
(const errors-only)
(const :tag "never" nil))
:group 'cider
:package-version '(cider . "0.7.0"))
(defcustom cider-auto-select-error-buffer t
"Controls whether to auto-select the error popup buffer."
:type 'boolean
:group 'cider)
(defcustom cider-auto-track-ns-form-changes t
"Controls whether to auto-evaluate a source buffer's ns form when changed.
When non-nil CIDER will check for ns form changes before each eval command.
When nil the users are expected to take care of the re-evaluating updated
ns forms manually themselves."
:type 'boolean
:group 'cider
:package-version '(cider . "0.15.0"))
(defcustom cider-auto-inspect-after-eval t
"Controls whether to auto-update the inspector buffer after eval.
Only applies when the *cider-inspect* buffer is currently visible."
:type 'boolean
:group 'cider
:package-version '(cider . "0.25.0"))
(defcustom cider-save-file-on-load 'prompt
"Controls whether to prompt to save the file when loading a buffer.
If nil, files are not saved.
If 'prompt, the user is prompted to save the file if it's been modified.
If t, save the file without confirmation."
:type '(choice (const prompt :tag "Prompt to save the file if it's been modified")
(const nil :tag "Don't save the file")
(const t :tag "Save the file without confirmation"))
:group 'cider
:package-version '(cider . "0.6.0"))
(defcustom cider-file-loaded-hook nil
"List of functions to call when a load file has completed."
:type 'hook
:group 'cider
:package-version '(cider . "0.1.7"))
(defconst cider-output-buffer "*cider-out*")
(defcustom cider-interactive-eval-output-destination 'repl-buffer
"The destination for stdout and stderr produced from interactive evaluation."
:type '(choice (const output-buffer)
(const repl-buffer))
:group 'cider
:package-version '(cider . "0.7.0"))
(defface cider-error-highlight-face
'((((supports :underline (:style wave)))
(:underline (:style wave :color "red") :inherit unspecified))
(t (:inherit font-lock-warning-face :underline t)))
"Face used to highlight compilation errors in Clojure buffers."
:group 'cider)
(defface cider-warning-highlight-face
'((((supports :underline (:style wave)))
(:underline (:style wave :color "yellow") :inherit unspecified))
(t (:inherit font-lock-warning-face :underline (:color "yellow"))))
"Face used to highlight compilation warnings in Clojure buffers."
:group 'cider)
(defcustom cider-comment-prefix ";; => "
"The prefix to insert before the first line of commented output."
:type 'string
:group 'cider
:package-version '(cider . "0.16.0"))
(defcustom cider-comment-continued-prefix ";; "
"The prefix to use on the second and subsequent lines of commented output."
:type 'string
:group 'cider
:package-version '(cider . "0.16.0"))
(defcustom cider-comment-postfix ""
"The postfix to be appended after the final line of commented output."
:type 'string
:group 'cider
:package-version '(cider . "0.16.0"))
(defcustom cider-eval-register ?e
"The text register assigned to the most recent evaluation result.
When non-nil, the return value of all CIDER eval commands are
automatically written into this register."
:type '(choice character
(const nil))
:group 'cider
:package-version '(cider . "1.4.0"))
;;; Utilities
(defun cider--clear-compilation-highlights ()
"Remove compilation highlights."
(remove-overlays (point-min) (point-max) 'cider-note-p t))
(defun cider-clear-compilation-highlights (&optional arg)
"Remove compilation highlights.
When invoked with a prefix ARG the command doesn't prompt for confirmation."
(interactive "P")
(when (or arg (y-or-n-p "Are you sure you want to clear the compilation highlights? "))
(cider--clear-compilation-highlights)))
(defun cider--quit-error-window ()
"Buries the `cider-error-buffer' and quits its containing window."
(when-let* ((error-win (get-buffer-window cider-error-buffer)))
(save-excursion
(quit-window nil error-win))))
;;; Sideloader
;;
;; nREPL includes sideloader middleware which provides a Java classloader that
;; is able to dynamically load classes and resources at runtime by interacting
;; with the nREPL client (as opposed to using the classpath of the JVM hosting
;; nREPL server).
;;
;; This performs a similar functionality as the load-file
;; operation, where we can load Clojure namespaces (as source files) or Java
;; classes (as bytecode) by simply requiring or importing them.
;;
;; See https://nrepl.org/nrepl/design/middleware.html#sideloading
(defcustom cider-sideloader-path nil
"List of directories and jar files to scan for sideloader resources.
When not set the cider-nrepl jar will be added automatically when upgrading
an nREPL connection."
:type 'list
:group 'cider
:package-version '(cider . "1.2.0"))
(defcustom cider-dynload-cider-nrepl-version nil
"Version of the cider-nrepl jar used for dynamically upgrading a connection.
Defaults to `cider-required-middleware-version'."
:type 'string
:group 'cider
:package-version '(cider . "1.2.0"))
(defun cider-read-bytes (path)
"Read binary data from PATH.
Return the binary data as unibyte string."
;; based on f-read-bytes
(with-temp-buffer
(set-buffer-multibyte nil)
(setq buffer-file-coding-system 'binary)
(insert-file-contents-literally path nil)
(buffer-substring-no-properties (point-min) (point-max))))
(defun cider-retrieve-resource (dirs name)
"Find a resource NAME in a list DIRS of directories or jar files.
Similar to a classpath lookup. Returns the file contents as a string."
(seq-some
(lambda (path)
(cond
((file-directory-p path)
(let ((expanded (expand-file-name name path)))
(when (file-exists-p expanded)
(cider-read-bytes expanded))))
((and (file-exists-p path) (string-suffix-p ".jar" path))
(cider-jar-retrieve-resource path name))))
dirs))
(defun cider-provide-file (file)
"Provide FILE in a format suitable for sideloading."
(let ((contents (cider-retrieve-resource cider-sideloader-path file)))
(if contents
(base64-encode-string contents 'no-line-breaks)
;; if we can't find the file we should return an empty string
(base64-encode-string ""))))
(defun cider-sideloader-lookup-handler ()
"Make a sideloader-lookup handler."
(lambda (response)
(nrepl-dbind-response response (id status type name)
(if status
(when (member "sideloader-lookup" status)
(cider-request:sideloader-provide id type name))))))
(defun cider-add-middleware-handler (continue)
"Make a add-middleware handler.
CONTINUE is an optional continuation function."
(lambda (response)
(nrepl-dbind-response response (status unresolved-middleware) ;; id middleware
(when unresolved-middleware
(seq-do
(lambda (mw)
(cider-repl-emit-interactive-stderr
(concat "WARNING: middleware " mw " was not found or failed to load.\n")))
unresolved-middleware))
(when (and status (member "done" status) continue)
(funcall continue)))))
(defun cider-request:sideloader-start (&optional connection tooling)
"Perform the nREPL \"sideloader-start\" op.
If CONNECTION is nil, use `cider-current-repl'.
If TOOLING is truthy then the operation is performed over the tooling
session, rather than the regular session."
(cider-ensure-op-supported "sideloader-start")
(cider-nrepl-send-request `("op" "sideloader-start")
(cider-sideloader-lookup-handler)
connection
tooling))
(defun cider-request:sideloader-provide (id type file &optional connection)
"Perform the nREPL \"sideloader-provide\" op for ID, TYPE and FILE.
If CONNECTION is nil, use `cider-current-repl'."
(cider-nrepl-send-request `("id" ,id
"op" "sideloader-provide"
"type" ,type
"name" ,file
"content" ,(cider-provide-file file))
(cider-sideloader-lookup-handler)
connection))
(defun cider-sideloader-start (&optional connection)
"Start nREPL's sideloader.
If CONNECTION is nil, use `cider-current-repl'."
(interactive)
(message "Starting nREPL's sideloader")
(cider-request:sideloader-start connection)
(cider-request:sideloader-start connection 'tooling))
(defvar cider-nrepl-middlewares
'("cider.nrepl/wrap-apropos"
"cider.nrepl/wrap-classpath"
"cider.nrepl/wrap-clojuredocs"
"cider.nrepl/wrap-complete"
"cider.nrepl/wrap-content-type"
"cider.nrepl/wrap-debug"
"cider.nrepl/wrap-enlighten"
"cider.nrepl/wrap-format"
"cider.nrepl/wrap-info"
"cider.nrepl/wrap-inspect"
"cider.nrepl/wrap-macroexpand"
"cider.nrepl/wrap-ns"
"cider.nrepl/wrap-out"
"cider.nrepl/wrap-slurp"
"cider.nrepl/wrap-profile"
"cider.nrepl/wrap-refresh"
"cider.nrepl/wrap-resource"
"cider.nrepl/wrap-spec"
"cider.nrepl/wrap-stacktrace"
"cider.nrepl/wrap-test"
"cider.nrepl/wrap-trace"
"cider.nrepl/wrap-tracker"
"cider.nrepl/wrap-undef"
"cider.nrepl/wrap-version"
"cider.nrepl/wrap-xref"))
(defun cider-request:add-middleware (middlewares
&optional connection tooling continue)
"Use the nREPL dynamic loader to add MIDDLEWARES to the nREPL session.
- If CONNECTION is nil, use `cider-current-repl'.
- If TOOLING it truthy, use the tooling session instead of the main session.
- CONTINUE is an optional continuation function, which will be called when the
add-middleware op has finished successfully."
(cider-nrepl-send-request `("op" "add-middleware"
"middleware" ,middlewares)
(cider-add-middleware-handler continue)
connection
tooling))
(defun cider-add-cider-nrepl-middlewares (&optional connection)
"Use dynamic loading to add the cider-nrepl middlewares to nREPL.
If CONNECTION is nil, use `cider-current-repl'."
(cider-request:add-middleware
cider-nrepl-middlewares connection nil
(lambda ()
;; When the main session is done adding middleware, then do the tooling
;; session. At this point all the namespaces have been sideloaded so this
;; is faster, we don't want these to race to sideload resources.
(cider-request:add-middleware
cider-nrepl-middlewares connection 'tooling
(lambda ()
;; Ask nREPL again what its capabilities are, so we know which new
;; operations are supported.
(nrepl--init-capabilities (or connection (cider-current-repl))))))))
(defvar cider-required-middleware-version)
(defun cider-upgrade-nrepl-connection (&optional connection)
"Sideload cider-nrepl middleware.
If CONNECTION is nil, use `cider-current-repl'."
(interactive)
(when (not cider-sideloader-path)
(setq cider-sideloader-path (list (cider-jar-find-or-fetch
"cider" "cider-nrepl"
(or cider-dynload-cider-nrepl-version
cider-required-middleware-version)))))
(cider-sideloader-start connection)
(cider-add-cider-nrepl-middlewares connection))
;;; Dealing with compilation (evaluation) errors and warnings
(defun cider-find-property (property &optional backward)
"Find the next text region which has the specified PROPERTY.
If BACKWARD is t, then search backward.
Returns the position at which PROPERTY was found, or nil if not found."
(let ((p (if backward
(previous-single-char-property-change (point) property)
(next-single-char-property-change (point) property))))
(when (and (not (= p (point-min))) (not (= p (point-max))))
p)))
(defun cider-jump-to-compilation-error (&optional _arg _reset)
"Jump to the line causing the current compilation error.
_ARG and _RESET are ignored, as there is only ever one compilation error.
They exist for compatibility with `next-error'."
(interactive)
(cl-labels ((goto-next-note-boundary
()
(let ((p (or (cider-find-property 'cider-note-p)
(cider-find-property 'cider-note-p t))))
(when p
(goto-char p)
(message "%s" (get-char-property p 'cider-note))))))
;; if we're already on a compilation error, first jump to the end of
;; it, so that we find the next error.
(when (get-char-property (point) 'cider-note-p)
(goto-next-note-boundary))
(goto-next-note-boundary)))
(defun cider--show-error-buffer-p ()
"Return non-nil if the error buffer must be shown on error.
Takes into account both the value of `cider-show-error-buffer' and the
currently selected buffer."
(let* ((selected-buffer (window-buffer (selected-window)))
(replp (with-current-buffer selected-buffer (derived-mode-p 'cider-repl-mode))))
(memq cider-show-error-buffer
(if replp
'(t always only-in-repl)
'(t always except-in-repl)))))
(defun cider-new-error-buffer (&optional mode error-types)
"Return an empty error buffer using MODE.
When deciding whether to display the buffer, takes into account not only
the value of `cider-show-error-buffer' and the currently selected buffer
but also the ERROR-TYPES of the error, which is checked against the
`cider-stacktrace-suppressed-errors' set.
When deciding whether to select the buffer, takes into account the value of
`cider-auto-select-error-buffer'."
(if (and (cider--show-error-buffer-p)
(not (cider-stacktrace-some-suppressed-errors-p error-types)))
(cider-popup-buffer cider-error-buffer cider-auto-select-error-buffer mode 'ancillary)
(cider-make-popup-buffer cider-error-buffer mode 'ancillary)))
(defun cider-emit-into-color-buffer (buffer value)
"Emit into color BUFFER the provided VALUE."
(with-current-buffer buffer
(let ((inhibit-read-only t)
(buffer-undo-list t))
(goto-char (point-max))
(insert (format "%s" value))
(ansi-color-apply-on-region (point-min) (point-max)))
(goto-char (point-min))))
(defun cider--handle-err-eval-response (response)
"Render eval RESPONSE into a new error buffer.
Uses the value of the `out' slot in RESPONSE."
(nrepl-dbind-response response (out)
(when out
(let ((error-buffer (cider-new-error-buffer)))
(cider-emit-into-color-buffer error-buffer out)
(with-current-buffer error-buffer
(compilation-minor-mode +1))))))
(defun cider-default-err-eval-handler ()
"Display the last exception without middleware support."
(cider--handle-err-eval-response
(cider-nrepl-sync-request:eval
"(clojure.stacktrace/print-cause-trace *e)")))
(defun cider--render-stacktrace-causes (causes &optional error-types)
"If CAUSES is non-nil, render its contents into a new error buffer.
Optional argument ERROR-TYPES contains a list which should determine the
op/situation that originated this error."
(when causes
(let ((error-buffer (cider-new-error-buffer #'cider-stacktrace-mode error-types)))
(cider-stacktrace-render error-buffer (reverse causes) error-types))))
(defun cider--handle-stacktrace-response (response causes)
"Handle stacktrace op RESPONSE, aggregating the result into CAUSES.
If RESPONSE contains a cause, cons it onto CAUSES and return that. If
RESPONSE is the final message (i.e. it contains a status), render CAUSES
into a new error buffer."
(nrepl-dbind-response response (class status)
(cond (class (cons response causes))
(status (cider--render-stacktrace-causes causes)))))
(defun cider-default-err-op-handler ()
"Display the last exception, with middleware support."
;; Causes are returned as a series of messages, which we aggregate in `causes'
(let (causes)
(cider-nrepl-send-request
(thread-last
(map-merge 'list
'(("op" "stacktrace"))
(cider--nrepl-print-request-map fill-column))
(seq-mapcat #'identity))
(lambda (response)
;; While the return value of `cider--handle-stacktrace-response' is not
;; meaningful for the last message, we do not need the value of `causes'
;; after it has been handled, so it's fine to set it unconditionally here
(setq causes (cider--handle-stacktrace-response response causes))))))
(defun cider-default-err-handler ()
"This function determines how the error buffer is shown.
It delegates the actual error content to the eval or op handler."
(if (cider-nrepl-op-supported-p "stacktrace")
(cider-default-err-op-handler)
(cider-default-err-eval-handler)))
;; The format of the error messages emitted by Clojure's compiler changed in
;; Clojure 1.10. That's why we're trying to match error messages to both the
;; old and the new format, by utilizing a combination of two different regular
;; expressions.
(defconst cider-clojure-1.10-error `(sequence
"Syntax error "
(minimal-match (zero-or-more anything))
"compiling "
(minimal-match (zero-or-more anything))
"at ("
(group-n 2 (minimal-match (zero-or-more anything)))
":"
(group-n 3 (one-or-more digit))
(optional ":" (group-n 4 (one-or-more digit)))
")."))
(defconst cider-clojure-1.9-error `(sequence
(zero-or-more anything)
", compiling:("
(group-n 2 (minimal-match (zero-or-more anything)))
":"
(group-n 3 (one-or-more digit))
(optional ":" (group-n 4 (one-or-more digit)))
")"))
(defconst cider-clojure-warning `(sequence
(minimal-match (zero-or-more anything))
(group-n 1 "warning")
", "
(group-n 2 (minimal-match (zero-or-more anything)))
":"
(group-n 3 (one-or-more digit))
(optional ":" (group-n 4 (one-or-more digit)))
" - "))
(defconst cider-clojure-compilation-regexp
(eval
`(rx bol (or ,cider-clojure-1.9-error
,cider-clojure-warning
,cider-clojure-1.10-error))
t))
(defvar cider-compilation-regexp
(list cider-clojure-compilation-regexp 2 3 4 '(1))
"Specifications for matching errors and warnings in Clojure stacktraces.
See `compilation-error-regexp-alist' for help on their format.")
(add-to-list 'compilation-error-regexp-alist-alist
(cons 'cider cider-compilation-regexp))
(add-to-list 'compilation-error-regexp-alist 'cider)
(defun cider-extract-error-info (regexp message)
"Extract error information with REGEXP against MESSAGE."
(let ((file (nth 1 regexp))
(line (nth 2 regexp))
(col (nth 3 regexp))
(type (nth 4 regexp))
(pat (car regexp)))
(when (string-match pat message)
;; special processing for type (1.2) style
(setq type (if (consp type)
(or (and (car type) (match-end (car type)) 1)
(and (cdr type) (match-end (cdr type)) 0)
2)))
(list
(when file
(let ((val (match-string-no-properties file message)))
(unless (string= val "NO_SOURCE_PATH") val)))
(when line (string-to-number (match-string-no-properties line message)))
(when col
(let ((val (match-string-no-properties col message)))
(when (and val (not (string-blank-p val))) (string-to-number val))))
(aref [cider-warning-highlight-face
cider-warning-highlight-face
cider-error-highlight-face]
(or type 2))
message))))
(defun cider--goto-expression-start ()
"Go to the beginning a list, vector, map or set outside of a string.
We do so by starting and the current position and proceeding backwards
until we find a delimiters that's not inside a string."
(if (and (looking-back "[])}]" (line-beginning-position))
(null (nth 3 (syntax-ppss))))
(backward-sexp)
(while (or (not (looking-at-p "[({[]"))
(nth 3 (syntax-ppss)))
(backward-char))))
(defun cider--find-last-error-location (message)
"Return the location (begin end buffer) from the Clojure error MESSAGE.
If location could not be found, return nil."
(save-excursion
(let ((info (cider-extract-error-info cider-compilation-regexp message)))
(when info
(let ((file (nth 0 info))
(line (nth 1 info))
(col (nth 2 info)))
(unless (or (not (stringp file))
(cider--tooling-file-p file))
(when-let* ((buffer (cider-find-file file)))
(with-current-buffer buffer
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(forward-line (1- line))
(move-to-column (or col 0))
(let ((begin (progn (if col (cider--goto-expression-start) (back-to-indentation))
(point)))
(end (progn (if col (forward-list) (move-end-of-line nil))
(point))))
(list begin end buffer))))))))))))
(defun cider-handle-compilation-errors (message eval-buffer)
"Highlight and jump to compilation error extracted from MESSAGE.
EVAL-BUFFER is the buffer that was current during user's interactive
evaluation command. Honor `cider-auto-jump-to-error'."
(when-let* ((loc (cider--find-last-error-location message))
(overlay (make-overlay (nth 0 loc) (nth 1 loc) (nth 2 loc)))
(info (cider-extract-error-info cider-compilation-regexp message)))
(let* ((face (nth 3 info))
(note (nth 4 info))
(auto-jump (if (eq cider-auto-jump-to-error 'errors-only)
(not (or (eq face 'cider-warning-highlight-face)
(string-match-p "warning" note)))
cider-auto-jump-to-error)))
(overlay-put overlay 'cider-note-p t)
(overlay-put overlay 'font-lock-face face)
(overlay-put overlay 'cider-note note)
(overlay-put overlay 'help-echo note)
(overlay-put overlay 'modification-hooks
(list (lambda (o &rest _args) (delete-overlay o))))
(when auto-jump
(with-current-buffer eval-buffer
(push-mark)
;; At this stage selected window commonly is *cider-error* and we need to
;; re-select the original user window. If eval-buffer is not
;; visible it was probably covered as a result of a small screen or user
;; configuration (https://github.com/clojure-emacs/cider/issues/847). In
;; that case we don't jump at all in order to avoid covering *cider-error*
;; buffer.
(when-let* ((win (get-buffer-window eval-buffer)))
(with-selected-window win
(cider-jump-to (nth 2 loc) (car loc)))))))))
;;; Interactive evaluation handlers
(defun cider-insert-eval-handler (&optional buffer)
"Make an nREPL evaluation handler for the BUFFER.
The handler simply inserts the result value in BUFFER."
(let ((eval-buffer (current-buffer))
(res ""))
(nrepl-make-response-handler (or buffer eval-buffer)
(lambda (_buffer value)
(with-current-buffer buffer
(insert value))
(when cider-eval-register
(setq res (concat res value))))
(lambda (_buffer out)
(cider-repl-emit-interactive-stdout out))
(lambda (_buffer err)
(cider-handle-compilation-errors err eval-buffer))
(lambda (_buffer)
(when cider-eval-register
(set-register cider-eval-register res))))))
(defun cider--emit-interactive-eval-output (output repl-emit-function)
"Emit output resulting from interactive code evaluation.
The OUTPUT can be sent to either a dedicated output buffer or the current
REPL buffer. This is controlled by `cider-interactive-eval-output-destination'.
REPL-EMIT-FUNCTION emits the OUTPUT."
(pcase cider-interactive-eval-output-destination
(`output-buffer (let ((output-buffer (or (get-buffer cider-output-buffer)
(cider-popup-buffer cider-output-buffer t))))
(cider-emit-into-popup-buffer output-buffer output)
(pop-to-buffer output-buffer)))
(`repl-buffer (funcall repl-emit-function output))
(_ (error "Unsupported value %s for `cider-interactive-eval-output-destination'"
cider-interactive-eval-output-destination))))
(defun cider-emit-interactive-eval-output (output)
"Emit OUTPUT resulting from interactive code evaluation.
The output can be send to either a dedicated output buffer or the current
REPL buffer. This is controlled via
`cider-interactive-eval-output-destination'."
(cider--emit-interactive-eval-output output 'cider-repl-emit-interactive-stdout))
(defun cider-emit-interactive-eval-err-output (output)
"Emit err OUTPUT resulting from interactive code evaluation.
The output can be send to either a dedicated output buffer or the current
REPL buffer. This is controlled via
`cider-interactive-eval-output-destination'."
(cider--emit-interactive-eval-output output 'cider-repl-emit-interactive-stderr))
(defun cider--make-fringe-overlays-for-region (beg end)
"Place eval indicators on all sexps between BEG and END."
(with-current-buffer (if (markerp end)
(marker-buffer end)
(current-buffer))
(save-excursion
(goto-char beg)
(remove-overlays beg end 'category 'cider-fringe-indicator)
(condition-case nil
(while (progn (clojure-forward-logical-sexp)
(and (<= (point) end)
(not (eobp))))
(cider--make-fringe-overlay (point)))
(scan-error nil)))))
(declare-function cider-inspect-last-result "cider-inspector")
(defun cider-interactive-eval-handler (&optional buffer place)
"Make an interactive eval handler for BUFFER.
PLACE is used to display the evaluation result.
If non-nil, it can be the position where the evaluated sexp ends,
or it can be a list with (START END) of the evaluated region.
Update the cider-inspector buffer with the evaluation result
when `cider-auto-inspect-after-eval' is non-nil."
(let* ((eval-buffer (current-buffer))
(beg (car-safe place))
(end (or (car-safe (cdr-safe place)) place))
(beg (when beg (copy-marker beg)))
(end (when end (copy-marker end)))
(fringed nil)
(res ""))
(nrepl-make-response-handler (or buffer eval-buffer)
(lambda (_buffer value)
(setq res (concat res value))
(cider--display-interactive-eval-result res end))
(lambda (_buffer out)
(cider-emit-interactive-eval-output out))
(lambda (_buffer err)
(cider-emit-interactive-eval-err-output err)
(unless cider-show-error-buffer
;; Display errors as temporary overlays
(let ((cider-result-use-clojure-font-lock nil))
(cider--display-interactive-eval-result
err end 'cider-error-overlay-face)))
(cider-handle-compilation-errors err eval-buffer))
(lambda (buffer)
(if beg
(unless fringed
(cider--make-fringe-overlays-for-region beg end)
(setq fringed t))
(cider--make-fringe-overlay end))
(when (and cider-auto-inspect-after-eval
(boundp 'cider-inspector-buffer)
(windowp (get-buffer-window cider-inspector-buffer 'visible)))
(cider-inspect-last-result)
(select-window (get-buffer-window buffer)))
(when cider-eval-register
(set-register cider-eval-register res))))))
(defun cider-load-file-handler (&optional buffer done-handler)
"Make a load file handler for BUFFER.
Optional argument DONE-HANDLER lambda will be run once load is complete."
(let ((eval-buffer (current-buffer))
(res ""))
(nrepl-make-response-handler (or buffer eval-buffer)
(lambda (buffer value)
(cider--display-interactive-eval-result value)
(when cider-eval-register
(setq res (concat res value)))
(when (buffer-live-p buffer)
(with-current-buffer buffer
(cider--make-fringe-overlays-for-region (point-min) (point-max))
(run-hooks 'cider-file-loaded-hook))))
(lambda (_buffer value)
(cider-emit-interactive-eval-output value))
(lambda (_buffer err)
(cider-emit-interactive-eval-err-output err)
(cider-handle-compilation-errors err eval-buffer))
(lambda (buffer)
(when cider-eval-register
(set-register cider-eval-register res))
(when done-handler
(funcall done-handler buffer)))
(lambda ()
(funcall nrepl-err-handler)))))
(defun cider-eval-print-handler (&optional buffer)
"Make a handler for evaluating and printing result in BUFFER."
;; NOTE: cider-eval-register behavior is not implemented here for performance reasons.
;; See https://github.com/clojure-emacs/cider/pull/3162
(nrepl-make-response-handler (or buffer (current-buffer))
(lambda (buffer value)
(with-current-buffer buffer
(insert
(if (derived-mode-p 'cider-clojure-interaction-mode)
(format "\n%s\n" value)
value))))
(lambda (_buffer out)
(cider-emit-interactive-eval-output out))
(lambda (_buffer err)
(cider-emit-interactive-eval-err-output err))
()))
(defun cider-eval-print-with-comment-handler (buffer location comment-prefix)
"Make a handler for evaluating and printing commented results in BUFFER.
LOCATION is the location marker at which to insert. COMMENT-PREFIX is the
comment prefix to use."
(let ((res ""))
(nrepl-make-response-handler buffer
(lambda (_buffer value)
(setq res (concat res value)))
(lambda (_buffer out)
(cider-emit-interactive-eval-output out))
(lambda (_buffer err)
(cider-emit-interactive-eval-err-output err))
(lambda (buffer)
(with-current-buffer buffer
(save-excursion
(goto-char (marker-position location))
(insert (concat comment-prefix
res "\n"))))
(when cider-eval-register
(set-register cider-eval-register res))))))
(defun cider-maybe-insert-multiline-comment (result comment-prefix continued-prefix comment-postfix)
"Insert eval RESULT at current location if RESULT is not empty.
RESULT will be preceded by COMMENT-PREFIX.
CONTINUED-PREFIX is inserted for each additional line of output.
COMMENT-POSTFIX is inserted after final text output."
(unless (string= result "")
(clojure-indent-line)
(let ((lines (split-string result "[\n]+" t))
(beg (point))
(col (current-indentation)))
;; only the first line gets the normal comment-prefix
(insert (concat comment-prefix (pop lines)))
(dolist (elem lines)
(insert (concat "\n" continued-prefix elem)))
(indent-rigidly beg (point) col)
(unless (string= comment-postfix "")
(insert comment-postfix)))))
(defun cider-eval-pprint-with-multiline-comment-handler (buffer location comment-prefix continued-prefix comment-postfix)
"Make a handler for evaluating and inserting results in BUFFER.
The inserted text is pretty-printed and region will be commented.
LOCATION is the location marker at which to insert.
COMMENT-PREFIX is the comment prefix for the first line of output.
CONTINUED-PREFIX is the comment prefix to use for the remaining lines.
COMMENT-POSTFIX is the text to output after the last line."
(let ((res ""))
(nrepl-make-response-handler
buffer
(lambda (_buffer value)
(setq res (concat res value)))
nil
(lambda (_buffer err)
(setq res (concat res err)))
(lambda (buffer)
(with-current-buffer buffer
(save-excursion
(goto-char (marker-position location))
;; edge case: defun at eob
(unless (bolp) (insert "\n"))
(cider-maybe-insert-multiline-comment res comment-prefix continued-prefix comment-postfix)))
(when cider-eval-register
(set-register cider-eval-register res)))
nil
nil
(lambda (_buffer warning)
(setq res (concat res warning))))))
(defun cider-popup-eval-handler (&optional buffer)
"Make a handler for printing evaluation results in popup BUFFER.
This is used by pretty-printing commands."
;; NOTE: cider-eval-register behavior is not implemented here for performance reasons.
;; See https://github.com/clojure-emacs/cider/pull/3162
(nrepl-make-response-handler
(or buffer (current-buffer))
(lambda (buffer value)
(cider-emit-into-popup-buffer buffer (ansi-color-apply value) nil t))
(lambda (_buffer out)
(cider-emit-interactive-eval-output out))
(lambda (_buffer err)
(cider-emit-interactive-eval-err-output err))
nil
nil
nil
(lambda (buffer warning)
(cider-emit-into-popup-buffer buffer warning 'font-lock-warning-face t))))
;;; Interactive valuation commands
(defvar cider-to-nrepl-filename-function
(with-no-warnings
(lambda (path)
(let ((path* (if (eq system-type 'cygwin)
(cygwin-convert-file-name-to-windows path)
path)))
(or (cider--translate-path-to-nrepl path*) path*))))
"Function to translate Emacs filenames to nREPL namestrings.")
(defun cider--prep-interactive-eval (form connection)
"Prepare the environment for an interactive eval of FORM in CONNECTION.
Ensure the current ns declaration has been evaluated (so that the ns
containing FORM exists). Cache ns-form in the current buffer unless FORM is
ns declaration itself. Clear any compilation highlights and kill the error
window."
(cider--clear-compilation-highlights)
(cider--quit-error-window)
(let ((cur-ns-form (cider-ns-form)))
(when (and cur-ns-form
(not (cider-ns-form-p form))
(cider-repl--ns-form-changed-p cur-ns-form connection))
(when cider-auto-track-ns-form-changes
;; The first interactive eval on a file can load a lot of libs. This can
;; easily lead to more than 10 sec.
(let ((nrepl-sync-request-timeout 30))
;; TODO: check for evaluation errors
(cider-nrepl-sync-request:eval cur-ns-form connection)))
;; cache at the end, in case of errors
(cider-repl--cache-ns-form cur-ns-form connection))))
(defvar-local cider-interactive-eval-override nil
"Function to call instead of `cider-interactive-eval'.")
(defun cider-interactive-eval (form &optional callback bounds additional-params)
"Evaluate FORM and dispatch the response to CALLBACK.
If the code to be evaluated comes from a buffer, it is preferred to use a
nil FORM, and specify the code via the BOUNDS argument instead.
This function is the main entry point in CIDER's interactive evaluation
API. Most other interactive eval functions should rely on this function.
If CALLBACK is nil use `cider-interactive-eval-handler'.
BOUNDS, if non-nil, is a list of two numbers marking the start and end
positions of FORM in its buffer.
ADDITIONAL-PARAMS is a map to be merged into the request message.
If `cider-interactive-eval-override' is a function, call it with the same
arguments and only proceed with evaluation if it returns nil."
(let ((form (or form (apply #'buffer-substring-no-properties bounds)))
(start (car-safe bounds))
(end (car-safe (cdr-safe bounds))))
(when (and start end)
;; NOTE: don't use `remove-overlays' as it splits and leaves behind
;; partial overlays, leading to duplicate eval results in some situations.
(dolist (ov (overlays-in start end))
(when (eq (overlay-get ov 'cider-temporary) t)
(delete-overlay ov))))
(unless (and cider-interactive-eval-override
(functionp cider-interactive-eval-override)
(funcall cider-interactive-eval-override form callback bounds))
(cider-map-repls :auto
(lambda (connection)
(cider--prep-interactive-eval form connection)
(cider-nrepl-request:eval
form
(or callback (cider-interactive-eval-handler nil bounds))
;; always eval ns forms in the user namespace
;; otherwise trying to eval ns form for the first time will produce an error
(if (cider-ns-form-p form) "user" (cider-current-ns))
(when start (line-number-at-pos start))
(when start (cider-column-number-at-pos start))
(seq-mapcat #'identity additional-params)
connection))))))
(defun cider-eval-region (start end)
"Evaluate the region between START and END."
(interactive "r")
(cider-interactive-eval nil
nil
(list start end)
(cider--nrepl-pr-request-map)))
(defun cider-eval-last-sexp (&optional output-to-current-buffer)
"Evaluate the expression preceding point.
If invoked with OUTPUT-TO-CURRENT-BUFFER, print the result in the current
buffer."
(interactive "P")
(cider-interactive-eval nil
(when output-to-current-buffer (cider-eval-print-handler))
(cider-last-sexp 'bounds)
(cider--nrepl-pr-request-map)))
(defun cider-eval-last-sexp-and-replace ()
"Evaluate the expression preceding point and replace it with its result."
(interactive)
(let ((last-sexp (cider-last-sexp)))
;; we have to be sure the evaluation won't result in an error
(cider-nrepl-sync-request:eval last-sexp)
;; seems like the sexp is valid, so we can safely kill it
(let ((opoint (point)))
(clojure-backward-logical-sexp)
(kill-region (point) opoint))
(cider-interactive-eval last-sexp
(cider-eval-print-handler)
nil
(cider--nrepl-pr-request-map))))
(defun cider-eval-list-at-point (&optional output-to-current-buffer)
"Evaluate the list (eg. a function call, surrounded by parens) around point.
If invoked with OUTPUT-TO-CURRENT-BUFFER, output the result to current buffer."
(interactive "P")
(save-excursion
(goto-char (cadr (cider-list-at-point 'bounds)))
(cider-eval-last-sexp output-to-current-buffer)))
(defun cider-eval-sexp-at-point (&optional output-to-current-buffer)
"Evaluate the expression around point.
If invoked with OUTPUT-TO-CURRENT-BUFFER, output the result to current buffer."
(interactive "P")
(save-excursion
(goto-char (cadr (cider-sexp-at-point 'bounds)))
(cider-eval-last-sexp output-to-current-buffer)))
(defun cider-tap-last-sexp (&optional output-to-current-buffer)
"Evaluate and tap the expression preceding point.
If invoked with OUTPUT-TO-CURRENT-BUFFER, print the result in the current
buffer."
(interactive "P")
(let ((tapped-form (concat "(clojure.core/doto " (cider-last-sexp) " (clojure.core/tap>))")))
(cider-interactive-eval tapped-form
(when output-to-current-buffer (cider-eval-print-handler))
nil
(cider--nrepl-pr-request-map))))
(defun cider-tap-sexp-at-point (&optional output-to-current-buffer)
"Evaluate and tap the expression around point.
If invoked with OUTPUT-TO-CURRENT-BUFFER, output the result to current buffer."
(interactive "P")
(save-excursion
(goto-char (cadr (cider-sexp-at-point 'bounds)))
(cider-tap-last-sexp output-to-current-buffer)))
(defvar-local cider-previous-eval-context nil
"The previous evaluation context if any.
That's set by commands like `cider-eval-last-sexp-in-context'.")
(defun cider--guess-eval-context ()
"Return context for `cider--eval-in-context'.
This is done by extracting all parent let bindings."
(save-excursion
(let ((res ""))
(condition-case nil
(while t
(backward-up-list)
(when (looking-at (rx "(" (or "when-let" "if-let" "let") (opt "*")
symbol-end (* space)
(group "["))) ;; binding vector
(let ((beg (match-end 1))
(end (save-excursion
(goto-char (match-beginning 1))
(forward-sexp 1)
(1- (point)))))
(setq res (concat (buffer-substring-no-properties beg end) ", " res)))))
(scan-error res)))))
(defun cider--eval-in-context (bounds &optional guess)
"Evaluate code at BOUNDS in user-provided evaluation context.
When GUESS is non-nil, attempt to extract the context from parent let-bindings."
(let* ((code (string-trim-right
(buffer-substring-no-properties (car bounds) (cadr bounds))))
(eval-context
(minibuffer-with-setup-hook (if guess #'beginning-of-buffer #'ignore)
(read-string "Evaluation context (let-style): "
(if guess (cider--guess-eval-context)
cider-previous-eval-context))))
(code (concat "(let [" eval-context "]\n " code ")")))
(setq-local cider-previous-eval-context eval-context)
(cider-interactive-eval code
nil
bounds
(cider--nrepl-pr-request-map))))
(defun cider-eval-last-sexp-in-context (guess)
"Evaluate the preceding sexp in user-supplied context.
The context is just a let binding vector (without the brackets).
The context is remembered between command invocations.
When GUESS is non-nil, or called interactively with \\[universal-argument],
attempt to extract the context from parent let-bindings."
(interactive "P")
(cider--eval-in-context (cider-last-sexp 'bounds) guess))
(defun cider-eval-sexp-at-point-in-context (guess)
"Evaluate the sexp around point in user-supplied context.
The context is just a let binding vector (without the brackets).
The context is remembered between command invocations.
When GUESS is non-nil, or called interactively with \\[universal-argument],
attempt to extract the context from parent let-bindings."
(interactive "P")
(cider--eval-in-context (cider-sexp-at-point 'bounds) guess))
(defun cider-eval-defun-to-comment (&optional insert-before)
"Evaluate the \"top-level\" form and insert result as comment.
The formatting of the comment is defined in `cider-comment-prefix'
which, by default, is \";; => \" and can be customized.
With the prefix arg INSERT-BEFORE, insert before the form, otherwise afterwards."
(interactive "P")
(let* ((bounds (cider-defun-at-point 'bounds))
(insertion-point (nth (if insert-before 0 1) bounds)))
(cider-interactive-eval nil
(cider-eval-print-with-comment-handler
(current-buffer)
(set-marker (make-marker) insertion-point)
cider-comment-prefix)
bounds
(cider--nrepl-pr-request-map))))
(defun cider-pprint-form-to-comment (form-fn insert-before)
"Evaluate the form selected by FORM-FN and insert result as comment.
FORM-FN can be either `cider-last-sexp' or `cider-defun-at-point'.
The formatting of the comment is controlled via three options:
`cider-comment-prefix' \";; => \"
`cider-comment-continued-prefix' \";; \"
`cider-comment-postfix' \"\"
so that with customization you can optionally wrap the output
in the reader macro \"#_( .. )\", or \"(comment ... )\", or any
other desired formatting.
If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards."
(let* ((bounds (funcall form-fn 'bounds))
(insertion-point (nth (if insert-before 0 1) bounds))
;; when insert-before, we need a newline after the output to
;; avoid commenting the first line of the form
(comment-postfix (concat cider-comment-postfix
(if insert-before "\n" ""))))
(cider-interactive-eval nil
(cider-eval-pprint-with-multiline-comment-handler
(current-buffer)
(set-marker (make-marker) insertion-point)
cider-comment-prefix
cider-comment-continued-prefix
comment-postfix)
bounds
(cider--nrepl-print-request-map fill-column))))
(defun cider-pprint-eval-last-sexp-to-comment (&optional insert-before)
"Evaluate the last sexp and insert result as comment.
The formatting of the comment is controlled via three options:
`cider-comment-prefix' \";; => \"
`cider-comment-continued-prefix' \";; \"
`cider-comment-postfix' \"\"
so that with customization you can optionally wrap the output
in the reader macro \"#_( .. )\", or \"(comment ... )\", or any
other desired formatting.
If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards."
(interactive "P")
(cider-pprint-form-to-comment 'cider-last-sexp insert-before))
(defun cider-pprint-eval-defun-to-comment (&optional insert-before)
"Evaluate the \"top-level\" form and insert result as comment.
The formatting of the comment is controlled via three options:
`cider-comment-prefix' \";; => \"
`cider-comment-continued-prefix' \";; \"
`cider-comment-postfix' \"\"
so that with customization you can optionally wrap the output
in the reader macro \"#_( .. )\", or \"(comment ... )\", or any
other desired formatting.
If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards."
(interactive "P")
(cider-pprint-form-to-comment 'cider-defun-at-point insert-before))
(declare-function cider-switch-to-repl-buffer "cider-mode")
(defun cider-eval-last-sexp-to-repl (&optional prefix)
"Evaluate the expression preceding point and insert its result in the REPL.
If invoked with a PREFIX argument, switch to the REPL buffer."
(interactive "P")
(cider-interactive-eval nil
(cider-insert-eval-handler (cider-current-repl))
(cider-last-sexp 'bounds)
(cider--nrepl-pr-request-map))
(when prefix
(cider-switch-to-repl-buffer)))
(defun cider-pprint-eval-last-sexp-to-repl (&optional prefix)
"Evaluate expr before point and insert its pretty-printed result in the REPL.
If invoked with a PREFIX argument, switch to the REPL buffer."
(interactive "P")
(cider-interactive-eval nil
(cider-insert-eval-handler (cider-current-repl))
(cider-last-sexp 'bounds)
(cider--nrepl-print-request-map fill-column))
(when prefix
(cider-switch-to-repl-buffer)))
(defun cider-eval-print-last-sexp (&optional pretty-print)
"Evaluate the expression preceding point.
Print its value into the current buffer.
With an optional PRETTY-PRINT prefix it pretty-prints the result."
(interactive "P")
(cider-interactive-eval nil
(cider-eval-print-handler)
(cider-last-sexp 'bounds)
(if pretty-print
(cider--nrepl-print-request-map fill-column)
(cider--nrepl-pr-request-map))))
(defun cider--pprint-eval-form (form)
"Pretty print FORM in popup buffer."
(let* ((buffer (current-buffer))
(result-buffer (cider-popup-buffer cider-result-buffer nil 'clojure-mode 'ancillary))
(handler (cider-popup-eval-handler result-buffer)))
(with-current-buffer buffer
(cider-interactive-eval (when (stringp form) form)
handler
(when (consp form) form)
(cider--nrepl-print-request-map fill-column)))))
(defun cider-pprint-eval-last-sexp (&optional output-to-current-buffer)
"Evaluate the sexp preceding point and pprint its value.
If invoked with OUTPUT-TO-CURRENT-BUFFER, insert as comment in the current
buffer, else display in a popup buffer."
(interactive "P")
(if output-to-current-buffer
(cider-pprint-eval-last-sexp-to-comment)
(cider--pprint-eval-form (cider-last-sexp 'bounds))))
(defun cider--prompt-and-insert-inline-dbg ()
"Insert a #dbg button at the current sexp."
(save-excursion
(let ((beg))
(skip-chars-forward "\r\n[:blank:]")
(unless (looking-at-p "(")
(ignore-errors (backward-up-list)))
(setq beg (point))
(let* ((cond (cider-read-from-minibuffer "Condition for debugging (leave empty for \"always\"): "))
(button (propertize (concat "#dbg"
(unless (equal cond "")
(format " ^{:break/when %s}" cond)))
'font-lock-face 'cider-fragile-button-face)))
(when (> (current-column) 30)
(insert "\n")
(indent-according-to-mode))
(insert button)
(when (> (current-column) 40)
(insert "\n")
(indent-according-to-mode)))
(make-button beg (point)
'help-echo "Breakpoint. Reevaluate this form to remove it."
:type 'cider-fragile))))
(defun cider-eval-defun-at-point (&optional debug-it)
"Evaluate the current toplevel form, and print result in the minibuffer.
With DEBUG-IT prefix argument, also debug the entire form as with the
command `cider-debug-defun-at-point'."
(interactive "P")
(let ((inline-debug (eq 16 (car-safe debug-it))))
(when debug-it
(when (derived-mode-p 'clojurescript-mode)
(when (y-or-n-p (concat "The debugger doesn't support ClojureScript yet, and we need help with that."
" \nWould you like to read the Feature Request?"))
(browse-url "https://github.com/clojure-emacs/cider/issues/1416"))
(user-error "The debugger does not support ClojureScript"))
(when inline-debug
(cider--prompt-and-insert-inline-dbg)))
(cider-interactive-eval (when (and debug-it (not inline-debug))
(concat "#dbg\n" (cider-defun-at-point)))
nil
(cider-defun-at-point 'bounds)
(cider--nrepl-pr-request-map))))
(defun cider--insert-closing-delimiters (code)
"Closes all open parenthesized or bracketed expressions of CODE."
(with-temp-buffer
(insert code)
(goto-char (point-max))
(let ((matching-delimiter nil))
(while (ignore-errors
(save-excursion
(backward-up-list 1)
(setq matching-delimiter (cdr (syntax-after (point)))))
t)
(insert-char matching-delimiter)))
(buffer-string)))
(defun cider-eval-defun-up-to-point (&optional output-to-current-buffer)
"Evaluate the current toplevel form up to point.
If invoked with OUTPUT-TO-CURRENT-BUFFER, print the result in the current
buffer. It constructs an expression to eval in the following manner:
- It find the code between the point and the start of the toplevel expression;
- It balances this bit of code by closing all open expressions;
- It evaluates the resulting code using `cider-interactive-eval'."
(interactive "P")
(let* ((beg-of-defun (save-excursion (beginning-of-defun) (point)))
(code (buffer-substring-no-properties beg-of-defun (point)))
(code (cider--insert-closing-delimiters code)))
(cider-interactive-eval code
(when output-to-current-buffer
(cider-eval-print-handler))
(list beg-of-defun (point))
(cider--nrepl-pr-request-map))))
(defun cider--matching-delimiter (delimiter)
"Get the matching (opening/closing) delimiter for DELIMITER."
(pcase delimiter
(?\( ?\))
(?\[ ?\])
(?\{ ?\})
(?\) ?\()
(?\] ?\[)
(?\} ?\{)))
(defun cider-eval-sexp-up-to-point (&optional output-to-current-buffer)
"Evaluate the current sexp form up to point.
If invoked with OUTPUT-TO-CURRENT-BUFFER, print the result in the current
buffer. It constructs an expression to eval in the following manner:
- It finds the code between the point and the start of the sexp expression;
- It balances this bit of code by closing the expression;
- It evaluates the resulting code using `cider-interactive-eval'."
(interactive "P")
(let* ((beg-of-sexp (save-excursion (up-list) (backward-list) (point)))
(beg-delimiter (save-excursion (up-list) (backward-list) (char-after)))
(beg-set? (save-excursion (up-list) (backward-list) (char-before)))
(code (buffer-substring-no-properties beg-of-sexp (point)))
(code (if (= beg-set? ?#) (concat (list beg-set?) code) code))
(code (concat code (list (cider--matching-delimiter beg-delimiter)))))
(cider-interactive-eval code
(when output-to-current-buffer
(cider-eval-print-handler))
(list beg-of-sexp (point))
(cider--nrepl-pr-request-map))))
(defun cider-pprint-eval-defun-at-point (&optional output-to-current-buffer)
"Evaluate the \"top-level\" form at point and pprint its value.
If invoked with OUTPUT-TO-CURRENT-BUFFER, insert as comment in the current
buffer, else display in a popup buffer."
(interactive "P")
(if output-to-current-buffer
(cider-pprint-eval-defun-to-comment)
(cider--pprint-eval-form (cider-defun-at-point 'bounds))))
(defun cider-eval-ns-form (&optional undef-all)
"Evaluate the current buffer's namespace form.
When UNDEF-ALL is non-nil, unmap all symbols and aliases first."
(interactive "P")
(when-let ((ns (clojure-find-ns)))
(save-excursion
(goto-char (match-beginning 0))
(when undef-all
(cider-undef-all ns))
(cider-eval-defun-at-point))))
(defun cider-read-and-eval (&optional value)
"Read a sexp from the minibuffer and output its result to the echo area.
If VALUE is non-nil, it is inserted into the minibuffer as initial input."
(interactive)
(let* ((form (cider-read-from-minibuffer "Clojure Eval: " value))
(override cider-interactive-eval-override)
(ns-form (if (cider-ns-form-p form) "" (format "(ns %s)" (cider-current-ns)))))
(with-current-buffer (get-buffer-create cider-read-eval-buffer)
(erase-buffer)
(clojure-mode)
(unless (string= "" ns-form)
(insert ns-form "\n\n"))
(insert form)
(let ((cider-interactive-eval-override override))
(cider-interactive-eval form
nil
nil
(cider--nrepl-pr-request-map))))))
(defun cider-read-and-eval-defun-at-point ()
"Insert the toplevel form at point in the minibuffer and output its result.
The point is placed next to the function name in the minibuffer to allow
passing arguments."
(interactive)
(let* ((fn-name (cadr (split-string (cider-defun-at-point))))
(form (format "(%s)" fn-name)))
(cider-read-and-eval (cons form (length form)))))
(defun cider-kill-last-result ()
"Save the last evaluated result into the kill ring."
(interactive)
(kill-new
(nrepl-dict-get (cider-nrepl-sync-request:eval "*1") "value")))
(defun cider-undef ()
"Undefine a symbol from the current ns."
(interactive)
(cider-ensure-op-supported "undef")
(cider-read-symbol-name
"Undefine symbol: "
(lambda (sym)
(cider-nrepl-send-request
`("op" "undef"
"ns" ,(cider-current-ns)
"sym" ,sym)
(cider-interactive-eval-handler (current-buffer))))))
(defun cider-undef-all (&optional ns)
"Undefine all symbols and aliases from the namespace NS."
(interactive)
(cider-ensure-op-supported "undef-all")
(cider-nrepl-send-sync-request
`("op" "undef-all"
"ns" ,(or ns (cider-current-ns)))))
;; Eval keymaps
(defvar cider-eval-pprint-commands-map
(let ((map (define-prefix-command 'cider-eval-pprint-commands-map)))
;; single key bindings defined last for display in menu
(define-key map (kbd "e") #'cider-pprint-eval-last-sexp)
(define-key map (kbd "d") #'cider-pprint-eval-defun-at-point)
(define-key map (kbd "c e") #'cider-pprint-eval-last-sexp-to-comment)
(define-key map (kbd "c d") #'cider-pprint-eval-defun-to-comment)
;; duplicates with C- for convenience
(define-key map (kbd "C-e") #'cider-pprint-eval-last-sexp)
(define-key map (kbd "C-d") #'cider-pprint-eval-defun-at-point)
(define-key map (kbd "C-c e") #'cider-pprint-eval-last-sexp-to-comment)
(define-key map (kbd "C-c C-e") #'cider-pprint-eval-last-sexp-to-comment)
(define-key map (kbd "C-c d") #'cider-pprint-eval-defun-to-comment)
(define-key map (kbd "C-c C-d") #'cider-pprint-eval-defun-to-comment)
map))
(defvar cider-eval-commands-map
(let ((map (define-prefix-command 'cider-eval-commands-map)))
;; single key bindings defined last for display in menu
(define-key map (kbd "w") #'cider-eval-last-sexp-and-replace)
(define-key map (kbd "r") #'cider-eval-region)
(define-key map (kbd "n") #'cider-eval-ns-form)
(define-key map (kbd "d") #'cider-eval-defun-at-point)
(define-key map (kbd "e") #'cider-eval-last-sexp)
(define-key map (kbd "q") #'cider-tap-last-sexp)
(define-key map (kbd "l") #'cider-eval-list-at-point)
(define-key map (kbd "v") #'cider-eval-sexp-at-point)
(define-key map (kbd "t") #'cider-tap-sexp-at-point)
(define-key map (kbd "o") #'cider-eval-sexp-up-to-point)
(define-key map (kbd ".") #'cider-read-and-eval-defun-at-point)
(define-key map (kbd "z") #'cider-eval-defun-up-to-point)
(define-key map (kbd "c") #'cider-eval-last-sexp-in-context)
(define-key map (kbd "b") #'cider-eval-sexp-at-point-in-context)
(define-key map (kbd "k") #'cider-kill-last-result)
(define-key map (kbd "f") 'cider-eval-pprint-commands-map)
;; duplicates with C- for convenience
(define-key map (kbd "C-w") #'cider-eval-last-sexp-and-replace)
(define-key map (kbd "C-r") #'cider-eval-region)
(define-key map (kbd "C-n") #'cider-eval-ns-form)
(define-key map (kbd "C-d") #'cider-eval-defun-at-point)
(define-key map (kbd "C-e") #'cider-eval-last-sexp)
(define-key map (kbd "C-q") #'cider-tap-last-sexp)
(define-key map (kbd "C-l") #'cider-eval-list-at-point)
(define-key map (kbd "C-v") #'cider-eval-sexp-at-point)
(define-key map (kbd "C-t") #'cider-tap-sexp-at-point)
(define-key map (kbd "C-o") #'cider-eval-sexp-up-to-point)
(define-key map (kbd "C-.") #'cider-read-and-eval-defun-at-point)
(define-key map (kbd "C-z") #'cider-eval-defun-up-to-point)
(define-key map (kbd "C-c") #'cider-eval-last-sexp-in-context)
(define-key map (kbd "C-b") #'cider-eval-sexp-at-point-in-context)
(define-key map (kbd "C-k") #'cider-kill-last-result)
(define-key map (kbd "C-f") 'cider-eval-pprint-commands-map)
map))
(defun cider--file-string (file)
"Read the contents of a FILE and return as a string."
(with-current-buffer (find-file-noselect file)
(save-restriction
(widen)
(substring-no-properties (buffer-string)))))
(defun cider-load-buffer (&optional buffer callback undef-all)
"Load (eval) BUFFER's file in nREPL.
If no buffer is provided the command acts on the current buffer. If the
buffer is for a cljc file, and both a Clojure and ClojureScript REPL exists
for the project, it is evaluated in both REPLs.
Optional argument CALLBACK will override the default ‘cider-load-file-handler’.
When UNDEF-ALL is non-nil or called with \\[universal-argument], removes
all ns aliases and var mappings from the namespace before reloading it."
(interactive (list (current-buffer) nil (equal current-prefix-arg '(4))))
(setq buffer (or buffer (current-buffer)))
;; When cider-load-buffer or cider-load-file are called in programs the
;; current context might not match the buffer's context. We use the caller
;; context instead of the buffer's context because that's the common use
;; case. For the other use case just let-bind the default-directory.
(let ((orig-default-directory default-directory))
(with-current-buffer buffer
(check-parens)
(let ((default-directory orig-default-directory))
(unless buffer-file-name
(user-error "Buffer `%s' is not associated with a file" (current-buffer)))
(when (and cider-save-file-on-load
(buffer-modified-p)
(or (eq cider-save-file-on-load t)
(y-or-n-p (format "Save file %s? " buffer-file-name))))
(save-buffer))
(remove-overlays nil nil 'cider-temporary t)
(when undef-all
(cider-undef-all (cider-current-ns)))
(cider--clear-compilation-highlights)
(cider--quit-error-window)
(let ((filename (buffer-file-name buffer))
(ns-form (cider-ns-form)))
(cider-map-repls :auto
(lambda (repl)
(when ns-form
(cider-repl--cache-ns-form ns-form repl))
(cider-request:load-file (cider--file-string filename)
(funcall cider-to-nrepl-filename-function
(cider--server-filename filename))
(file-name-nondirectory filename)
repl
callback)))
(message "Loading %s..." filename))))))
(defun cider-load-file (filename &optional undef-all)
"Load (eval) the Clojure file FILENAME in nREPL.
If the file is a cljc file, and both a Clojure and ClojureScript REPL
exists for the project, it is evaluated in both REPLs. The heavy lifting
is done by `cider-load-buffer'.
When UNDEF-ALL is non-nil or called with \\[universal-argument], removes
all ns aliases and var mappings from the namespace before reloading it."
(interactive (list
(read-file-name "Load file: " nil nil nil
(when (buffer-file-name)
(file-name-nondirectory
(buffer-file-name))))
(equal current-prefix-arg '(4))))
(if-let* ((buffer (find-buffer-visiting filename)))
(cider-load-buffer buffer nil undef-all)
(cider-load-buffer (find-file-noselect filename) nil undef-all)))
(defun cider-load-all-files (directory undef-all)
"Load all files in DIRECTORY (recursively).
Useful when the running nREPL on remote host.
When UNDEF-ALL is non-nil or called with \\[universal-argument], removes
all ns aliases and var mappings from the namespaces being reloaded"
(interactive "DLoad files beneath directory: \nP")
(mapcar (lambda (file) (cider-load-file file undef-all))
(directory-files-recursively directory "\\.clj[cs]?$")))
(defalias 'cider-eval-file #'cider-load-file
"A convenience alias as some people are confused by the load-* names.")
(defalias 'cider-eval-all-files #'cider-load-all-files
"A convenience alias as some people are confused by the load-* names.")
(defalias 'cider-eval-buffer #'cider-load-buffer
"A convenience alias as some people are confused by the load-* names.")
(defun cider-load-all-project-ns ()
"Load all namespaces in the current project."
(interactive)
(cider-ensure-connected)
(cider-ensure-op-supported "ns-load-all")
(when (y-or-n-p "Are you sure you want to load all namespaces in the project? ")
(message "Loading all project namespaces...")
(let ((loaded-ns-count (length (cider-sync-request:ns-load-all))))
(message "Loaded %d namespaces" loaded-ns-count))))
(provide 'cider-eval)
;;; cider-eval.el ends here
;;; cider-find.el --- Functionality for finding things -*- lexical-binding: t -*-
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; A bunch of commands for finding resources and definitions.
;;; Code:
(require 'cider-client)
(require 'cider-common)
(require 'cider-resolve)
(require 'thingatpt)
(defun cider--find-var-other-window (var &optional line)
"Find the definition of VAR, optionally at a specific LINE.
Display the results in a different window."
(if-let* ((info (cider-var-info var)))
(progn
(if line (setq info (nrepl-dict-put info "line" line)))
(cider--jump-to-loc-from-info info t))
(user-error "Symbol `%s' not resolved" var)))
(defun cider--find-var (var &optional line)
"Find the definition of VAR, optionally at a specific LINE."
(if-let* ((info (cider-var-info var)))
(progn
(if line (setq info (nrepl-dict-put info "line" line)))
(cider--jump-to-loc-from-info info))
(user-error "Symbol `%s' not resolved" var)))
;;;###autoload
(defun cider-find-var (&optional arg var line)
"Find definition for VAR at LINE.
Prompt according to prefix ARG and `cider-prompt-for-symbol'.
A single or double prefix argument inverts the meaning of
`cider-prompt-for-symbol'. A prefix of `-` or a double prefix argument causes
the results to be displayed in a different window. The default value is
thing at point."
(interactive "P")
(if var
(cider--find-var var line)
(funcall (cider-prompt-for-symbol-function arg)
"Symbol"
(if (cider--open-other-window-p arg)
#'cider--find-var-other-window
#'cider--find-var))))
;;;###autoload
(defun cider-find-dwim-at-mouse (event)
"Find and display variable or resource at mouse EVENT."
(interactive "e")
(if-let* ((symbol-file (save-excursion
(mouse-set-point event)
(cider-symbol-at-point 'look-back))))
(cider-find-dwim symbol-file)
(user-error "No variable or resource here")))
(defun cider--find-dwim (symbol-file callback &optional other-window)
"Find the SYMBOL-FILE at point.
CALLBACK upon failure to invoke prompt if not prompted previously.
Show results in a different window if OTHER-WINDOW is true."
(if-let* ((info (cider-var-info symbol-file)))
(cider--jump-to-loc-from-info info other-window)
(progn
(cider-ensure-op-supported "resource")
(if-let* ((resource (cider-sync-request:resource symbol-file))
(buffer (cider-find-file resource)))
(cider-jump-to buffer 0 other-window)
(if (cider--prompt-for-symbol-p current-prefix-arg)
(error "Resource or var %s not resolved" symbol-file)
(let ((current-prefix-arg (if current-prefix-arg nil '(4))))
(call-interactively callback)))))))
(defun cider--find-dwim-interactive (prompt)
"Get interactive arguments for jump-to functions using PROMPT as needed."
(if (cider--prompt-for-symbol-p current-prefix-arg)
(list
(cider-read-from-minibuffer prompt (thing-at-point 'filename)))
(list (or (thing-at-point 'filename) "")))) ; No prompt.
(defun cider-find-dwim-other-window (symbol-file)
"Jump to SYMBOL-FILE at point, place results in other window."
(interactive (cider--find-dwim-interactive "Jump to: "))
(cider--find-dwim symbol-file 'cider-find-dwim-other-window t))
;;;###autoload
(defun cider-find-dwim (symbol-file)
"Find and display the SYMBOL-FILE at point.
SYMBOL-FILE could be a var or a resource. If thing at point is empty then
show Dired on project. If var is not found, try to jump to resource of the
same name. When called interactively, a prompt is given according to the
variable `cider-prompt-for-symbol'. A single or double prefix argument
inverts the meaning. A prefix of `-' or a double prefix argument causes
the results to be displayed in a different window. A default value of thing
at point is given when prompted."
(interactive (cider--find-dwim-interactive "Jump to: "))
(cider--find-dwim symbol-file `cider-find-dwim
(cider--open-other-window-p current-prefix-arg)))
;;;###autoload
(defun cider-find-resource (path)
"Find the resource at PATH.
Prompt for input as indicated by the variable `cider-prompt-for-symbol'.
A single or double prefix argument inverts the meaning of
`cider-prompt-for-symbol'. A prefix argument of `-` or a double prefix
argument causes the results to be displayed in other window. The default
value is thing at point."
(interactive
(list
(if (cider--prompt-for-symbol-p current-prefix-arg)
(completing-read "Resource: "
(cider-sync-request:resources-list)
nil nil
(thing-at-point 'filename))
(or (thing-at-point 'filename) ""))))
(cider-ensure-op-supported "resource")
(when (= (length path) 0)
(error "Cannot find resource for empty path"))
(if-let* ((resource (cider-sync-request:resource path))
(buffer (cider-find-file resource)))
(cider-jump-to buffer nil (cider--open-other-window-p current-prefix-arg))
(if (cider--prompt-for-symbol-p current-prefix-arg)
(error "Cannot find resource %s" path)
(let ((current-prefix-arg (cider--invert-prefix-arg current-prefix-arg)))
(call-interactively 'cider-find-resource)))))
(defun cider--invert-prefix-arg (arg)
"Invert the effect of prefix value ARG on `cider-prompt-for-symbol'.
This function preserves the `other-window' meaning of ARG."
(let ((narg (prefix-numeric-value arg)))
(pcase narg
(16 -1) ; empty empty -> -
(-1 16) ; - -> empty empty
(4 nil) ; empty -> no-prefix
(_ 4)))) ; no-prefix -> empty
(defun cider--prefix-invert-prompt-p (arg)
"Test prefix value ARG for its effect on `cider-prompt-for-symbol`."
(let ((narg (prefix-numeric-value arg)))
(pcase narg
(16 t) ; empty empty
(4 t) ; empty
(_ nil))))
(defun cider--prompt-for-symbol-p (&optional prefix)
"Check if cider should prompt for symbol.
Tests againsts PREFIX and the value of `cider-prompt-for-symbol'.
Invert meaning of `cider-prompt-for-symbol' if PREFIX indicates it should be."
(if (cider--prefix-invert-prompt-p prefix)
(not cider-prompt-for-symbol) cider-prompt-for-symbol))
(defun cider--find-ns (ns &optional other-window)
"Find the file containing NS's definition.
Optionally open it in a different window if OTHER-WINDOW is truthy."
(if-let* ((path (cider-sync-request:ns-path ns)))
(cider-jump-to (cider-find-file path) nil other-window)
(user-error "Can't find namespace `%s'" ns)))
;;;###autoload
(defun cider-find-ns (&optional arg ns)
"Find the file containing NS.
A prefix ARG of `-` or a double prefix argument causes
the results to be displayed in a different window."
(interactive "P")
(cider-ensure-connected)
(cider-ensure-op-supported "ns-path")
(if ns
(cider--find-ns ns)
(let* ((namespaces (cider-sync-request:ns-list))
(ns (completing-read "Find namespace: " namespaces)))
(cider--find-ns ns (cider--open-other-window-p arg)))))
;;;###autoload
(defun cider-find-keyword (&optional arg)
"Find the namespace of the keyword at point and its first occurrence there.
For instance - if the keyword at point is \":cider.demo/keyword\", this command
would find the namespace \"cider.demo\" and afterwards find the first mention
of \"::keyword\" there.
Prompt according to prefix ARG and `cider-prompt-for-symbol'.
A single or double prefix argument inverts the meaning of
`cider-prompt-for-symbol'. A prefix of `-` or a double prefix argument causes
the results to be displayed in a different window. The default value is
thing at point."
(interactive "P")
(cider-ensure-connected)
(let* ((kw (let ((kw-at-point (cider-symbol-at-point 'look-back)))
(if (or cider-prompt-for-symbol arg)
(read-string
(format "Keyword (default %s): " kw-at-point)
nil nil kw-at-point)
kw-at-point)))
(ns-qualifier (and
(string-match "^:+\\(.+\\)/.+$" kw)
(match-string 1 kw)))
(kw-ns (if ns-qualifier
(cider-resolve-alias (cider-current-ns) ns-qualifier)
(cider-current-ns)))
(kw-to-find (concat "::" (replace-regexp-in-string "^:+\\(.+/\\)?" "" kw))))
(when (and ns-qualifier (string= kw-ns (cider-current-ns)))
(error "Could not resolve alias `%s' in `%s'" ns-qualifier (cider-current-ns)))
(cider--find-ns kw-ns arg)
(search-forward-regexp kw-to-find nil 'noerror)))
;;; xref integration
;;
;; xref.el was introduced in Emacs 25.1.
;; CIDER's xref backend was added in CIDER 1.2.
(defun cider--xref-backend ()
"Used for xref integration."
;; Check if `cider-nrepl` middleware is loaded. Allows fallback to other xref
;; backends, if cider-nrepl is not loaded.
(when (cider-nrepl-op-supported-p "ns-path" nil 'skip-ensure)
'cider))
(cl-defmethod xref-backend-identifier-at-point ((_backend (eql cider)))
"Return the relevant identifier at point."
(cider-symbol-at-point 'look-back))
(defun cider--var-to-xref-location (var)
"Get location of definition of VAR."
(when-let* ((info (cider-var-info var))
(line (nrepl-dict-get info "line"))
(file (nrepl-dict-get info "file"))
(buf (cider--find-buffer-for-file file)))
(xref-make-buffer-location
buf
(with-current-buffer buf
(save-excursion
(goto-char 0)
(forward-line (1- line))
(back-to-indentation)
(point))))))
(cl-defmethod xref-backend-definitions ((_backend (eql cider)) var)
"Find definitions of VAR."
(cider-ensure-connected)
(cider-ensure-op-supported "ns-path")
(when-let* ((loc (cider--var-to-xref-location var)))
(list (xref-make var loc))))
(cl-defmethod xref-backend-identifier-completion-table ((_backend (eql cider)))
"Return the completion table for identifiers."
(cider-ensure-connected)
(when-let* ((ns (cider-current-ns))
(results (cider-sync-request:ns-vars ns)))
results))
(cl-defmethod xref-backend-references ((_backend (eql cider)) var)
"Find references of VAR."
(cider-ensure-connected)
(cider-ensure-op-supported "fn-refs")
(when-let* ((ns (cider-current-ns))
(results (cider-sync-request:fn-refs ns var)))
(mapcar (lambda (info)
(let* ((filename (nrepl-dict-get info "file"))
(column (nrepl-dict-get info "column"))
(line (nrepl-dict-get info "line"))
(loc (xref-make-file-location filename line column)))
(xref-make filename loc)))
results)))
(cl-defmethod xref-backend-apropos ((_backend (eql cider)) pattern)
"Find all symbols that match regexp PATTERN."
(cider-ensure-connected)
(cider-ensure-op-supported "apropos")
(when-let* ((ns (cider-current-ns))
(results (cider-sync-request:apropos pattern ns t t completion-ignore-case)))
(mapcar (lambda (info)
(let* ((symbol (nrepl-dict-get info "name"))
(loc (cider--var-to-xref-location symbol))
(type (nrepl-dict-get info "type"))
(doc (nrepl-dict-get info "doc")))
(xref-make (format "[%s] %s\n %s" (propertize symbol 'face 'bold) (capitalize type) doc)
loc)))
results)))
(provide 'cider-find)
;;; cider-find.el ends here
;;; cider-format.el --- Code and EDN formatting functionality -*- lexical-binding: t -*-
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Middleware-powered code and EDN formatting functionality.
;;; Code:
(require 'map)
(require 'seq)
(require 'subr-x)
(require 'cider-client)
(require 'cider-util)
;; Format
(defun cider--format-reindent (formatted start)
"Reindent FORMATTED to align with buffer position START."
(let* ((start-column (save-excursion (goto-char start) (current-column)))
(indent-line (concat "\n" (make-string start-column ? ))))
(replace-regexp-in-string "\n" indent-line formatted)))
;;; Format region
(defun cider--format-region (start end formatter)
"Format the contents of the given region.
START and END represent the region's boundaries.
FORMATTER is a function of one argument which is used to convert
the string contents of the region into a formatted string.
Uses the following heuristic to try to maintain point position:
- Take a snippet of text starting at current position, up to 64 chars.
- Search for the snippet, with lax whitespace, in the formatted text.
- If snippet is less than 64 chars (point was near end of buffer), search
from end instead of beginning.
- Place point at match beginning, or `point-min' if no match."
(let* ((original (buffer-substring-no-properties start end))
(formatted (funcall formatter original))
(indented (cider--format-reindent formatted start)))
(unless (equal original indented)
(let* ((pos (point))
(pos-max (1+ (buffer-size)))
(l 64)
(endp (> (+ pos l) pos-max))
(snippet (thread-last
(buffer-substring-no-properties
pos (min (+ pos l) pos-max))
(regexp-quote)
(replace-regexp-in-string "[[:space:]\t\n\r]+" "[[:space:]\t\n\r]*"))))
(delete-region start end)
(insert indented)
(goto-char (if endp (point-max) (point-min)))
(funcall (if endp #'re-search-backward #'re-search-forward) snippet nil t)
(goto-char (or (match-beginning 0) start))
(when (looking-at-p "\n") (forward-char))))))
;;;###autoload
(defun cider-format-region (start end)
"Format the Clojure code in the current region.
START and END represent the region's boundaries."
(interactive "r")
(cider-ensure-connected)
(cider--format-region start end
(lambda (buf)
(cider-sync-request:format-code buf cider-format-code-options))))
;;; Format defun
;;;###autoload
(defun cider-format-defun ()
"Format the code in the current defun."
(interactive)
(cider-ensure-connected)
(let ((defun-bounds (cider-defun-at-point 't)))
(cider-format-region (car defun-bounds) (cadr defun-bounds))))
;;; Format buffer
(defun cider--format-buffer (formatter)
"Format the contents of the current buffer.
Uses FORMATTER, a function of one argument, to convert the string contents
of the buffer into a formatted string."
(cider--format-region 1 (1+ (buffer-size)) formatter))
;;;###autoload
(defun cider-format-buffer ()
"Format the Clojure code in the current buffer."
(interactive)
(check-parens)
(cider-ensure-connected)
(cider--format-buffer (lambda (buf)
(cider-sync-request:format-code buf cider-format-code-options))))
;;; Format EDN
;;;###autoload
(defun cider-format-edn-buffer ()
"Format the EDN data in the current buffer."
(interactive)
(check-parens)
(cider-ensure-connected)
(cider--format-buffer (lambda (edn)
(cider-sync-request:format-edn edn fill-column))))
;;;###autoload
(defun cider-format-edn-region (start end)
"Format the EDN data in the current region.
START and END represent the region's boundaries."
(interactive "r")
(cider-ensure-connected)
(let* ((start-column (save-excursion (goto-char start) (current-column)))
(right-margin (- fill-column start-column)))
(cider--format-region start end
(lambda (edn)
(cider-sync-request:format-edn edn right-margin)))))
;;;###autoload
(defun cider-format-edn-last-sexp ()
"Format the EDN data of the last sexp."
(interactive)
(apply #'cider-format-edn-region (cider-sexp-at-point 'bounds)))
(provide 'cider-format)
;;; cider-format.el ends here
;;; cider-inspector.el --- Object inspector -*- lexical-binding: t -*-
;; Copyright © 2013-2022 Vital Reactor, LLC
;; Copyright © 2014-2022 Bozhidar Batsov and CIDER contributors
;; Author: Ian Eslick <ian@vitalreactor.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Clojure object inspector inspired by SLIME.
;;; Code:
(require 'cl-lib)
(require 'easymenu)
(require 'seq)
(require 'cider-eval)
;; ===================================
;; Inspector Key Map and Derived Mode
;; ===================================
(defconst cider-inspector-buffer "*cider-inspect*")
;;; Customization
(defgroup cider-inspector nil
"Presentation and behavior of the CIDER value inspector."
:prefix "cider-inspector-"
:group 'cider
:package-version '(cider . "0.10.0"))
(defcustom cider-inspector-page-size 32
"Default page size in paginated inspector view.
The page size can be also changed interactively within the inspector."
:type '(integer :tag "Page size" 32)
:package-version '(cider . "0.10.0"))
(defcustom cider-inspector-max-atom-length 150
"Default max length of nested atoms before they are truncated.
'Atom' here means any collection member that satisfies (complement coll?).
The max length can be also changed interactively within the inspector."
:type '(integer :tag "Max atom length" 150)
:package-version '(cider . "1.1.0"))
(defcustom cider-inspector-max-coll-size 5
"Default number of nested collection members to display before truncating.
The max size can be also changed interactively within the inspector."
:type '(integer :tag "Max collection size" 5)
:package-version '(cider . "1.1.0"))
(defcustom cider-inspector-fill-frame nil
"Controls whether the CIDER inspector window fills its frame."
:type 'boolean
:package-version '(cider . "0.15.0"))
(defcustom cider-inspector-skip-uninteresting t
"Controls whether to skip over uninteresting values in the inspector.
Only applies to navigation with `cider-inspector-prev-inspectable-object'
and `cider-inspector-next-inspectable-object', values are still inspectable
by clicking or navigating to them by other means."
:type 'boolean
:package-version '(cider . "0.25.0"))
(defcustom cider-inspector-auto-select-buffer t
"Determines if the inspector buffer should be auto selected."
:type 'boolean
:package-version '(cider . "0.27.0"))
(defvar cider-inspector-uninteresting-regexp
(concat "nil" ; nils are not interesting
"\\|:" clojure--sym-regexp ; nor keywords
;; FIXME: This range also matches ",", is it on purpose?
"\\|[+-.0-9]+") ; nor numbers. Note: BigInts, ratios etc. are interesting
"Regexp of uninteresting and skippable values.")
(defvar cider-inspector-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map cider-popup-buffer-mode-map)
(define-key map (kbd "RET") #'cider-inspector-operate-on-point)
(define-key map [mouse-1] #'cider-inspector-operate-on-click)
(define-key map "l" #'cider-inspector-pop)
(define-key map "g" #'cider-inspector-refresh)
;; Page-up/down
(define-key map [next] #'cider-inspector-next-page)
(define-key map [prior] #'cider-inspector-prev-page)
(define-key map " " #'cider-inspector-next-page)
(define-key map (kbd "M-SPC") #'cider-inspector-prev-page)
(define-key map (kbd "S-SPC") #'cider-inspector-prev-page)
(define-key map "s" #'cider-inspector-set-page-size)
(define-key map "a" #'cider-inspector-set-max-atom-length)
(define-key map "c" #'cider-inspector-set-max-coll-size)
(define-key map "d" #'cider-inspector-def-current-val)
(define-key map [tab] #'cider-inspector-next-inspectable-object)
(define-key map "\C-i" #'cider-inspector-next-inspectable-object)
(define-key map [(shift tab)] #'cider-inspector-previous-inspectable-object)
;; Emacs translates S-TAB to BACKTAB on X.
(define-key map [backtab] #'cider-inspector-previous-inspectable-object)
(easy-menu-define cider-inspector-mode-menu map
"Menu for CIDER's inspector."
`("CIDER Inspector"
["Inspect" cider-inspector-operate-on-point]
["Pop" cider-inspector-pop]
["Refresh" cider-inspector-refresh]
"--"
["Next Inspectable Object" cider-inspector-next-inspectable-object]
["Previous Inspectable Object" cider-inspector-previous-inspectable-object]
"--"
["Next Page" cider-inspector-next-page]
["Previous Page" cider-inspector-prev-page]
["Set Page Size" cider-inspector-set-page-size]
["Set Max Atom Length" cider-inspector-set-max-atom-length]
["Set Max Collection Size" cider-inspector-set-max-coll-size]
["Define Var" cider-inspector-def-current-val]
"--"
["Quit" cider-popup-buffer-quit-function]
))
map))
(define-derived-mode cider-inspector-mode special-mode "Inspector"
"Major mode for inspecting Clojure data structures.
\\{cider-inspector-mode-map}"
(set-syntax-table clojure-mode-syntax-table)
(setq-local electric-indent-chars nil)
(setq-local sesman-system 'CIDER)
(visual-line-mode 1))
;;;###autoload
(defun cider-inspect-last-sexp ()
"Inspect the result of the the expression preceding point."
(interactive)
(cider-inspect-expr (cider-last-sexp) (cider-current-ns)))
;;;###autoload
(defun cider-inspect-defun-at-point ()
"Inspect the result of the \"top-level\" expression at point."
(interactive)
(cider-inspect-expr (cider-defun-at-point) (cider-current-ns)))
;;;###autoload
(defun cider-inspect-last-result ()
"Inspect the most recent eval result."
(interactive)
(cider-inspect-expr "*1" (cider-current-ns)))
;;;###autoload
(defun cider-inspect (&optional arg)
"Inspect the result of the preceding sexp.
With a prefix argument ARG it inspects the result of the \"top-level\" form.
With a second prefix argument it prompts for an expression to eval and inspect."
(interactive "p")
(pcase arg
(1 (cider-inspect-last-sexp))
(4 (cider-inspect-defun-at-point))
(16 (call-interactively #'cider-inspect-expr))))
(defvar cider-inspector-location-stack nil
"A stack used to save point locations in inspector buffers.
These locations are used to emulate `save-excursion' between
`cider-inspector-push' and `cider-inspector-pop' operations.")
(defvar cider-inspector-page-location-stack nil
"A stack used to save point locations in inspector buffers.
These locations are used to emulate `save-excursion' between
`cider-inspector-next-page' and `cider-inspector-prev-page' operations.")
(defvar cider-inspector-last-command nil
"Contains the value of the most recently used `cider-inspector-*' command.
This is used as an alternative to the built-in `last-command'. Whenever we
invoke any command through \\[execute-extended-command] and its variants,
the value of `last-command' is not set to the command it invokes.")
(defvar cider-inspector--current-repl nil
"Contains the reference to the REPL where inspector was last invoked from.
This is needed for internal inspector buffer operations (push,
pop) to execute against the correct REPL session.")
;; Operations
;;;###autoload
(defun cider-inspect-expr (expr ns)
"Evaluate EXPR in NS and inspect its value.
Interactively, EXPR is read from the minibuffer, and NS the
current buffer's namespace."
(interactive (list (cider-read-from-minibuffer "Inspect expression: " (cider-sexp-at-point))
(cider-current-ns)))
(setq cider-inspector--current-repl (cider-current-repl))
(when-let* ((value (cider-sync-request:inspect-expr
expr ns
cider-inspector-page-size
cider-inspector-max-atom-length
cider-inspector-max-coll-size)))
(cider-inspector--render-value value)))
(defun cider-inspector-pop ()
"Pop the last value off the inspector stack and render it.
See `cider-sync-request:inspect-pop' and `cider-inspector--render-value'."
(interactive)
(setq cider-inspector-last-command 'cider-inspector-pop)
(when-let* ((value (cider-sync-request:inspect-pop)))
(cider-inspector--render-value value)))
(defun cider-inspector-push (idx)
"Inspect the value at IDX in the inspector stack and render it.
See `cider-sync-request:inspect-push' and `cider-inspector--render-value'"
(push (point) cider-inspector-location-stack)
(when-let* ((value (cider-sync-request:inspect-push idx)))
(cider-inspector--render-value value)
(cider-inspector-next-inspectable-object 1)))
(defun cider-inspector-refresh ()
"Re-render the currently inspected value.
See `cider-sync-request:inspect-refresh' and `cider-inspector--render-value'"
(interactive)
(when-let* ((value (cider-sync-request:inspect-refresh)))
(cider-inspector--render-value value)))
(defun cider-inspector-next-page ()
"Jump to the next page when inspecting a paginated sequence/map.
Does nothing if already on the last page."
(interactive)
(push (point) cider-inspector-page-location-stack)
(when-let* ((value (cider-sync-request:inspect-next-page)))
(cider-inspector--render-value value)))
(defun cider-inspector-prev-page ()
"Jump to the previous page when expecting a paginated sequence/map.
Does nothing if already on the first page."
(interactive)
(setq cider-inspector-last-command 'cider-inspector-prev-page)
(when-let* ((value (cider-sync-request:inspect-prev-page)))
(cider-inspector--render-value value)))
(defun cider-inspector-set-page-size (page-size)
"Set the page size in pagination mode to the specified PAGE-SIZE.
Current page will be reset to zero."
(interactive (list (read-number "Page size: " cider-inspector-page-size)))
(when-let ((value (cider-sync-request:inspect-set-page-size page-size)))
(cider-inspector--render-value value)))
(defun cider-inspector-set-max-atom-length (max-length)
"Set the max length of nested atoms to MAX-LENGTH."
(interactive (list (read-number "Max atom length: " cider-inspector-max-atom-length)))
(when-let ((value (cider-sync-request:inspect-set-max-atom-length max-length)))
(cider-inspector--render-value value)))
(defun cider-inspector-set-max-coll-size (max-size)
"Set the number of nested collection members to display before truncating.
MAX-SIZE is the new value."
(interactive (list (read-number "Max collection size: " cider-inspector-max-coll-size)))
(when-let ((value (cider-sync-request:inspect-set-max-coll-size max-size)))
(cider-inspector--render-value value)))
(defun cider-inspector-def-current-val (var-name ns)
"Defines a var with VAR-NAME in current namespace.
Doesn't modify current page. When called interactively NS defaults to
current-namespace."
(interactive (let ((ns (cider-current-ns)))
(list (read-from-minibuffer (concat "Var name: " ns "/"))
ns)))
(setq cider-inspector--current-repl (cider-current-repl))
(when-let* ((value (cider-sync-request:inspect-def-current-val ns var-name)))
(cider-inspector--render-value value)
(message "%s#'%s/%s = %s" cider-eval-result-prefix ns var-name value)))
;; nREPL interactions
(defun cider-sync-request:inspect-pop ()
"Move one level up in the inspector stack."
(thread-first '("op" "inspect-pop")
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
(defun cider-sync-request:inspect-push (idx)
"Inspect the inside value specified by IDX."
(thread-first `("op" "inspect-push"
"idx" ,idx)
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
(defun cider-sync-request:inspect-refresh ()
"Re-render the currently inspected value."
(thread-first '("op" "inspect-refresh")
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
(defun cider-sync-request:inspect-next-page ()
"Jump to the next page in paginated collection view."
(thread-first '("op" "inspect-next-page")
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
(defun cider-sync-request:inspect-prev-page ()
"Jump to the previous page in paginated collection view."
(thread-first '("op" "inspect-prev-page")
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
(defun cider-sync-request:inspect-set-page-size (page-size)
"Set the page size in paginated view to PAGE-SIZE."
(thread-first `("op" "inspect-set-page-size"
"page-size" ,page-size)
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
(defun cider-sync-request:inspect-set-max-atom-length (max-length)
"Set the max length of nested atoms to MAX-LENGTH."
(thread-first `("op" "inspect-set-max-atom-length"
"max-atom-length" ,max-length)
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
(defun cider-sync-request:inspect-set-max-coll-size (max-size)
"Set the number of nested collection members to display before truncating.
MAX-SIZE is the new value."
(thread-first `("op" "inspect-set-max-coll-size"
"max-coll-size" ,max-size)
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
(defun cider-sync-request:inspect-def-current-val (ns var-name)
"Defines a var with VAR-NAME in NS with the current inspector value."
(thread-first `("op" "inspect-def-current-value"
"ns" ,ns
"var-name" ,var-name)
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
(defun cider-sync-request:inspect-expr (expr ns page-size max-atom-length max-coll-size)
"Evaluate EXPR in context of NS and inspect its result.
Set the page size in paginated view to PAGE-SIZE, maximum length of atomic
collection members to MAX-ATOM-LENGTH, and maximum size of nested collections to
MAX-COLL-SIZE if non nil."
(thread-first (append (nrepl--eval-request expr ns)
`("inspect" "true"
,@(when page-size
`("page-size" ,page-size))
,@(when max-atom-length
`("max-atom-length" ,max-atom-length))
,@(when max-coll-size
`("max-coll-size" ,max-coll-size))))
(cider-nrepl-send-sync-request cider-inspector--current-repl)
(nrepl-dict-get "value")))
;; Render Inspector from Structured Values
(defun cider-inspector--render-value (value)
"Render VALUE."
(cider-make-popup-buffer cider-inspector-buffer 'cider-inspector-mode 'ancillary)
(cider-inspector-render cider-inspector-buffer value)
(cider-popup-buffer-display cider-inspector-buffer cider-inspector-auto-select-buffer)
(when cider-inspector-fill-frame (delete-other-windows))
(ignore-errors (cider-inspector-next-inspectable-object 1))
(with-current-buffer cider-inspector-buffer
(when (eq cider-inspector-last-command 'cider-inspector-pop)
(setq cider-inspector-last-command nil)
;; Prevents error message being displayed when we try to pop
;; from the top-level of a data structure
(when cider-inspector-location-stack
(goto-char (pop cider-inspector-location-stack))))
(when (eq cider-inspector-last-command 'cider-inspector-prev-page)
(setq cider-inspector-last-command nil)
;; Prevents error message being displayed when we try to
;; go to a prev-page from the first page
(when cider-inspector-page-location-stack
(goto-char (pop cider-inspector-page-location-stack))))))
(defun cider-inspector-render (buffer str)
"Render STR in BUFFER."
(with-current-buffer buffer
(cider-inspector-mode)
(let ((inhibit-read-only t))
(condition-case nil
(cider-inspector-render* (car (read-from-string str)))
(error (insert "\nInspector error for: " str))))
(goto-char (point-min))))
(defun cider-inspector-render* (elements)
"Render ELEMENTS."
(dolist (el elements)
(cider-inspector-render-el* el)))
(defun cider-inspector-render-el* (el)
"Render EL."
(cond ((symbolp el) (insert (symbol-name el)))
((stringp el) (insert (propertize el 'font-lock-face 'font-lock-keyword-face)))
((and (consp el) (eq (car el) :newline))
(insert "\n"))
((and (consp el) (eq (car el) :value))
(cider-inspector-render-value (cadr el) (cl-caddr el)))
(t (message "Unrecognized inspector object: %s" el))))
(defun cider-inspector-render-value (value idx)
"Render VALUE at IDX."
(cider-propertize-region
(list 'cider-value-idx idx
'mouse-face 'highlight)
(cider-inspector-render-el* (cider-font-lock-as-clojure value))))
;; ===================================================
;; Inspector Navigation (lifted from SLIME inspector)
;; ===================================================
(defun cider-find-inspectable-object (direction limit)
"Find the next/previous inspectable object.
DIRECTION can be either 'next or 'prev.
LIMIT is the maximum or minimum position in the current buffer.
Return a list of two values: If an object could be found, the
starting position of the found object and T is returned;
otherwise LIMIT and NIL is returned."
(let ((finder (cl-ecase direction
(next 'next-single-property-change)
(prev 'previous-single-property-change))))
(let ((prop nil) (curpos (point)))
(while (and (not prop) (not (= curpos limit)))
(let ((newpos (funcall finder curpos 'cider-value-idx nil limit)))
(setq prop (get-text-property newpos 'cider-value-idx))
(setq curpos newpos)))
(list curpos (and prop t)))))
(defun cider-inspector-next-inspectable-object (arg)
"Move point to the next inspectable object.
With optional ARG, move across that many objects.
If ARG is negative, move backwards."
(interactive "p")
(let ((maxpos (point-max)) (minpos (point-min))
(previously-wrapped-p nil))
;; Forward.
(while (> arg 0)
(seq-let (pos foundp) (cider-find-inspectable-object 'next maxpos)
(if foundp
(progn (goto-char pos)
(unless (and cider-inspector-skip-uninteresting
(looking-at-p cider-inspector-uninteresting-regexp))
(setq arg (1- arg))
(setq previously-wrapped-p nil)))
(if (not previously-wrapped-p) ; cycle detection
(progn (goto-char minpos) (setq previously-wrapped-p t))
(error "No inspectable objects")))))
;; Backward.
(while (< arg 0)
(seq-let (pos foundp) (cider-find-inspectable-object 'prev minpos)
;; CIDER-OPEN-INSPECTOR inserts the title of an inspector page
;; as a presentation at the beginning of the buffer; skip
;; that. (Notice how this problem can not arise in ``Forward.'')
(if (and foundp (/= pos minpos))
(progn (goto-char pos)
(unless (and cider-inspector-skip-uninteresting
(looking-at-p cider-inspector-uninteresting-regexp))
(setq arg (1+ arg))
(setq previously-wrapped-p nil)))
(if (not previously-wrapped-p) ; cycle detection
(progn (goto-char maxpos) (setq previously-wrapped-p t))
(error "No inspectable objects")))))))
(defun cider-inspector-previous-inspectable-object (arg)
"Move point to the previous inspectable object.
With optional ARG, move across that many objects.
If ARG is negative, move forwards."
(interactive "p")
(cider-inspector-next-inspectable-object (- arg)))
(defun cider-inspector-property-at-point ()
"Return property at point."
(let* ((properties '(cider-value-idx cider-range-button
cider-action-number))
(find-property
(lambda (point)
(cl-loop for property in properties
for value = (get-text-property point property)
when value
return (list property value)))))
(or (funcall find-property (point))
(funcall find-property (max (point-min) (1- (point)))))))
(defun cider-inspector-operate-on-point ()
"Invoke the command for the text at point.
1. If point is on a value then recursively call the inspector on
that value.
2. If point is on an action then call that action.
3. If point is on a range-button fetch and insert the range."
(interactive)
(seq-let (property value) (cider-inspector-property-at-point)
(cl-case property
(cider-value-idx
(cider-inspector-push value))
;; TODO: range and action handlers
(t (error "No object at point")))))
(defun cider-inspector-operate-on-click (event)
"Move to EVENT's position and operate the part."
(interactive "@e")
(let ((point (posn-point (event-end event))))
(cond ((and point
(or (get-text-property point 'cider-value-idx)))
(goto-char point)
(cider-inspector-operate-on-point))
(t
(error "No clickable part here")))))
(provide 'cider-inspector)
;;; cider-inspector.el ends here
;;; cider-jar.el --- Jar functionality for Clojure -*- lexical-binding: t -*-
;; Copyright © 2022 Arne Brasseur
;;
;; Author: Arne Brasseur <arne@arnebrasseur.net>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Dealing with JAR (Java archive) files, which are really just zip files in
;; disguise. In particular downloading and retrieving the cider-nrepl jar.
;;; Code:
(require 'url)
(require 'arc-mode)
(require 'map)
(defvar cider-jar-cache-dir (expand-file-name "cider-cache" user-emacs-directory)
"Location where we store downloaded files for later use.")
(defvar cider-jar-content-cache (make-hash-table :test #'equal)
"Nested hash table of jar-path -> file-path -> bool.
This provides an efficient check to see if a file exists in a jar or not.")
(defun cider-jar-clojars-url (group artifact version)
"URL to download a specific jar from Clojars.
GROUP, ARTIFACT, and VERSION are the components of the Maven coordinates."
(concat "https://repo.clojars.org/" group "/" artifact "/"
version
"/cider-nrepl-"
version
".jar"))
(defun cider-jar-find-or-fetch (group artifact version)
"Download the given jar off clojars and cache it.
GROUP, ARTIFACT, and VERSION are the components of the Maven coordinates.
Returns the path to the jar."
(let* ((m2-path (expand-file-name (concat "~/.m2/repository/" group "/" artifact "/" version "/" artifact "-" version ".jar")))
(clojars-url (cider-jar-clojars-url group artifact version))
(cache-path (expand-file-name (replace-regexp-in-string "https://" "" clojars-url) cider-jar-cache-dir)))
(cond
((file-exists-p m2-path) m2-path)
((file-exists-p cache-path) cache-path)
(t
(make-directory (file-name-directory cache-path) t)
(url-copy-file clojars-url cache-path)
cache-path))))
(defun cider-jar--archive-zip-summarize ()
"Forked version of `archive-zip-summarize'.
Only read the information we need, and be version independent."
(goto-char (- (point-max) (- 22 18)))
(search-backward-regexp "[P]K\005\006")
(let ((p (archive-l-e (+ (point) 16) 4))
files)
(when (or (= p #xffffffff) (= p -1))
;; If the offset of end-of-central-directory is 0xFFFFFFFF, this
;; is a Zip64 extended ZIP file format, and we need to glean the
;; info from Zip64 records instead.
;;
;; First, find the Zip64 end-of-central-directory locator.
(search-backward "PK\006\007")
(setq p (+ (point-min)
(archive-l-e (+ (point) 8) 8)))
(goto-char p)
;; We should be at Zip64 end-of-central-directory record now.
(or (string= "PK\006\006" (buffer-substring p (+ p 4)))
(error "Unrecognized ZIP file format"))
;; Offset to central directory:
(setq p (archive-l-e (+ p 48) 8)))
(setq p (+ p (point-min)))
(while (string= "PK\001\002" (buffer-substring p (+ p 4)))
(let* ((fnlen (archive-l-e (+ p 28) 2))
(exlen (archive-l-e (+ p 30) 2))
(fclen (archive-l-e (+ p 32) 2))
(efnname (let ((str (buffer-substring (+ p 46) (+ p 46 fnlen))))
(decode-coding-string
str archive-file-name-coding-system))))
(setq files (cons efnname files)
p (+ p 46 fnlen exlen fclen))))
files))
(defun cider-jar-contents (jarfile)
"Get the list of filenames in a jar (or zip) file.
JARFILE is the location of the archive."
(with-temp-buffer
(set-buffer-multibyte nil)
(setq buffer-file-coding-system 'binary)
(insert-file-contents-literally jarfile nil)
(cider-jar--archive-zip-summarize)))
(defun cider-jar-contents-cached (jarfile)
"Like cider-jar-contents, but cached.
Instead of returning a list of strings this returns a hash table of string
keys and values `t`, for quick lookup. JARFILE is the location of the
archive."
(let ((m (map-elt cider-jar-content-cache jarfile)))
(if m
m
(let ((m (make-hash-table :test #'equal)))
(seq-do (lambda (path)
(puthash path t m))
(cider-jar-contents jarfile))
(puthash jarfile m cider-jar-content-cache)
m))))
(defun cider-jar-contains-p (jarfile name)
"Does the JARFILE contain a file with the given NAME?"
(map-elt (cider-jar-contents-cached jarfile) name))
(defun cider-jar-retrieve-resource (jarfile name)
"Extract a file NAME from a JARFILE as a string."
(make-directory archive-tmpdir :make-parents)
(when (cider-jar-contains-p jarfile name)
(let ((default-directory archive-tmpdir))
(with-temp-buffer
(set-buffer-multibyte nil)
(setq buffer-file-coding-system 'binary)
(archive-zip-extract jarfile name)
(buffer-substring-no-properties (point-min) (point-max))))))
(provide 'cider-jar)
;;; cider-jar.el ends here
;;; cider-macroexpansion.el --- Macro expansion support -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Macro expansion support.
;;; Code:
(require 'cider-mode)
(require 'subr-x)
(defconst cider-macroexpansion-buffer "*cider-macroexpansion*")
(defcustom cider-macroexpansion-display-namespaces 'tidy
"Determines if namespaces are displayed in the macroexpansion buffer.
Possible values are:
'qualified ;=> Vars are fully-qualified in the expansion
'none ;=> Vars are displayed without namespace qualification
'tidy ;=> Vars that are :refer-ed or defined in the current namespace are
displayed with their simple name, non-referred vars from other
namespaces are referred using the alias for that namespace (if
defined), other vars are displayed fully qualified."
:type '(choice (const :tag "Suppress namespaces" none)
(const :tag "Show fully-qualified namespaces" qualified)
(const :tag "Show namespace aliases" tidy))
:group 'cider
:package-version '(cider . "0.7.0"))
(defcustom cider-macroexpansion-print-metadata nil
"Determines if metadata is included in macroexpansion results."
:type 'boolean
:group 'cider
:package-version '(cider . "0.9.0"))
(defun cider-sync-request:macroexpand (expander expr &optional display-namespaces)
"Macroexpand, using EXPANDER, the given EXPR.
The default for DISPLAY-NAMESPACES is taken from
`cider-macroexpansion-display-namespaces'."
(cider-ensure-op-supported "macroexpand")
(thread-first `("op" "macroexpand"
"expander" ,expander
"code" ,expr
"ns" ,(cider-current-ns)
"display-namespaces" ,(or display-namespaces
(symbol-name cider-macroexpansion-display-namespaces)))
(nconc (when cider-macroexpansion-print-metadata
'("print-meta" "true")))
(cider-nrepl-send-sync-request)
(nrepl-dict-get "expansion")))
(defun cider-macroexpand-undo (&optional arg)
"Undo the last macroexpansion, using `undo-only'.
ARG is passed along to `undo-only'."
(interactive)
(let ((inhibit-read-only t))
(undo-only arg)))
(defvar cider-last-macroexpand-expression nil
"Specify the last macroexpansion performed.
This variable specifies both what was expanded and the expander.")
(defun cider-macroexpand-expr (expander expr)
"Macroexpand, use EXPANDER, the given EXPR."
(when-let* ((expansion (cider-sync-request:macroexpand expander expr)))
(setq cider-last-macroexpand-expression expr)
(cider-initialize-macroexpansion-buffer expansion (cider-current-ns))))
(defun cider-macroexpand-expr-inplace (expander)
"Substitute the form preceding point with its macroexpansion using EXPANDER."
(interactive)
(let* ((expansion (cider-sync-request:macroexpand expander (cider-last-sexp)))
(bounds (cons (save-excursion (clojure-backward-logical-sexp 1) (point)) (point))))
(cider-redraw-macroexpansion-buffer
expansion (current-buffer) (car bounds) (cdr bounds))))
(defun cider-macroexpand-again ()
"Repeat the last macroexpansion."
(interactive)
(cider-initialize-macroexpansion-buffer cider-last-macroexpand-expression (cider-current-ns)))
;;;###autoload
(defun cider-macroexpand-1 (&optional prefix)
"Invoke \\=`macroexpand-1\\=` on the expression preceding point.
If invoked with a PREFIX argument, use \\=`macroexpand\\=` instead of
\\=`macroexpand-1\\=`."
(interactive "P")
(let ((expander (if prefix "macroexpand" "macroexpand-1")))
(cider-macroexpand-expr expander (cider-last-sexp))))
(defun cider-macroexpand-1-inplace (&optional prefix)
"Perform inplace \\=`macroexpand-1\\=` on the expression preceding point.
If invoked with a PREFIX argument, use \\=`macroexpand\\=` instead of
\\=`macroexpand-1\\=`."
(interactive "P")
(let ((expander (if prefix "macroexpand" "macroexpand-1")))
(cider-macroexpand-expr-inplace expander)))
;;;###autoload
(defun cider-macroexpand-all ()
"Invoke \\=`macroexpand-all\\=` on the expression preceding point."
(interactive)
(cider-macroexpand-expr "macroexpand-all" (cider-last-sexp)))
(defun cider-macroexpand-all-inplace ()
"Perform inplace \\=`macroexpand-all\\=` on the expression preceding point."
(interactive)
(cider-macroexpand-expr-inplace "macroexpand-all"))
(defun cider-initialize-macroexpansion-buffer (expansion ns)
"Create a new Macroexpansion buffer with EXPANSION and namespace NS."
(pop-to-buffer (cider-create-macroexpansion-buffer))
(setq cider-buffer-ns ns)
(setq buffer-undo-list nil)
(let ((inhibit-read-only t)
(buffer-undo-list t))
(erase-buffer)
(insert (format "%s" expansion))
(goto-char (point-max))
(font-lock-ensure)))
(defun cider-redraw-macroexpansion-buffer (expansion buffer start end)
"Redraw the macroexpansion with new EXPANSION.
Text in BUFFER from START to END is replaced with new expansion,
and point is placed after the expanded form."
(with-current-buffer buffer
(let ((buffer-read-only nil))
(goto-char start)
(delete-region start end)
(insert (format "%s" expansion))
(goto-char start)
(indent-sexp)
(forward-sexp))))
(declare-function cider-mode "cider-mode")
(defun cider-create-macroexpansion-buffer ()
"Create a new macroexpansion buffer."
(with-current-buffer (cider-popup-buffer cider-macroexpansion-buffer 'select 'clojure-mode 'ancillary)
(cider-mode -1)
(cider-macroexpansion-mode 1)
(current-buffer)))
(declare-function cider-find-var "cider-find")
(defvar cider-macroexpansion-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "g") #'cider-macroexpand-again)
(define-key map (kbd "q") #'cider-popup-buffer-quit-function)
(define-key map (kbd "d") #'cider-doc)
(define-key map (kbd "j") #'cider-javadoc)
(define-key map (kbd ".") #'cider-find-var)
(define-key map (kbd "m") #'cider-macroexpand-1-inplace)
(define-key map (kbd "a") #'cider-macroexpand-all-inplace)
(define-key map (kbd "u") #'cider-macroexpand-undo)
(define-key map [remap undo] #'cider-macroexpand-undo)
(easy-menu-define cider-macroexpansion-mode-menu map
"Menu for CIDER's doc mode"
'("Macroexpansion"
["Restart expansion" cider-macroexpand-again]
["Macroexpand-1" cider-macroexpand-1-inplace]
["Macroexpand-all" cider-macroexpand-all-inplace]
["Macroexpand-undo" cider-macroexpand-undo]
["Go to source" cider-find-var]
["Go to doc" cider-doc]
["Go to Javadoc" cider-docview-javadoc]
["Quit" cider-popup-buffer-quit-function]))
map))
(define-minor-mode cider-macroexpansion-mode
"Minor mode for CIDER macroexpansion."
:lighter " Macroexpand")
(provide 'cider-macroexpansion)
;;; cider-macroexpansion.el ends here
;;; cider-mode.el --- Minor mode for REPL interactions -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Minor mode for REPL interactions.
;;; Code:
(require 'clojure-mode)
(require 'cider-eval)
(require 'cider-test) ; required only for the menu
(require 'cider-eldoc)
(require 'cider-resolve)
(require 'cider-doc) ; required only for the menu
(require 'cider-profile) ; required only for the menu
(require 'cider-completion)
(require 'cider-inspector)
(require 'cider-find)
(require 'subr-x)
(defcustom cider-mode-line-show-connection t
"If the mode-line lighter should detail the connection."
:group 'cider
:type 'boolean
:package-version '(cider "0.10.0"))
(defun cider--modeline-info ()
"Return info for the cider mode modeline.
Info contains the connection type, project name and host:port endpoint."
(if-let* ((current-connection (ignore-errors (cider-current-repl))))
(with-current-buffer current-connection
(concat
(symbol-name cider-repl-type)
(when cider-mode-line-show-connection
(format ":%s@%s:%s"
(or (cider--project-name nrepl-project-dir) "<no project>")
(pcase (plist-get nrepl-endpoint :host)
("localhost" "")
(x x))
(plist-get nrepl-endpoint :port)))))
"not connected"))
;;;###autoload
(defcustom cider-mode-line
'(:eval (format " cider[%s]" (cider--modeline-info)))
"Mode line lighter for cider mode.
The value of this variable is a mode line template as in
`mode-line-format'. See Info Node `(elisp)Mode Line Format' for details
about mode line templates.
Customize this variable to change how cider mode displays its status in the
mode line. The default value displays the current connection. Set this
variable to nil to disable the mode line entirely."
:group 'cider
:type 'sexp
:risky t
:package-version '(cider "0.7.0"))
;;; Switching between REPL & source buffers
(defun cider--switch-to-repl-buffer (repl-buffer &optional set-namespace)
"Select the REPL-BUFFER, when possible in an existing window.
When SET-NAMESPACE is t, sets the namespace in the REPL buffer to
that of the namespace in the Clojure source buffer."
(let ((buffer (current-buffer)))
;; first we switch to the REPL buffer
(if cider-repl-display-in-current-window
(pop-to-buffer-same-window repl-buffer)
(pop-to-buffer repl-buffer))
;; then if necessary we update its namespace
(when set-namespace
(cider-repl-set-ns (with-current-buffer buffer (cider-current-ns))))
(goto-char (point-max))))
(defun cider-switch-to-repl-buffer (&optional set-namespace)
"Switch to current REPL buffer, when possible in an existing window.
The type of the REPL is inferred from the mode of current buffer. With a
prefix arg SET-NAMESPACE sets the namespace in the REPL buffer to that of
the namespace in the Clojure source buffer"
(interactive "P")
(cider--switch-to-repl-buffer
(cider-current-repl nil 'ensure)
set-namespace))
(declare-function cider-load-buffer "cider-eval")
(defun cider-load-buffer-and-switch-to-repl-buffer (&optional set-namespace)
"Load the current buffer into the matching REPL buffer and switch to it.
When SET-NAMESPACE is true, we'll also set the REPL's ns to match that of the
Clojure buffer."
(interactive "P")
(cider-load-buffer)
(cider-switch-to-repl-buffer set-namespace))
(defun cider-switch-to-last-clojure-buffer ()
"Switch to the last Clojure buffer.
The default keybinding for this command is
the same as variable `cider-switch-to-repl-buffer',
so that it is very convenient to jump between a
Clojure buffer and the REPL buffer."
(interactive)
(if (derived-mode-p 'cider-repl-mode)
(let* ((a-buf)
(the-buf (let ((repl-type (cider-repl-type-for-buffer)))
(seq-find (lambda (b)
(unless (with-current-buffer b (derived-mode-p 'cider-repl-mode))
(when-let* ((type (cider-repl-type-for-buffer b)))
(unless a-buf
(setq a-buf b))
(or (eq type 'multi)
(eq type repl-type)))))
(buffer-list)))))
(if-let* ((buf (or the-buf a-buf)))
(if cider-repl-display-in-current-window
(pop-to-buffer-same-window buf)
(pop-to-buffer buf))
(user-error "No Clojure buffer found")))
(user-error "Not in a CIDER REPL buffer")))
(defun cider-find-and-clear-repl-output (&optional clear-repl)
"Find the current REPL buffer and clear it.
With a prefix argument CLEAR-REPL the command clears the entire REPL
buffer. Returns to the buffer in which the command was invoked. See also
the related commands `cider-repl-clear-buffer' and
`cider-repl-clear-output'."
(interactive "P")
(let ((origin-buffer (current-buffer)))
(switch-to-buffer (cider-current-repl nil 'ensure))
(if clear-repl
(cider-repl-clear-buffer)
(cider-repl-clear-output))
(switch-to-buffer origin-buffer)))
;;; cider-run
(defvar cider--namespace-history nil
"History of user input for namespace prompts.")
(defun cider--var-namespace (var)
"Return the namespace of VAR.
VAR is a fully qualified Clojure variable name as a string."
(replace-regexp-in-string "\\(?:#'\\)?\\(.*\\)/.*" "\\1" var))
(defun cider-run (&optional function)
"Run -main or FUNCTION, prompting for its namespace if necessary.
With a prefix argument, prompt for function to run instead of -main."
(interactive (list (when current-prefix-arg (read-string "Function name: "))))
(cider-ensure-connected)
(let ((name (or function "-main")))
(when-let* ((response (cider-nrepl-send-sync-request
`("op" "ns-list-vars-by-name"
"name" ,name))))
(if-let* ((vars (split-string (substring (nrepl-dict-get response "var-list") 1 -1))))
(cider-interactive-eval
(if (= (length vars) 1)
(concat "(" (car vars) ")")
(let* ((completions (mapcar #'cider--var-namespace vars))
(def (or (car cider--namespace-history)
(car completions))))
(format "(#'%s/%s)"
(completing-read (format "Namespace (%s): " def)
completions nil t nil
'cider--namespace-history def)
name))))
(user-error "No %s var defined in any namespace" (cider-propertize name 'fn))))))
;;; Insert (and eval) in REPL functionality
(defvar cider-insert-commands-map
(let ((map (define-prefix-command 'cider-insert-commands-map)))
;; single key bindings defined last for display in menu
(define-key map (kbd "e") #'cider-insert-last-sexp-in-repl)
(define-key map (kbd "d") #'cider-insert-defun-in-repl)
(define-key map (kbd "r") #'cider-insert-region-in-repl)
(define-key map (kbd "n") #'cider-insert-ns-form-in-repl)
;; duplicates with C- for convenience
(define-key map (kbd "C-e") #'cider-insert-last-sexp-in-repl)
(define-key map (kbd "C-d") #'cider-insert-defun-in-repl)
(define-key map (kbd "C-r") #'cider-insert-region-in-repl)
(define-key map (kbd "C-n") #'cider-insert-ns-form-in-repl)))
(defcustom cider-switch-to-repl-on-insert t
"Whether to switch to the REPL when inserting a form into the REPL."
:type 'boolean
:group 'cider
:package-version '(cider . "0.21.0"))
(defcustom cider-invert-insert-eval-p nil
"Whether to invert the behavior of evaling.
Default behavior when inserting is to NOT eval the form and only eval with
a prefix. This allows to invert this so that default behavior is to insert
and eval and the prefix is required to prevent evaluation."
:type 'boolean
:group 'cider
:package-version '(cider . "0.18.0"))
(defun cider-insert-in-repl (form eval)
"Insert FORM in the REPL buffer and switch to it.
If EVAL is non-nil the form will also be evaluated. Use
`cider-invert-insert-eval-p' to invert this behavior."
(while (string-match "\\`[ \t\n\r]+\\|[ \t\n\r]+\\'" form)
(setq form (replace-match "" t t form)))
(when cider-switch-to-repl-on-insert
(cider-switch-to-repl-buffer))
(let ((repl (cider-current-repl)))
(with-selected-window (or (get-buffer-window repl)
(selected-window))
(with-current-buffer repl
(goto-char (point-max))
(let ((beg (point)))
(insert form)
(indent-region beg (point))
(font-lock-ensure beg (point)))
(when (if cider-invert-insert-eval-p
(not eval)
eval)
(cider-repl-return))
(goto-char (point-max))))))
(defun cider-insert-last-sexp-in-repl (&optional arg)
"Insert the expression preceding point in the REPL buffer.
If invoked with a prefix ARG eval the expression after inserting it."
(interactive "P")
(cider-insert-in-repl (cider-last-sexp) arg))
(defun cider-insert-defun-in-repl (&optional arg)
"Insert the top level form at point in the REPL buffer.
If invoked with a prefix ARG eval the expression after inserting it."
(interactive "P")
(cider-insert-in-repl (cider-defun-at-point) arg))
(defun cider-insert-region-in-repl (start end &optional arg)
"Insert the current region in the REPL buffer.
START and END represent the region's boundaries.
If invoked with a prefix ARG eval the expression after inserting it."
(interactive "rP")
(cider-insert-in-repl
(buffer-substring-no-properties start end) arg))
(defun cider-insert-ns-form-in-repl (&optional arg)
"Insert the current buffer's ns form in the REPL buffer.
If invoked with a prefix ARG eval the expression after inserting it."
(interactive "P")
(cider-insert-in-repl (cider-ns-form) arg))
;;; The menu-bar
(defconst cider-mode-menu
`("CIDER"
["Start or connect to any REPL" cider
:help "A simple wrapper around all commands for starting/connecting to a REPL."]
("Clojure"
["Start a Clojure REPL" cider-jack-in
:help "Starts an nREPL server and connects a Clojure REPL to it."]
["Connect to a Clojure REPL" cider-connect
:help "Connects to a REPL that's already running."])
("ClojureScript"
["Start a ClojureScript REPL" cider-jack-in-cljs
:help "Starts an nREPL server and connects a ClojureScript REPL to it."]
["Connect to a ClojureScript REPL" cider-connect-clojurescript
:help "Connects to a ClojureScript REPL that's already running."]
["Create a ClojureScript REPL from a Clojure REPL" cider-jack-in-sibling-clojurescript])
"--"
["Quit" cider-quit :active (cider-connected-p)]
["Restart" cider-restart :active (cider-connected-p)]
"--"
["Connection info" cider-describe-connection
:active (cider-connected-p)]
["Select any CIDER buffer" cider-selector]
"--"
["Configure CIDER" (customize-group 'cider)]
"--"
["A sip of CIDER" cider-drink-a-sip]
["View user manual" cider-view-manual]
["View quick reference card" cider-view-refcard]
["Report a bug" cider-report-bug]
["Version info" cider-version]
"--"
["Close ancillary buffers" cider-close-ancillary-buffers
:active (seq-remove #'null cider-ancillary-buffers)]
("nREPL" :active (cider-connected-p)
["List nREPL middleware" cider-list-nrepl-middleware]
["Describe nREPL session" cider-describe-nrepl-session]
["Toggle message logging" nrepl-toggle-message-logging]))
"Menu for CIDER mode.")
(defconst cider-mode-eval-menu
'("CIDER Eval" :visible (cider-connected-p)
["Eval top-level sexp" cider-eval-defun-at-point]
["Eval top-level sexp to point" cider-eval-defun-up-to-point]
["Eval top-level sexp to comment" cider-eval-defun-to-comment]
["Eval top-level sexp and pretty-print to comment" cider-pprint-eval-defun-to-comment]
"--"
["Eval current list" cider-eval-list-at-point]
["Eval current sexp" cider-eval-sexp-at-point]
["Eval and tap current sexp" cider-tap-sexp-at-point]
["Eval current sexp to point" cider-eval-sexp-up-to-point]
["Eval current sexp in context" cider-eval-sexp-at-point-in-context]
"--"
["Eval last sexp" cider-eval-last-sexp]
["Eval and tap last sexp" cider-tap-last-sexp]
["Eval last sexp in context" cider-eval-last-sexp-in-context]
["Eval last sexp and insert" cider-eval-print-last-sexp
:keys "\\[universal-argument] \\[cider-eval-last-sexp]"]
["Eval last sexp in popup buffer" cider-pprint-eval-last-sexp]
["Eval last sexp and replace" cider-eval-last-sexp-and-replace]
["Eval last sexp to REPL" cider-eval-last-sexp-to-repl]
["Eval last sexp and pretty-print to REPL" cider-pprint-eval-last-sexp-to-repl]
["Eval last sexp and pretty-print to comment" cider-pprint-eval-last-sexp-to-comment]
"--"
["Eval selected region" cider-eval-region]
["Eval ns form" cider-eval-ns-form]
"--"
["Interrupt evaluation" cider-interrupt]
"--"
["Insert last sexp in REPL" cider-insert-last-sexp-in-repl]
["Insert last sexp in REPL and eval" (cider-insert-last-sexp-in-repl t)
:keys "\\[universal-argument] \\[cider-insert-last-sexp-in-repl]"]
["Insert top-level sexp in REPL" cider-insert-defun-in-repl]
["Insert region in REPL" cider-insert-region-in-repl]
["Insert ns form in REPL" cider-insert-ns-form-in-repl]
"--"
["Load this buffer" cider-load-buffer]
["Load this buffer and switch to REPL" cider-load-buffer-and-switch-to-repl-buffer]
["Load another file" cider-load-file]
["Recursively load all files in directory" cider-load-all-files]
["Load all project files" cider-load-all-project-ns]
["Refresh loaded code" cider-ns-refresh]
["Require and reload" cider-ns-reload]
["Require and reload all" cider-ns-reload-all]
["Run project (-main function)" cider-run])
"Menu for CIDER mode eval commands.")
(defconst cider-mode-interactions-menu
`("CIDER Interactions" :visible (cider-connected-p)
["Complete symbol" complete-symbol]
"--"
("REPL"
["Set REPL to this ns" cider-repl-set-ns]
["Switch to REPL" cider-switch-to-repl-buffer]
["REPL Pretty Print" cider-repl-toggle-pretty-printing
:style toggle :selected cider-repl-use-pretty-printing]
["Clear latest output" cider-find-and-clear-repl-output]
["Clear all output" (cider-find-and-clear-repl-output t)
:keys "\\[universal-argument] \\[cider-find-and-clear-repl-output]"]
"--"
["Configure the REPL" (customize-group 'cider-repl)])
,cider-doc-menu
("Find (jump to)"
["Find definition" cider-find-var]
["Find namespace" cider-find-ns]
["Find resource" cider-find-resource]
["Find keyword" cider-find-keyword]
["Go back" cider-pop-back])
("Xref"
["Find fn references" cider-xref-fn-refs]
["Find fn references and select" cider-xref-fn-refs-select]
["Find fn dependencies" cider-xref-fn-defs]
["Find fn dependencies and select" cider-xref-fn-defs-select])
("Browse"
["Browse namespace" cider-browse-ns]
["Browse all namespaces" cider-browse-ns-all]
["Browse spec" cider-browse-spec]
["Browse all specs" cider-browse-spec-all]
["Browse REPL input history" cider-repl-history]
["Browse classpath" cider-classpath]
["Browse classpath entry" cider-open-classpath-entry])
("Format"
["Format EDN last sexp" cider-format-edn-last-sexp]
["Format EDN region" cider-format-edn-region]
["Format EDN buffer" cider-format-edn-buffer])
("Macroexpand"
["Macroexpand-1" cider-macroexpand-1]
["Macroexpand-all" cider-macroexpand-all])
,cider-test-menu
("Debug"
["Inspect" cider-inspect]
["Toggle var tracing" cider-toggle-trace-var]
["Toggle ns tracing" cider-toggle-trace-ns]
"--"
["Debug top-level form" cider-debug-defun-at-point
:keys "\\[universal-argument] \\[cider-eval-defun-at-point]"]
["List instrumented defs" cider-browse-instrumented-defs]
"--"
["Configure the Debugger" (customize-group 'cider-debug)])
,cider-profile-menu
("Misc"
["Clojure Cheatsheet" cider-cheatsheet]
["Flush completion cache" cider-completion-flush-caches]))
"Menu for CIDER interactions.")
(declare-function cider-ns-refresh "cider-ns")
(declare-function cider-ns-reload "cider-ns")
(declare-function cider-ns-reload-all "cider-ns")
(declare-function cider-browse-ns "cider-browse-ns")
(declare-function cider-eval-ns-form "cider-eval")
(declare-function cider-repl-set-ns "cider-repl")
(declare-function cider-find-ns "cider-find")
(defvar cider-ns-map
(let ((map (define-prefix-command 'cider-ns-map)))
(define-key map (kbd "b") #'cider-browse-ns)
(define-key map (kbd "M-b") #'cider-browse-ns)
(define-key map (kbd "e") #'cider-eval-ns-form)
(define-key map (kbd "M-e") #'cider-eval-ns-form)
(define-key map (kbd "f") #'cider-find-ns)
(define-key map (kbd "M-f") #'cider-find-ns)
(define-key map (kbd "n") #'cider-repl-set-ns)
(define-key map (kbd "M-n") #'cider-repl-set-ns)
(define-key map (kbd "r") #'cider-ns-refresh)
(define-key map (kbd "M-r") #'cider-ns-refresh)
(define-key map (kbd "l") #'cider-ns-reload)
(define-key map (kbd "M-l") #'cider-ns-reload-all)
map)
"CIDER NS keymap.")
;; Those declares are needed, because we autoload all those commands when first
;; used. That optimizes CIDER's initial load time.
(declare-function cider-macroexpand-1 "cider-macroexpansion")
(declare-function cider-macroexpand-all "cider-macroexpansion")
(declare-function cider-selector "cider-selector")
(declare-function cider-toggle-trace-ns "cider-tracing")
(declare-function cider-toggle-trace-var "cider-tracing")
(declare-function cider-find-resource "cider-find")
(declare-function cider-find-keyword "cider-find")
(declare-function cider-find-var "cider-find")
(declare-function cider-find-dwim-at-mouse "cider-find")
(declare-function cider-xref-fn-refs "cider-xref")
(declare-function cider-xref-fn-refs-select "cider-xref")
(declare-function cider-xref-fn-deps "cider-xref")
(declare-function cider-xref-fn-deps-select "cider-xref")
(defconst cider--has-many-mouse-buttons (not (memq window-system '(mac ns)))
"Non-nil if system binds forward and back buttons to <mouse-8> and <mouse-9>.
As it stands Emacs fires these events on <mouse-8> and <mouse-9> on 'x' and
'w32'systems while on macOS it presents them on <mouse-4> and <mouse-5>.")
(defcustom cider-use-xref t
"Enable xref integration."
:type 'boolean
:safe #'booleanp
:group 'cider
:version '(cider . "1.2.0"))
(defcustom cider-xref-fn-depth -90
"The depth to use when adding the CIDER xref function to the relevant hook.
By convention this is a number between -100 and 100, lower numbers indicating a
higher precedence."
:type 'integer
:group 'cider
:version '(cider . "1.2.0"))
(defconst cider-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-d") 'cider-doc-map)
(unless cider-use-xref
(define-key map (kbd "M-.") #'cider-find-var)
(define-key map (kbd "M-,") #'cider-pop-back))
(define-key map (kbd (if cider--has-many-mouse-buttons "<mouse-8>" "<mouse-4>")) #'xref-pop-marker-stack)
(define-key map (kbd (if cider--has-many-mouse-buttons "<mouse-9>" "<mouse-5>")) #'cider-find-dwim-at-mouse)
(define-key map (kbd "C-c C-.") #'cider-find-ns)
(define-key map (kbd "C-c C-:") #'cider-find-keyword)
(define-key map (kbd "C-c M-.") #'cider-find-resource)
(define-key map (kbd "M-TAB") #'complete-symbol)
(define-key map (kbd "C-M-x") #'cider-eval-defun-at-point)
(define-key map (kbd "C-c C-c") #'cider-eval-defun-at-point)
(define-key map (kbd "C-x C-e") #'cider-eval-last-sexp)
(define-key map (kbd "C-c C-e") #'cider-eval-last-sexp)
(define-key map (kbd "C-c C-p") #'cider-pprint-eval-last-sexp)
(define-key map (kbd "C-c C-f") #'cider-pprint-eval-defun-at-point)
(define-key map (kbd "C-c C-v") 'cider-eval-commands-map)
(define-key map (kbd "C-c C-j") 'cider-insert-commands-map)
(define-key map (kbd "C-c M-;") #'cider-eval-defun-to-comment)
(define-key map (kbd "C-c M-e") #'cider-eval-last-sexp-to-repl)
(define-key map (kbd "C-c M-p") #'cider-insert-last-sexp-in-repl)
(define-key map (kbd "C-c M-:") #'cider-read-and-eval)
(define-key map (kbd "C-c C-u") #'cider-undef)
(define-key map (kbd "C-c C-M-u") #'cider-undef-all)
(define-key map (kbd "C-c C-m") #'cider-macroexpand-1)
(define-key map (kbd "C-c M-m") #'cider-macroexpand-all)
(define-key map (kbd "C-c M-n") 'cider-ns-map)
(define-key map (kbd "C-c M-i") #'cider-inspect)
(define-key map (kbd "C-c M-t v") #'cider-toggle-trace-var)
(define-key map (kbd "C-c M-t n") #'cider-toggle-trace-ns)
(define-key map (kbd "C-c C-z") #'cider-switch-to-repl-buffer)
(define-key map (kbd "C-c M-z") #'cider-load-buffer-and-switch-to-repl-buffer)
(define-key map (kbd "C-c C-o") #'cider-find-and-clear-repl-output)
(define-key map (kbd "C-c C-k") #'cider-load-buffer)
(define-key map (kbd "C-c C-l") #'cider-load-file)
(define-key map (kbd "C-c C-M-l") #'cider-load-all-files)
(define-key map (kbd "C-c C-b") #'cider-interrupt)
(define-key map (kbd "C-c ,") 'cider-test-commands-map)
(define-key map (kbd "C-c C-t") 'cider-test-commands-map)
(define-key map (kbd "C-c M-s") #'cider-selector)
(define-key map (kbd "C-c M-d") #'cider-describe-connection)
(define-key map (kbd "C-c C-=") 'cider-profile-map)
(define-key map (kbd "C-c C-? r") #'cider-xref-fn-refs)
(define-key map (kbd "C-c C-? C-r") #'cider-xref-fn-refs-select)
(define-key map (kbd "C-c C-? d") #'cider-xref-fn-deps)
(define-key map (kbd "C-c C-? C-d") #'cider-xref-fn-deps-select)
(define-key map (kbd "C-c C-q") #'cider-quit)
(define-key map (kbd "C-c M-r") #'cider-restart)
(dolist (variable '(cider-mode-interactions-menu
cider-mode-eval-menu
cider-mode-menu))
(easy-menu-do-define (intern (format "%s-open" variable))
map
(get variable 'variable-documentation)
(cider--menu-add-help-strings (symbol-value variable))))
map))
;; This menu works as an easy entry-point into CIDER. Even if cider.el isn't
;; loaded yet, this will be shown in Clojure buffers next to the "Clojure"
;; menu.
;;;###autoload
(with-eval-after-load 'clojure-mode
(easy-menu-define cider-clojure-mode-menu-open clojure-mode-map
"Menu for Clojure mode.
This is displayed in `clojure-mode' buffers, if `cider-mode' is not active."
`("CIDER" :visible (not cider-mode)
["Start a Clojure REPL" cider-jack-in-clj
:help "Starts an nREPL server and connects a Clojure REPL to it."]
["Connect to a Clojure REPL" cider-connect-clj
:help "Connects to a REPL that's already running."]
["Start a ClojureScript REPL" cider-jack-in-cljs
:help "Starts an nREPL server and connects a ClojureScript REPL to it."]
["Connect to a ClojureScript REPL" cider-connect-cljs
:help "Connects to a ClojureScript REPL that's already running."]
["Start a Clojure REPL, and a ClojureScript REPL" cider-jack-in-clj&cljs
:help "Starts an nREPL server, connects a Clojure REPL to it, and then a ClojureScript REPL."]
"--"
["View user manual" cider-view-manual])))
;;; Dynamic indentation
(defcustom cider-dynamic-indentation t
"Whether CIDER should aid Clojure(Script) indentation.
If non-nil, CIDER uses runtime information (such as the \":style/indent\"
metadata) to improve standard `clojure-mode' indentation.
If nil, CIDER won't interfere with `clojure-mode's indentation.
Toggling this variable only takes effect after a file is closed and
re-visited."
:type 'boolean
:package-version '(cider . "0.11.0")
:group 'cider)
(defun cider--get-symbol-indent (symbol-name)
"Return the indent metadata for SYMBOL-NAME in the current namespace."
(let* ((ns (let ((clojure-cache-ns t)) ; we force ns caching here for performance reasons
;; silence bytecode warning of unused lexical var
(ignore clojure-cache-ns)
(cider-current-ns))))
(if-let* ((meta (cider-resolve-var ns symbol-name))
(indent (or (nrepl-dict-get meta "style/indent")
(nrepl-dict-get meta "indent"))))
(let ((format (format ":indent metadata on ‘%s’ is unreadable! \nERROR: %%s"
symbol-name)))
(with-demoted-errors format
(cider--deep-vector-to-list (read indent))))
;; There's no indent metadata, but there might be a clojure-mode
;; indent-spec with fully-qualified namespace.
(when (string-match cider-resolve--prefix-regexp symbol-name)
(when-let* ((sym (intern-soft (replace-match (save-match-data
(cider-resolve-alias ns (match-string 1 symbol-name)))
t t symbol-name 1))))
(get sym 'clojure-indent-function))))))
;;; Dynamic font locking
(defcustom cider-font-lock-dynamically '(macro core deprecated)
"Specifies how much dynamic font-locking CIDER should use.
Dynamic font-locking this refers to applying syntax highlighting to vars
defined in the currently active nREPL connection. This is done in addition
to `clojure-mode's usual (static) font-lock, so even if you set this
variable to nil you'll still see basic syntax highlighting.
The value is a list of symbols, each one indicates a different type of var
that should be font-locked:
`macro' (default): Any defined macro gets the `font-lock-keyword-face'.
`function': Any defined function gets the `font-lock-function-face'.
`var': Any non-local var gets the `font-lock-variable-name-face'.
`deprecated' (default): Any deprecated var gets the `cider-deprecated-face'
face.
`core' (default): Any symbol from clojure.core (face depends on type).
The value can also be t, which means to font-lock as much as possible."
:type '(choice (set :tag "Fine-tune font-locking"
(const :tag "Any defined macro" macro)
(const :tag "Any defined function" function)
(const :tag "Any defined var" var)
(const :tag "Any defined deprecated" deprecated)
(const :tag "Any symbol from clojure.core" core))
(const :tag "Font-lock as much as possible" t))
:group 'cider
:package-version '(cider . "0.10.0"))
(defcustom cider-font-lock-reader-conditionals t
"Apply font-locking to unused reader conditional expressions.
The result depends on the buffer CIDER connection type."
:type 'boolean
:group 'cider
:package-version '(cider . "0.15.0"))
(defface cider-deprecated-face
'((((background light)) :background "light goldenrod")
(((background dark)) :background "#432"))
"Face used on deprecated vars."
:group 'cider)
(defface cider-instrumented-face
'((((type graphic)) :box (:color "#c00" :line-width -1))
(t :underline t :background "#800"))
"Face used to mark code being debugged."
:group 'cider-debug
:group 'cider
:package-version '(cider . "0.10.0"))
(defface cider-traced-face
'((((type graphic)) :box (:color "cyan" :line-width -1))
(t :underline t :background "#066"))
"Face used to mark code being traced."
:group 'cider
:package-version '(cider . "0.11.0"))
(defface cider-reader-conditional-face
'((t (:inherit font-lock-comment-face)))
"Face used to mark unused reader conditional expressions."
:group 'cider
:package-version '(cider . "0.15.0"))
(defconst cider-reader-conditionals-regexp "\\(?:#\\?@?[[:space:]\n]*(\\)"
"Regexp for matching reader conditionals with a non-capturing group.
Starts from the reader macro characters to the opening parentheses.")
(defvar cider--reader-conditionals-match-data (list nil nil)
"Reusable list for `match-data` in reader conditionals font lock matchers.")
(defun cider--search-reader-conditionals (limit)
"Matcher for finding reader conditionals.
Search is done with the given LIMIT."
(when (and cider-font-lock-reader-conditionals
(cider-connected-p))
(when (search-forward-regexp cider-reader-conditionals-regexp limit t)
(let ((start (match-beginning 0))
(state (syntax-ppss)))
(if (or (nth 3 state) (nth 4 state)) ; inside string or comment?
(cider--search-reader-conditionals limit)
(when (<= (point) limit)
(ignore-errors
(let ((md (match-data nil cider--reader-conditionals-match-data)))
(setf (nth 0 md) start)
(setf (nth 1 md) (point))
(set-match-data md)
t))))))))
(defun cider--anchored-search-suppressed-forms-internal (repl-types limit)
"Helper function for `cider--anchored-search-suppressed-forms`.
REPL-TYPES is a list of strings repl-type strings. LIMIT is the same as
the LIMIT in `cider--anchored-search-suppressed-forms`"
(when (= (length repl-types) 1)
(let ((type (car repl-types))
(expr (read (current-buffer)))
(start (save-excursion (backward-sexp) (point))))
(when (<= (point) limit)
(forward-sexp)
(if (not (string-equal (symbol-name expr) (concat ":" type)))
(ignore-errors
(cl-assert (<= (point) limit))
(let ((md (match-data nil cider--reader-conditionals-match-data)))
(setf (nth 0 md) start)
(setf (nth 1 md) (point))
(set-match-data md)
t))
(cider--anchored-search-suppressed-forms-internal repl-types limit))))))
(defun cider--anchored-search-suppressed-forms (limit)
"Matcher for finding unused reader conditional expressions.
An unused reader conditional expression is an expression for a platform
that does not match the CIDER connection for the buffer. Search is done
with the given LIMIT."
(let ((repl-types (seq-uniq (seq-map
(lambda (repl)
(symbol-name (cider-repl-type repl)))
(cider-repls))))
(result 'retry))
(while (and (eq result 'retry) (<= (point) limit))
(condition-case condition
(setq result
(cider--anchored-search-suppressed-forms-internal
repl-types limit))
(invalid-read-syntax
(setq result 'retry))
(wrong-type-argument
(setq result 'retry))
(scan-error
(setq result 'retry))
(end-of-file
(setq result nil))
(error
(setq result nil)
(message
"Error during fontification while searching for forms: %S"
condition))))
(if (eq result 'retry) (setq result nil))
result))
(defconst cider--reader-conditionals-font-lock-keywords
'((cider--search-reader-conditionals
(cider--anchored-search-suppressed-forms
(save-excursion
(let* ((state (syntax-ppss))
(list-pt (nth 1 state)))
(when list-pt
(goto-char list-pt)
(forward-list)
(backward-char)
(point))))
nil
(0 'cider-reader-conditional-face t))))
"Font Lock keywords for unused reader conditionals in CIDER mode.")
(defun cider--unless-local-match (value)
"Return VALUE, unless `match-string' is a local var."
(unless (or (get-text-property (point) 'cider-block-dynamic-font-lock)
(member (match-string 0)
(get-text-property (point) 'cider-locals)))
value))
(defun cider--compile-font-lock-keywords (symbols-plist core-plist)
"Return a list of font-lock rules for symbols in SYMBOLS-PLIST, CORE-PLIST."
(let ((cider-font-lock-dynamically (if (eq cider-font-lock-dynamically t)
'(function var macro core deprecated)
cider-font-lock-dynamically))
deprecated enlightened
macros functions vars instrumented traced)
(cl-labels ((handle-plist
(plist)
(let ((do-function (memq 'function cider-font-lock-dynamically))
(do-var (memq 'var cider-font-lock-dynamically))
(do-macro (memq 'macro cider-font-lock-dynamically))
(do-deprecated (memq 'deprecated cider-font-lock-dynamically)))
(while plist
(let ((sym (pop plist))
(meta (pop plist)))
(pcase (nrepl-dict-get meta "cider/instrumented")
(`nil nil)
(`"\"breakpoint-if-interesting\""
(push sym instrumented))
(`"\"light-form\""
(push sym enlightened)))
;; The ::traced keywords can be inlined by MrAnderson, so
;; we catch that case too.
;; FIXME: This matches values too, not just keys.
(when (seq-find (lambda (k) (and (stringp k)
(string-match (rx "clojure.tools.trace/traced" eos) k)))
meta)
(push sym traced))
(when (and do-deprecated (nrepl-dict-get meta "deprecated"))
(push sym deprecated))
(let ((is-macro (nrepl-dict-get meta "macro"))
(is-function (or (nrepl-dict-get meta "fn")
(nrepl-dict-get meta "arglists"))))
(cond ((and do-macro is-macro)
(push sym macros))
((and do-function is-function)
(push sym functions))
((and do-var (not is-function) (not is-macro))
(push sym vars)))))))))
(when (memq 'core cider-font-lock-dynamically)
(let ((cider-font-lock-dynamically '(function var macro core deprecated)))
(handle-plist core-plist)))
(handle-plist symbols-plist))
`(
,@(when macros
`((,(concat (rx (or "(" "#'")) ; Can't take the value of macros.
"\\(" (regexp-opt macros 'symbols) "\\)")
1 (cider--unless-local-match font-lock-keyword-face))))
,@(when functions
`((,(regexp-opt functions 'symbols) 0
(cider--unless-local-match font-lock-function-name-face))))
,@(when vars
`((,(regexp-opt vars 'symbols) 0
(cider--unless-local-match font-lock-variable-name-face))))
,@(when deprecated
`((,(regexp-opt deprecated 'symbols) 0
(cider--unless-local-match 'cider-deprecated-face) append)))
,@(when enlightened
`((,(regexp-opt enlightened 'symbols) 0
(cider--unless-local-match 'cider-enlightened-face) append)))
,@(when instrumented
`((,(regexp-opt instrumented 'symbols) 0
(cider--unless-local-match 'cider-instrumented-face) append)))
,@(when traced
`((,(regexp-opt traced 'symbols) 0
(cider--unless-local-match 'cider-traced-face) append))))))
(defconst cider--static-font-lock-keywords
(eval-when-compile
`((,(regexp-opt '("#break" "#dbg" "#light") 'symbols) 0 font-lock-warning-face)))
"Default expressions to highlight in CIDER mode.")
(defvar-local cider--dynamic-font-lock-keywords nil)
(defun cider-refresh-dynamic-font-lock (&optional ns)
"Ensure that the current buffer has up-to-date font-lock rules.
NS defaults to `cider-current-ns', and it can also be a dict describing the
namespace itself."
(interactive)
(when (and cider-font-lock-dynamically
font-lock-mode)
(font-lock-remove-keywords nil cider--dynamic-font-lock-keywords)
(when-let* ((ns (or ns (cider-current-ns)))
(symbols (cider-resolve-ns-symbols ns)))
(setq-local cider--dynamic-font-lock-keywords
(cider--compile-font-lock-keywords
symbols (cider-resolve-ns-symbols (cider-resolve-core-ns))))
(font-lock-add-keywords nil cider--dynamic-font-lock-keywords 'end))
(font-lock-flush)))
;;; Detecting local variables
(defun cider--read-locals-from-next-sexp ()
"Return a list of all locals inside the next logical sexp."
(save-excursion
(ignore-errors
(clojure-forward-logical-sexp 1)
(let ((out nil)
(end (point)))
(forward-sexp -1)
;; FIXME: This returns locals found inside the :or clause of a
;; destructuring map.
(while (search-forward-regexp "\\_<[^:&]\\(\\sw\\|\\s_\\)*\\_>" end 'noerror)
(push (match-string-no-properties 0) out))
out))))
(defun cider--read-locals-from-bindings-vector ()
"Return a list of all locals inside the next bindings vector."
(save-excursion
(ignore-errors
(cider-start-of-next-sexp)
(when (eq (char-after) ?\[)
(forward-char 1)
(let ((out nil))
(setq out (append (cider--read-locals-from-next-sexp) out))
(while (ignore-errors (clojure-forward-logical-sexp 3)
(unless (eobp)
(forward-sexp -1)
t))
(setq out (append (cider--read-locals-from-next-sexp) out)))
out)))))
(defun cider--read-locals-from-arglist ()
"Return a list of all locals in current form's arglist(s)."
(let ((out nil))
(save-excursion
(ignore-errors
(cider-start-of-next-sexp)
;; Named fn
(when (looking-at-p "\\s_\\|\\sw")
(cider-start-of-next-sexp 1))
;; Docstring
(when (eq (char-after) ?\")
(cider-start-of-next-sexp 1))
;; Attribute map
(when (eq (char-after) ?{)
(cider-start-of-next-sexp 1))
;; The arglist
(pcase (char-after)
(?\[ (setq out (cider--read-locals-from-next-sexp)))
;; FIXME: This returns false positives. It takes all arglists of a
;; function and returns all args it finds. The logic should be changed
;; so that each arglist applies to its own scope.
(?\( (ignore-errors
(while (eq (char-after) ?\()
(save-excursion
(forward-char 1)
(setq out (append (cider--read-locals-from-next-sexp) out)))
(cider-start-of-next-sexp 1)))))))
out))
(defun cider--parse-and-apply-locals (end &optional outer-locals)
"Figure out local variables between point and END.
A list of these variables is set as the `cider-locals' text property over
the code where they are in scope.
Optional argument OUTER-LOCALS is used to specify local variables defined
before point."
(while (search-forward-regexp "(\\(ns\\_>\\|def\\|fn\\|for\\b\\|loop\\b\\|with-\\|do[a-z]+\\|\\([a-z]+-\\)?let\\b\\)"
end 'noerror)
(goto-char (match-beginning 0))
(let ((sym (match-string 1))
(sexp-end (save-excursion
(or (ignore-errors (forward-sexp 1)
(point))
end))))
;; #1324: Don't do dynamic font-lock in `ns' forms, they are special
;; macros where nothing is evaluated, so we'd get a lot of false
;; positives.
(if (equal sym "ns")
(add-text-properties (point) sexp-end '(cider-block-dynamic-font-lock t))
(forward-char 1)
(forward-sexp 1)
(let ((locals (append outer-locals
(pcase sym
((or "fn" "def" "") (cider--read-locals-from-arglist))
(_ (cider--read-locals-from-bindings-vector))))))
(add-text-properties (point) sexp-end (list 'cider-locals locals))
(clojure-forward-logical-sexp 1)
(cider--parse-and-apply-locals sexp-end locals)))
(goto-char sexp-end))))
(defun cider--update-locals-for-region (beg end)
"Update the `cider-locals' text property for region from BEG to END."
(save-excursion
(goto-char beg)
;; If the inside of a `ns' form changed, reparse it from the start.
(when (and (not (bobp))
(get-text-property (1- (point)) 'cider-block-dynamic-font-lock))
(ignore-errors (beginning-of-defun)))
(save-excursion
;; Move up until we reach a sexp that encloses the entire region (or
;; a top-level sexp), and set that as the new BEG.
(goto-char end)
(while (and (or (> (point) beg)
(not (eq (char-after) ?\()))
(condition-case nil
(progn (backward-up-list) t)
(scan-error nil))))
(setq beg (min beg (point)))
;; If there are locals above the current sexp, reapply them to the
;; current sexp.
(let ((locals-above (when (> beg (point-min))
(get-text-property (1- beg) 'cider-locals))))
(condition-case nil
(clojure-forward-logical-sexp 1)
(error (goto-char end)))
(add-text-properties beg (point) `(cider-locals ,locals-above))
;; Extend the region being font-locked to include whole sexps.
(setq end (max end (point)))
(goto-char beg)
(ignore-errors
(cider--parse-and-apply-locals end locals-above))))))
(defun cider--docview-as-string (sym info)
"Return a string of what would be displayed by `cider-docview-render'.
SYM and INFO is passed to `cider-docview-render'"
(with-temp-buffer
(cider-docview-render (current-buffer) sym info)
(goto-char (point-max))
(forward-line -1)
(replace-regexp-in-string
"[`']" "\\\\=\\&"
(buffer-substring-no-properties (point-min) (1- (point))))))
(defcustom cider-use-tooltips t
"If non-nil, CIDER displays mouse-over tooltips.
It does this as well as the `help-echo' mechanism."
:group 'cider
:type 'boolean
:package-version '(cider "0.12.0"))
(defvar cider--debug-mode-response)
(defvar cider--debug-mode)
(defun cider--help-echo (_ obj pos)
"Return the help-echo string for OBJ at POS.
See \(info \"(elisp) Special Properties\")"
(while-no-input
(when (and (bufferp obj)
(cider-connected-p)
cider-use-tooltips
(not (eq help-at-pt-display-when-idle t)))
(with-current-buffer obj
(ignore-errors
(save-excursion
(goto-char pos)
(when-let* ((sym (cider-symbol-at-point)))
(if (member sym (get-text-property (point) 'cider-locals))
(concat (format "`%s' is a local" sym)
(when cider--debug-mode
(let* ((locals (nrepl-dict-get cider--debug-mode-response "locals"))
(local-val (cadr (assoc sym locals))))
(format " with value:\n%s" local-val))))
(let* ((info (cider-sync-request:info sym))
(candidates (nrepl-dict-get info "candidates")))
(if candidates
(concat "There were ambiguities resolving this symbol:\n\n"
(mapconcat (lambda (x) (cider--docview-as-string sym x))
candidates
(concat "\n\n" (make-string 60 ?-) "\n\n")))
(cider--docview-as-string sym info)))))))))))
(defun cider--wrap-fontify-locals (func)
"Return a function that will call FUNC after parsing local variables.
The local variables are stored in a list under the `cider-locals' text
property."
(lambda (beg end &rest rest)
(with-silent-modifications
(remove-text-properties beg end '(cider-locals nil cider-block-dynamic-font-lock nil))
(when cider-use-tooltips
(add-text-properties beg end '(help-echo cider--help-echo)))
(when cider-font-lock-dynamically
(cider--update-locals-for-region beg end)))
(apply func beg end rest)))
;;; Minor-mode definition
(defvar x-gtk-use-system-tooltips)
;;;###autoload
(define-minor-mode cider-mode
"Minor mode for REPL interaction from a Clojure buffer.
\\{cider-mode-map}"
:init-value nil
:lighter cider-mode-line
:keymap cider-mode-map
(if cider-mode
(progn
(setq-local sesman-system 'CIDER)
(cider-eldoc-setup)
(add-hook 'completion-at-point-functions #'cider-complete-at-point nil t)
(font-lock-add-keywords nil cider--static-font-lock-keywords)
(cider-refresh-dynamic-font-lock)
(font-lock-add-keywords nil cider--reader-conditionals-font-lock-keywords)
;; `font-lock-mode' might get enabled after `cider-mode'.
(add-hook 'font-lock-mode-hook #'cider-refresh-dynamic-font-lock nil 'local)
(setq-local font-lock-fontify-region-function
(cider--wrap-fontify-locals font-lock-fontify-region-function))
;; GTK tooltips look bad, and we have no control over the face.
(setq-local x-gtk-use-system-tooltips nil)
;; `tooltip' has variable-width by default, which looks terrible.
(set-face-attribute 'tooltip nil :inherit 'unspecified)
(when cider-dynamic-indentation
(setq-local clojure-get-indent-function #'cider--get-symbol-indent))
(setq-local clojure-expected-ns-function #'cider-expected-ns)
(when cider-use-xref
(add-hook 'xref-backend-functions #'cider--xref-backend cider-xref-fn-depth 'local))
(setq next-error-function #'cider-jump-to-compilation-error))
;; Mode cleanup
(mapc #'kill-local-variable '(next-error-function
x-gtk-use-system-tooltips
font-lock-fontify-region-function
clojure-get-indent-function))
(remove-hook 'completion-at-point-functions #'cider-complete-at-point t)
(when cider-use-xref
(remove-hook 'xref-backend-functions #'cider--xref-backend 'local))
(remove-hook 'font-lock-mode-hook #'cider-refresh-dynamic-font-lock 'local)
(font-lock-add-keywords nil cider--reader-conditionals-font-lock-keywords)
(font-lock-remove-keywords nil cider--dynamic-font-lock-keywords)
(font-lock-remove-keywords nil cider--static-font-lock-keywords)
(font-lock-flush)
(remove-hook 'completion-at-point-functions #'cider-complete-at-point t)))
(defun cider-set-buffer-ns (ns)
"Set this buffer's namespace to NS and refresh font-locking."
(setq-local cider-buffer-ns ns)
(when (or cider-mode (derived-mode-p 'cider-repl-mode))
(cider-refresh-dynamic-font-lock ns)))
(provide 'cider-mode)
;;; cider-mode.el ends here
;;; cider-ns.el --- Namespace manipulation functionality -*- lexical-binding: t -*-
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Smart code refresh functionality based on ideas from:
;; http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded
;;
;; Note that refresh with clojure.tools.namespace.repl is a smarter way to
;; reload code: the traditional way to reload Clojure code without restarting
;; the JVM is (require ... :reload) or an editor/IDE feature that does the same
;; thing.
;;
;; This has several problems:
;;
;; If you modify two namespaces which depend on each other, you must remember to
;; reload them in the correct order to avoid compilation errors.
;;
;; If you remove definitions from a source file and then reload it, those
;; definitions are still available in memory. If other code depends on those
;; definitions, it will continue to work but will break the next time you
;; restart the JVM.
;;
;; If the reloaded namespace contains defmulti, you must also reload all of the
;; associated defmethod expressions.
;;
;; If the reloaded namespace contains defprotocol, you must also reload any
;; records or types implementing that protocol and replace any existing
;; instances of those records/types with new instances.
;;
;; If the reloaded namespace contains macros, you must also reload any
;; namespaces which use those macros.
;;
;; If the running program contains functions which close over values in the
;; reloaded namespace, those closed-over values are not updated (This is common
;; in web applications which construct the "handler stack" as a composition of
;; functions.)
;;; Code:
(require 'map)
(require 'seq)
(require 'subr-x)
(require 'cider-client)
(require 'cider-eval)
(require 'cider-popup)
(require 'cider-stacktrace)
(defcustom cider-ns-save-files-on-refresh 'prompt
"Controls whether to prompt to save files before refreshing.
If nil, files are not saved.
If 'prompt, the user is prompted to save files if they have been modified.
If t, save the files without confirmation."
:type '(choice (const prompt :tag "Prompt to save files if they have been modified")
(const nil :tag "Don't save the files")
(const t :tag "Save the files without confirmation"))
:group 'cider
:package-version '(cider . "0.15.0"))
(defcustom cider-ns-save-files-on-refresh-modes '(clojure-mode)
"Controls which files might be saved before refreshing.
If a list of modes, any buffers visiting files on the classpath whose major
mode is derived from any of the modes might be saved.
If t, all buffers visiting files on the classpath might be saved."
:type '(choice listp
(const t))
:group 'cider
:package-version '(cider . "0.21.0"))
(defconst cider-ns-refresh-log-buffer "*cider-ns-refresh-log*")
(defcustom cider-ns-refresh-show-log-buffer nil
"Controls when to display the refresh log buffer.
If non-nil, the log buffer will be displayed every time `cider-ns-refresh' is
called. If nil, the log buffer will still be written to, but will never be
displayed automatically. Instead, the most relevant information will be
displayed in the echo area."
:type '(choice (const :tag "always" t)
(const :tag "never" nil))
:group 'cider
:package-version '(cider . "0.10.0"))
(defcustom cider-ns-refresh-before-fn nil
"Clojure function for `cider-ns-refresh' to call before reloading.
If nil, nothing will be invoked before reloading. Must be a
namespace-qualified function of zero arity. Any thrown exception will
prevent reloading from occurring."
:type 'string
:group 'cider
:package-version '(cider . "0.10.0"))
(defcustom cider-ns-refresh-after-fn nil
"Clojure function for `cider-ns-refresh' to call after reloading.
If nil, nothing will be invoked after reloading. Must be a
namespace-qualified function of zero arity."
:type 'string
:group 'cider
:package-version '(cider . "0.10.0"))
(defun cider-ns-refresh--handle-response (response log-buffer)
"Refresh LOG-BUFFER with RESPONSE."
(nrepl-dbind-response response (out err reloading status error error-ns after before)
(cl-flet* ((log (message &optional face)
(cider-emit-into-popup-buffer log-buffer message face t))
(log-echo (message &optional face)
(log message face)
(unless cider-ns-refresh-show-log-buffer
(let ((message-truncate-lines t))
(message "cider-ns-refresh: %s" message)))))
(cond
(out
(log out))
(err
(log err 'font-lock-warning-face))
((member "invoking-before" status)
(log-echo (format "Calling %s\n" before) 'font-lock-string-face))
((member "invoked-before" status)
(log-echo (format "Successfully called %s\n" before) 'font-lock-string-face))
((member "invoked-not-resolved" status)
(log-echo "Could not resolve refresh function\n" 'font-lock-string-face))
(reloading
(log-echo (format "Reloading %s\n" reloading) 'font-lock-string-face))
((member "reloading" (nrepl-dict-keys response))
(log-echo "Nothing to reload\n" 'font-lock-string-face))
((member "ok" status)
(log-echo "Reloading successful\n" 'font-lock-string-face))
(error-ns
(log-echo (format "Error reloading %s\n" error-ns) 'font-lock-warning-face))
((member "invoking-after" status)
(log-echo (format "Calling %s\n" after) 'font-lock-string-face))
((member "invoked-after" status)
(log-echo (format "Successfully called %s\n" after) 'font-lock-string-face))))
(with-selected-window (or (get-buffer-window cider-ns-refresh-log-buffer)
(selected-window))
(with-current-buffer cider-ns-refresh-log-buffer
(goto-char (point-max))))
(when (member "error" status)
(cider--render-stacktrace-causes error))))
(defun cider-ns-refresh--save-modified-buffers ()
"Ensure any relevant modified buffers are saved before refreshing.
Its behavior is controlled by `cider-ns-save-files-on-refresh' and
`cider-ns-save-files-on-refresh-modes'."
(when cider-ns-save-files-on-refresh
(let ((dirs (seq-filter #'file-directory-p
(cider-classpath-entries))))
(save-some-buffers
(not (eq cider-ns-save-files-on-refresh 'prompt))
(lambda ()
(and (seq-some #'derived-mode-p cider-ns-save-files-on-refresh-modes)
(seq-some (lambda (dir)
(file-in-directory-p buffer-file-name dir))
dirs)))))))
;;;###autoload
(defun cider-ns-reload (&optional prompt)
"Send a (require 'ns :reload) to the REPL.
With an argument PROMPT, it prompts for a namespace name. This is the
Clojure out of the box reloading experience and does not rely on
org.clojure/tools.namespace. See Commentary of this file for a longer list
of differences. From the Clojure doc: \":reload forces loading of all the
identified libs even if they are already loaded\"."
(interactive "P")
(let ((ns (if prompt
(string-remove-prefix "'" (read-from-minibuffer "Namespace: " (clojure-find-ns)))
(clojure-find-ns))))
(cider-interactive-eval (format "(require '%s :reload)" ns))))
;;;###autoload
(defun cider-ns-reload-all (&optional prompt)
"Send a (require 'ns :reload-all) to the REPL.
With an argument PROMPT, it prompts for a namespace name. This is the
Clojure out of the box reloading experience and does not rely on
org.clojure/tools.namespace. See Commentary of this file for a longer list
of differences. From the Clojure doc: \":reload-all implies :reload and
also forces loading of all libs that the identified libs directly or
indirectly load via require\"."
(interactive "P")
(let ((ns (if prompt
(string-remove-prefix "'" (read-from-minibuffer "Namespace: " (clojure-find-ns)))
(clojure-find-ns))))
(cider-interactive-eval (format "(require '%s :reload-all)" ns))))
;;;###autoload
(defun cider-ns-refresh (&optional mode)
"Reload modified and unloaded namespaces on the classpath.
With a single prefix argument, or if MODE is `refresh-all', reload all
namespaces on the classpath unconditionally.
With a double prefix argument, or if MODE is `clear', clear the state of
the namespace tracker before reloading. This is useful for recovering from
some classes of error (for example, those caused by circular dependencies)
that a normal reload would not otherwise recover from. The trade-off of
clearing is that stale code from any deleted files may not be completely
unloaded.
With a negative prefix argument, or if MODE is `inhibit-fns', prevent any
refresh functions (defined in `cider-ns-refresh-before-fn' and
`cider-ns-refresh-after-fn') from being invoked."
(interactive "p")
(cider-ensure-connected)
(cider-ensure-op-supported "refresh")
(cider-ns-refresh--save-modified-buffers)
(let ((clear? (member mode '(clear 16)))
(refresh-all? (member mode '(refresh-all 4)))
(inhibit-refresh-fns (member mode '(inhibit-fns -1))))
(cider-map-repls :clj
(lambda (conn)
;; Inside the lambda, so the buffer is not created if we error out.
(let ((log-buffer (or (get-buffer cider-ns-refresh-log-buffer)
(cider-make-popup-buffer cider-ns-refresh-log-buffer))))
(when cider-ns-refresh-show-log-buffer
(cider-popup-buffer-display log-buffer))
(when inhibit-refresh-fns
(cider-emit-into-popup-buffer log-buffer
"inhibiting refresh functions\n"
nil
t))
(when clear?
(cider-nrepl-send-sync-request '("op" "refresh-clear") conn))
(cider-nrepl-send-request
(thread-last
(map-merge 'list
`(("op" ,(if refresh-all? "refresh-all" "refresh")))
(cider--nrepl-print-request-map fill-column)
(when (and (not inhibit-refresh-fns) cider-ns-refresh-before-fn)
`(("before" ,cider-ns-refresh-before-fn)))
(when (and (not inhibit-refresh-fns) cider-ns-refresh-after-fn)
`(("after" ,cider-ns-refresh-after-fn))))
(seq-mapcat #'identity))
(lambda (response)
(cider-ns-refresh--handle-response response log-buffer))
conn))))))
(provide 'cider-ns)
;;; cider-ns.el ends here
;;; cider-overlays.el --- Managing CIDER overlays -*- lexical-binding: t; -*-
;; Copyright © 2015-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Use `cider--make-overlay' to place a generic overlay at point. Or use
;; `cider--make-result-overlay' to place an interactive eval result overlay at
;; the end of a specified line.
;;; Code:
(require 'cider-common)
(require 'subr-x)
(require 'cl-lib)
;;; Customization
(defface cider-result-overlay-face
'((((class color) (background light))
:background "grey90" :box (:line-width -1 :color "yellow"))
(((class color) (background dark))
:background "grey10" :box (:line-width -1 :color "black")))
"Face used to display evaluation results at the end of line.
If `cider-overlays-use-font-lock' is non-nil, this face is
applied with lower priority than the syntax highlighting."
:group 'cider
:package-version '(cider "0.9.1"))
(defface cider-error-overlay-face
'((((class color) (background light))
:background "orange red"
:extend t)
(((class color) (background dark))
:background "firebrick"
:extend t))
"Like `cider-result-overlay-face', but for evaluation errors."
:group 'cider
:package-version '(cider "0.25.0"))
(defcustom cider-result-use-clojure-font-lock t
"If non-nil, interactive eval results are font-locked as Clojure code."
:group 'cider
:type 'boolean
:package-version '(cider . "0.10.0"))
(defcustom cider-overlays-use-font-lock t
"If non-nil, results overlays are font-locked as Clojure code.
If nil, apply `cider-result-overlay-face' to the entire overlay instead of
font-locking it."
:group 'cider
:type 'boolean
:package-version '(cider . "0.10.0"))
(defcustom cider-use-overlays 'both
"Whether to display evaluation results with overlays.
If t, use overlays determined by `cider-result-overlay-position'.
If nil, display on the echo area.
If both, display on both places.
Only applies to evaluation commands. To configure the debugger overlays,
see `cider-debug-use-overlays'."
:type '(choice (const :tag "Display using overlays" t)
(const :tag "Display in echo area" nil)
(const :tag "Both" both))
:group 'cider
:package-version '(cider . "0.10.0"))
(defcustom cider-result-overlay-position 'at-eol
"Where to display result overlays for inline evaluation and the debugger.
If 'at-eol, display at the end of the line.
If 'at-point, display at the end of the respective sexp."
:group 'cider
:type ''(choice (const :tag "End of line" at-eol)
(const :tag "End of sexp" at-point))
:package-version '(cider . "0.23.0"))
(defcustom cider-eval-result-prefix "=> "
"The prefix displayed in the minibuffer before a result value."
:type 'string
:group 'cider
:package-version '(cider . "0.5.0"))
(defcustom cider-eval-result-duration 'command
"Duration, in seconds, of CIDER's eval-result overlays.
If nil, overlays last indefinitely.
If the symbol `command', they're erased after the next command.
If the symbol `change', they last until the next change to the buffer.
Also see `cider-use-overlays'."
:type '(choice (integer :tag "Duration in seconds")
(const :tag "Until next command" command)
(const :tag "Until next buffer change" change)
(const :tag "Last indefinitely" nil))
:group 'cider
:package-version '(cider . "0.10.0"))
;;; Overlay logic
(defun cider--delete-overlay (ov &rest _)
"Safely delete overlay OV.
Never throws errors, and can be used in an overlay's modification-hooks."
(ignore-errors (delete-overlay ov)))
(defun cider--make-overlay (l r type &rest props)
"Place an overlay between L and R and return it.
TYPE is a symbol put on the overlay's category property. It is used to
easily remove all overlays from a region with:
(remove-overlays start end 'category TYPE)
PROPS is a plist of properties and values to add to the overlay."
(let ((o (make-overlay l (or r l) (current-buffer))))
(overlay-put o 'category type)
(overlay-put o 'cider-temporary t)
(while props (overlay-put o (pop props) (pop props)))
(push #'cider--delete-overlay (overlay-get o 'modification-hooks))
o))
(defun cider--remove-result-overlay (&rest _)
"Remove result overlay from current buffer.
This function also removes itself from `post-command-hook' and
`after-change-functions'."
(let ((hook (pcase cider-eval-result-duration
(`command 'post-command-hook)
(`change 'after-change-functions))))
(remove-hook hook #'cider--remove-result-overlay 'local))
(remove-overlays nil nil 'category 'result))
(defun cider--remove-result-overlay-after-command ()
"Add `cider--remove-result-overlay' locally to `post-command-hook'.
This function also removes itself from `post-command-hook'."
(remove-hook 'post-command-hook #'cider--remove-result-overlay-after-command 'local)
(add-hook 'post-command-hook #'cider--remove-result-overlay nil 'local))
(defface cider-fringe-good-face
'((((class color) (background light)) :foreground "lightgreen")
(((class color) (background dark)) :foreground "darkgreen"))
"Face used on the fringe indicator for successful evaluation."
:group 'cider)
(defconst cider--fringe-overlay-good
(propertize " " 'display '(left-fringe empty-line cider-fringe-good-face))
"The before-string property that adds a green indicator on the fringe.")
(defcustom cider-use-fringe-indicators t
"Whether to display evaluation indicators on the left fringe."
:safe #'booleanp
:group 'cider
:type 'boolean
:package-version '(cider . "0.13.0"))
(defun cider--make-fringe-overlay (&optional end)
"Place an eval indicator at the fringe before a sexp.
END is the position where the sexp ends, and defaults to point."
(when cider-use-fringe-indicators
(with-current-buffer (if (markerp end)
(marker-buffer end)
(current-buffer))
(save-excursion
(if end
(goto-char end)
(setq end (point)))
(clojure-forward-logical-sexp -1)
;; Create the green-circle overlay.
(cider--make-overlay (point) end 'cider-fringe-indicator
'before-string cider--fringe-overlay-good)))))
(cl-defun cider--make-result-overlay (value &rest props &key where duration (type 'result)
(format (concat " " cider-eval-result-prefix "%s "))
(prepend-face 'cider-result-overlay-face)
&allow-other-keys)
"Place an overlay displaying VALUE at the position determined by WHERE.
VALUE is used as the overlay's after-string property, meaning it is
displayed at the end of the overlay.
Return nil if the overlay was not placed or if it might not be visible, and
return the overlay otherwise.
Return the overlay if it was placed successfully, and nil if it failed.
This function takes some optional keyword arguments:
If WHERE is a number or a marker, apply the overlay as determined by
`cider-result-overlay-position'. If it is a cons cell, the car and cdr
determine the start and end of the overlay.
DURATION takes the same possible values as the
`cider-eval-result-duration' variable.
TYPE is passed to `cider--make-overlay' (defaults to `result').
FORMAT is a string passed to `format'. It should have
exactly one %s construct (for VALUE).
All arguments beyond these (PROPS) are properties to be used on the
overlay."
(declare (indent 1))
(while (keywordp (car props))
(setq props (cdr (cdr props))))
;; If the marker points to a dead buffer, don't do anything.
(let ((buffer (cond
((markerp where) (marker-buffer where))
((markerp (car-safe where)) (marker-buffer (car where)))
(t (current-buffer)))))
(with-current-buffer buffer
(save-excursion
(when (number-or-marker-p where)
(goto-char where))
;; Make sure the overlay is actually at the end of the sexp.
(skip-chars-backward "\r\n[:blank:]")
(let* ((beg (if (consp where)
(car where)
(save-excursion
(clojure-backward-logical-sexp 1)
(point))))
(end (if (consp where)
(cdr where)
(pcase cider-result-overlay-position
('at-eol (line-end-position))
('at-point (point)))))
;; Specify `default' face, otherwise unformatted text will
;; inherit the face of the following text.
(display-string (format (propertize format 'face 'default) value))
(o nil))
(remove-overlays beg end 'category type)
(funcall (if cider-overlays-use-font-lock
#'font-lock-prepend-text-property
#'put-text-property)
0 (length display-string)
'face prepend-face
display-string)
;; If the display spans multiple lines or is very long, display it at
;; the beginning of the next line.
(when (or (string-match "\n." display-string)
(> (string-width display-string)
(- (window-width) (current-column))))
(setq display-string (concat " \n" display-string)))
;; Put the cursor property only once we're done manipulating the
;; string, since we want it to be at the first char.
(put-text-property 0 1 'cursor 0 display-string)
(when (> (string-width display-string) (* 3 (window-width)))
(setq display-string
(concat (substring display-string 0 (* 3 (window-width)))
(substitute-command-keys
"...\nResult truncated. Type `\\[cider-inspect-last-result]' to inspect it."))))
;; Create the result overlay.
(setq o (apply #'cider--make-overlay
beg end type
'after-string display-string
props))
(pcase duration
((pred numberp) (run-at-time duration nil #'cider--delete-overlay o))
(`command
;; If inside a command-loop, tell `cider--remove-result-overlay'
;; to only remove after the *next* command.
(if this-command
(add-hook 'post-command-hook
#'cider--remove-result-overlay-after-command
nil 'local)
(cider--remove-result-overlay-after-command)))
(`change
(add-hook 'after-change-functions
#'cider--remove-result-overlay
nil 'local)))
(when-let* ((win (get-buffer-window buffer)))
;; Left edge is visible.
(when (and (<= (window-start win) (point) (window-end win))
;; Right edge is visible. This is a little conservative
;; if the overlay contains line breaks.
(or (< (+ (current-column) (string-width value))
(window-width win))
(not truncate-lines)))
o)))))))
;;; Displaying eval result
(defun cider--display-interactive-eval-result (value &optional point overlay-face)
"Display the result VALUE of an interactive eval operation.
VALUE is syntax-highlighted and displayed in the echo area.
OVERLAY-FACE is the face applied to the overlay, which defaults to
`cider-result-overlay-face' if nil.
If POINT and `cider-use-overlays' are non-nil, it is also displayed in an
overlay at the end of the line containing POINT.
Note that, while POINT can be a number, it's preferable to be a marker, as
that will better handle some corner cases where the original buffer is not
focused."
(let* ((font-value (if cider-result-use-clojure-font-lock
(cider-font-lock-as-clojure value)
value))
(used-overlay (when (and point cider-use-overlays)
(cider--make-result-overlay font-value
:where point
:duration cider-eval-result-duration
:prepend-face (or overlay-face 'cider-result-overlay-face)))))
(message
"%s"
(propertize (format "%s%s" cider-eval-result-prefix font-value)
;; The following hides the message from the echo-area, but
;; displays it in the Messages buffer. We only hide the message
;; if the user wants to AND if the overlay succeeded.
'invisible (and used-overlay
(not (eq cider-use-overlays 'both)))))))
;;; Fragile buttons
(defface cider-fragile-button-face
'((((type graphic))
:box (:line-width 3 :style released-button)
:inherit font-lock-warning-face)
(t :inverse-video t))
"Face for buttons that vanish when clicked."
:package-version '(cider . "0.12.0")
:group 'cider)
(define-button-type 'cider-fragile
'action #'cider--overlay-destroy
'follow-link t
'face nil
'modification-hooks '(cider--overlay-destroy)
'help-echo "RET: delete this.")
(defun cider--overlay-destroy (ov &rest r)
"Delete overlay OV and its underlying text.
If any other arguments are given (collected in R), only actually do anything
if the first one is non-nil. This is so it works in `modification-hooks'."
(unless (and r (not (car r)))
(let ((inhibit-modification-hooks t)
(beg (copy-marker (overlay-start ov)))
(end (copy-marker (overlay-end ov))))
(delete-overlay ov)
(delete-region beg end)
(goto-char beg)
(when (= (char-after) (char-before) ?\n)
(delete-char 1)))))
(provide 'cider-overlays)
;;; cider-overlays.el ends here
(define-package "cider" "20221026.1100" "Clojure Interactive Development Environment that Rocks"
'((emacs "26")
(clojure-mode "5.15.1")
(parseedn "1.0.6")
(queue "0.2")
(spinner "1.7")
(seq "2.22")
(sesman "0.3.2"))
:commit "d08609feb7141b8dbece6abb958bddd4ef0129d5" :authors
'(("Tim King" . "kingtim@gmail.com")
("Phil Hagelberg" . "technomancy@gmail.com")
("Bozhidar Batsov" . "bozhidar@batsov.dev")
("Artur Malabarba" . "bruce.connor.am@gmail.com")
("Hugo Duncan" . "hugo@hugoduncan.org")
("Steve Purcell" . "steve@sanityinc.com"))
:maintainer
'("Bozhidar Batsov" . "bozhidar@batsov.dev")
:keywords
'("languages" "clojure" "cider")
:url "http://www.github.com/clojure-emacs/cider")
;; Local Variables:
;; no-byte-compile: t
;; End:
;;; cider-popup.el --- Creating and quitting popup buffers -*- lexical-binding: t; -*-
;; Copyright © 2015-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Common functionality for dealing with popup buffers.
;;; Code:
(require 'subr-x)
(define-minor-mode cider-popup-buffer-mode
"Mode for CIDER popup buffers."
:lighter (" cider-tmp")
:keymap '(("q" . cider-popup-buffer-quit-function)))
(defvar-local cider-popup-buffer-quit-function #'cider-popup-buffer-quit
"The function that is used to quit a temporary popup buffer.")
(defun cider-popup-buffer-quit-function (&optional kill-buffer-p)
"Wrapper to invoke the function `cider-popup-buffer-quit-function'.
KILL-BUFFER-P is passed along."
(interactive)
(funcall cider-popup-buffer-quit-function kill-buffer-p))
(defun cider-popup-buffer (name &optional select mode ancillary)
"Create new popup buffer called NAME.
If SELECT is non-nil, select the newly created window.
If major MODE is non-nil, enable it for the popup buffer.
If ANCILLARY is non-nil, the buffer is added to `cider-ancillary-buffers'
and automatically removed when killed."
(thread-first (cider-make-popup-buffer name mode ancillary)
(cider-popup-buffer-display select)))
(defun cider-popup-buffer-display (buffer &optional select)
"Display BUFFER.
If SELECT is non-nil, select the BUFFER."
(let ((window (get-buffer-window buffer 'visible)))
(when window
(with-current-buffer buffer
(set-window-point window (point))))
;; If the buffer we are popping up is already displayed in the selected
;; window, the below `inhibit-same-window' logic will cause it to be
;; displayed twice - so we early out in this case. Note that we must check
;; `selected-window', as async request handlers are executed in the context
;; of the current connection buffer (i.e. `current-buffer' is dynamically
;; bound to that).
(unless (eq window (selected-window))
;; Non nil `inhibit-same-window' ensures that current window is not covered
;; Non nil `inhibit-switch-frame' ensures that the other frame is not selected
;; if that's where the buffer is being shown.
(funcall (if select #'pop-to-buffer #'display-buffer)
buffer `(nil . ((inhibit-same-window . ,pop-up-windows)
(reusable-frames . visible))))))
buffer)
(defun cider-popup-buffer-quit (&optional kill)
"Quit the current (temp) window.
Bury its buffer using `quit-restore-window'.
If prefix argument KILL is non-nil, kill the buffer instead of burying it."
(interactive)
(quit-restore-window (selected-window) (if kill 'kill 'append)))
(defvar-local cider-popup-output-marker nil)
(defvar cider-ancillary-buffers nil
"A list ancillary buffers created by the various CIDER commands.
We track them mostly to be able to clean them up on quit.")
(defun cider-make-popup-buffer (name &optional mode ancillary)
"Create a temporary buffer called NAME using major MODE (if specified).
If ANCILLARY is non-nil, the buffer is added to `cider-ancillary-buffers'
and automatically removed when killed."
(with-current-buffer (get-buffer-create name)
(kill-all-local-variables)
(setq buffer-read-only nil)
(erase-buffer)
(when mode
(funcall mode))
(cider-popup-buffer-mode 1)
(setq cider-popup-output-marker (point-marker))
(setq buffer-read-only t)
(when ancillary
(add-to-list 'cider-ancillary-buffers name)
(add-hook 'kill-buffer-hook
(lambda ()
(setq cider-ancillary-buffers
(remove name cider-ancillary-buffers)))
nil 'local))
(current-buffer)))
(defun cider-emit-into-popup-buffer (buffer value &optional face inhibit-indent)
"Emit into BUFFER the provided VALUE optionally using FACE.
Indent emitted value (usually a sexp) unless INHIBIT-INDENT is specified
and non-nil."
;; Long string output renders Emacs unresponsive and users might intentionally
;; kill the frozen popup buffer. Therefore, we don't re-create the buffer and
;; silently ignore the output.
(when (buffer-live-p buffer)
(with-current-buffer buffer
(let ((inhibit-read-only t)
(buffer-undo-list t)
(moving (= (point) cider-popup-output-marker)))
(save-excursion
(goto-char cider-popup-output-marker)
(let ((value-str (format "%s" value)))
(when face
(add-face-text-property 0 (length value-str) face nil value-str))
(insert value-str))
(unless inhibit-indent
(indent-sexp))
(set-marker cider-popup-output-marker (point)))
(when moving (goto-char cider-popup-output-marker))))))
(provide 'cider-popup)
;;; cider-popup.el ends here
;;; cider-profile.el --- CIDER support for profiling -*- lexical-binding: t; -*-
;; Copyright © 2014-2022 Edwin Watkeys and CIDER contributors
;; Author: Edwin Watkeys <edw@poseur.com>
;; Juan E. Maya <jmayaalv@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Provides coarse-grained interactive profiling support.
;; Based on earlier work by Edwin Watkeys (https://github.com/thunknyc/nrepl-profile).
;;; Code:
(require 'cider-client)
(require 'cider-popup)
(require 'cider-eval)
(defconst cider-profile-buffer "*cider-profile*")
(defvar cider-profile-map
(let ((map (define-prefix-command 'cider-profile-map)))
(define-key map (kbd "t") #'cider-profile-toggle)
(define-key map (kbd "C-t") #'cider-profile-toggle)
(define-key map (kbd "c") #'cider-profile-clear)
(define-key map (kbd "C-c") #'cider-profile-clear)
(define-key map (kbd "S") #'cider-profile-summary)
(define-key map (kbd "C-S") #'cider-profile-summary)
(define-key map (kbd "s") #'cider-profile-var-summary)
(define-key map (kbd "C-s") #'cider-profile-var-summary)
(define-key map (kbd "n") #'cider-profile-ns-toggle)
(define-key map (kbd "C-n") #'cider-profile-ns-toggle)
(define-key map (kbd "v") #'cider-profile-var-profiled-p)
(define-key map (kbd "C-v") #'cider-profile-var-profiled-p)
(define-key map (kbd "+") #'cider-profile-samples)
(define-key map (kbd "C-+") #'cider-profile-samples)
map)
"CIDER profiler keymap.")
(defconst cider-profile-menu
'("Profile"
["Toggle var profiling" cider-profile-toggle]
["Toggle namespace profiling" cider-profile-ns-toggle]
"--"
["Display var profiling status" cider-profile-var-profiled-p]
["Display max sample count" cider-profile-samples]
["Display var summary" cider-profile-var-summary]
["Display summary" cider-profile-summary]
["Clear data" cider-profile-clear])
"CIDER profiling submenu.")
(defun cider-profile--make-response-handler (handler &optional buffer)
"Make a response handler using value handler HANDLER for connection BUFFER.
Optional argument BUFFER defaults to current buffer."
(nrepl-make-response-handler
(or buffer (current-buffer)) handler nil nil nil))
;;;###autoload
(defun cider-profile-samples (&optional query)
"Displays current max-sample-count.
If optional QUERY is specified, set max-sample-count and display new value."
(interactive "P")
(cider-ensure-op-supported "set-max-samples")
(cider-ensure-op-supported "get-max-samples")
(if (not (null query))
(cider-nrepl-send-request
(let ((max-samples (if (numberp query) query '())))
(message "query: %s" max-samples)
`("op" "set-max-samples" "max-samples" ,max-samples))
(cider-profile--make-response-handler
(lambda (_buffer value)
(let ((value (if (zerop (length value)) "unlimited" value)))
(message "max-sample-count is now %s" value)))))
(cider-nrepl-send-request
'("op" "get-max-samples")
(cider-profile--make-response-handler
(lambda (_buffer value)
(let ((value (if (zerop (length value)) "unlimited" value)))
(message "max-sample-count is now %s" value))))))
query)
;;;###autoload
(defun cider-profile-var-profiled-p (query)
"Displays the profiling status of var under point.
Prompts for var if none under point or QUERY is present."
(interactive "P")
(cider-ensure-op-supported "is-var-profiled")
(cider-read-symbol-name
"Report profiling status for var: "
(lambda (sym)
(let ((ns (cider-current-ns)))
(cider-nrepl-send-request
`("op" "is-var-profiled"
"ns" ,ns
"sym" ,sym)
(cider-profile--make-response-handler
(lambda (_buffer value)
(pcase value
("profiled" (message "Profiling is currently enabled for %s/%s" ns sym))
("unprofiled" (message "Profiling is currently disabled for %s/%s" ns sym))
("unbound" (message "%s/%s is unbound" ns sym)))))))))
query)
;;;###autoload
(defun cider-profile-ns-toggle (&optional query)
"Toggle profiling for the ns associated with optional QUERY.
If optional argument QUERY is non-nil, prompt for ns. Otherwise use
current ns."
(interactive "P")
(cider-ensure-op-supported "toggle-profile-ns")
(let ((ns (if query
(completing-read "Toggle profiling for ns: "
(cider-sync-request:ns-list))
(cider-current-ns))))
(cider-nrepl-send-request
`("op" "toggle-profile-ns"
"ns" ,ns)
(cider-profile--make-response-handler
(lambda (_buffer value)
(pcase value
("profiled" (message "Profiling enabled for %s" ns))
("unprofiled" (message "Profiling disabled for %s" ns)))))))
query)
;;;###autoload
(defun cider-profile-toggle (query)
"Toggle profiling for the given QUERY.
Defaults to the symbol at point.
With prefix arg or no symbol at point, prompts for a var."
(interactive "P")
(cider-ensure-op-supported "toggle-profile")
(cider-read-symbol-name
"Toggle profiling for var: "
(lambda (sym)
(let ((ns (cider-current-ns)))
(cider-nrepl-send-request
`("op" "toggle-profile"
"ns" ,ns
"sym" ,sym)
(cider-profile--make-response-handler
(lambda (_buffer value)
(pcase value
("profiled" (message "Profiling enabled for %s/%s" ns sym))
("unprofiled" (message "Profiling disabled for %s/%s" ns sym))
("unbound" (message "%s/%s is unbound" ns sym)))))))))
query)
(defun cider-profile-display-stats (stats-response)
"Displays the STATS-RESPONSE on `cider-profile-buffer`."
(let ((table (nrepl-dict-get stats-response "err")))
(if cider-profile-buffer
(let ((buffer (cider-make-popup-buffer cider-profile-buffer)))
(with-current-buffer buffer
(let ((inhibit-read-only t)) (insert table)))
(display-buffer buffer)
(let ((window (get-buffer-window buffer)))
(set-window-point window 0)
(select-window window)
(fit-window-to-buffer window)))
(cider-emit-interactive-eval-err-output table))))
;;;###autoload
(defun cider-profile-summary ()
"Display a summary of currently collected profile data."
(interactive)
(cider-ensure-op-supported "profile-summary")
(cider-profile-display-stats
(cider-nrepl-send-sync-request '("op" "profile-summary"))))
;;;###autoload
(defun cider-profile-var-summary (query)
"Display profile data for var under point QUERY.
Defaults to the symbol at point. With prefix arg or no symbol at point,
prompts for a var."
(interactive "P")
(cider-ensure-op-supported "profile-var-summary")
(cider-read-symbol-name
"Profile-summary for var: "
(lambda (sym)
(cider-profile-display-stats
(cider-nrepl-send-sync-request
`("op" "profile-var-summary"
"ns" ,(cider-current-ns)
"sym" ,sym)))))
query)
;;;###autoload
(defun cider-profile-clear ()
"Clear any collected profile data."
(interactive)
(cider-ensure-op-supported "clear-profile")
(cider-nrepl-send-request
'("op" "clear-profile")
(cider-profile--make-response-handler
(lambda (_buffer value)
(when (equal value "cleared")
(message "Cleared profile data"))))))
(provide 'cider-profile)
;;; cider-profile.el ends here
;;; cider-repl-history.el --- REPL input history browser -*- lexical-binding: t; -*-
;; Copyright (c) 2017-2022 John Valente and browse-kill-ring authors
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;; Based heavily on browse-kill-ring
;; https://github.com/browse-kill-ring/browse-kill-ring
;;; Commentary:
;; REPL input history browser for CIDER.
;; Allows you to browse the full input history for your REPL buffer, and
;; insert previous commands at the prompt.
;;; Code:
(require 'cl-lib)
(require 'cider-popup)
(require 'clojure-mode)
(require 'derived)
(require 'pulse)
(require 'sesman)
(defconst cider-repl-history-buffer "*cider-repl-history*")
(defgroup cider-repl-history nil
"A package for browsing and inserting the items in the CIDER command history."
:prefix "cider-repl-history-"
:group 'cider)
(defvar cider-repl-history-display-styles
'((separated . cider-repl-history-insert-as-separated)
(one-line . cider-repl-history-insert-as-one-line)))
(defcustom cider-repl-history-display-style 'separated
"How to display the CIDER command history items.
If `one-line', then replace newlines with \"\\n\" for display.
If `separated', then display `cider-repl-history-separator' between
entries."
:type '(choice (const :tag "One line" one-line)
(const :tag "Separated" separated))
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-quit-action 'quit-window
"What action to take when `cider-repl-history-quit' is called.
If `bury-buffer', then simply bury the *cider-repl-history* buffer, but keep
the window.
If `bury-and-delete-window', then bury the buffer, and (if there is
more than one window) delete the window.
If `delete-and-restore', then restore the window configuration to what it was
before `cider-repl-history' was called, and kill the *cider-repl-history*
buffer.
If `quit-window', then restore the window configuration to what
it was before `cider-repl-history' was called, and bury *cider-repl-history*.
This is the default.
If `kill-and-delete-window', then kill the *cider-repl-history* buffer, and
delete the window on close.
Otherwise, it should be a function to call."
;; Note, if you use one of the non-"delete" options, after you "quit",
;; the *cider-repl-history* buffer is still available. If you are using
;; `cider-repl-history-show-preview', and you switch to *cider-repl-history* (i.e.,
;; with C-x b), it will not give the preview unless and until you "update"
;; the *cider-repl-history* buffer.
;;
;; This really should not be an issue, because there's no reason to "switch"
;; back to the buffer. If you want to get it back, you can just do C-c M-p
;; from the REPL buffer.
;; If you get in this situation and find it annoying, you can either disable
;; the preview, or set `cider-repl-history-quit-action' to 'delete-and-restore.
;; Then you will simply not have the *cider-repl-history* buffer after you quit,
;; and it won't be an issue.
:type '(choice (const :tag "Bury buffer"
:value bury-buffer)
(const :tag "Bury buffer and delete window"
:value bury-and-delete-window)
(const :tag "Delete window"
:value delete-and-restore)
(const :tag "Save and restore"
:value quit-window)
(const :tag "Kill buffer and delete window"
:value kill-and-delete-window)
function)
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-resize-window nil
"Whether to resize the `cider-repl-history' window to fit its contents.
Value is either t, meaning yes, or a cons pair of integers,
(MAXIMUM . MINIMUM) for the size of the window. MAXIMUM defaults to
the window size chosen by `pop-to-buffer'; MINIMUM defaults to
`window-min-height'."
:type '(choice (const :tag "No" nil)
(const :tag "Yes" t)
(cons (integer :tag "Maximum") (integer :tag "Minimum")))
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-separator ";;;;;;;;;;"
"The string separating entries in the `separated' style.
See `cider-repl-history-display-style'."
;; The (default) separator is a Clojure comment, to preserve fontification
;; in the buffer.
:type 'string
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-recenter nil
"If non-nil, then always keep the current entry at the top of the window."
:type 'boolean
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-highlight-current-entry nil
"If non-nil, highlight the currently selected command history entry."
:type 'boolean
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-highlight-inserted-item nil
"If non-nil, then temporarily highlight the inserted command history entry.
The value selected controls how the inserted item is highlighted,
possible values are `solid' (highlight the inserted text for a
fixed period of time), or `pulse' (fade out the highlighting gradually).
Setting this variable to the value t will select the default
highlighting style, which currently `pulse'.
The variable `cider-repl-history-inserted-item-face' contains the
face used for highlighting."
:type '(choice (const nil) (const t) (const solid) (const pulse))
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-separator-face 'bold
"The face in which to highlight the `cider-repl-history-separator'."
:type 'face
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-current-entry-face 'highlight
"The face in which to highlight the command history current entry."
:type 'face
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-inserted-item-face 'highlight
"The face in which to highlight the inserted item."
:type 'face
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-maximum-display-length nil
"Whether or not to limit the length of displayed items.
If this variable is an integer, the display of the command history will be
limited to that many characters.
Setting this variable to nil means no limit."
:type '(choice (const :tag "None" nil)
integer)
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-display-duplicates t
"If non-nil, then display duplicate items in the command history."
:type 'boolean
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-display-duplicate-highest t
"If non-nil, then display most recent duplicate items in the command history.
Only takes effect when `cider-repl-history-display-duplicates' is nil."
:type 'boolean
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-text-properties nil
"If non-nil, maintain text properties of the command history items."
:type 'boolean
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-hook nil
"A list of functions to call after `cider-repl-history'."
:type 'hook
:package-version '(cider . "0.15.0"))
(defcustom cider-repl-history-show-preview nil
"If non-nil, show a preview of the inserted text in the REPL buffer.
The REPL buffer would show a preview of what the buffer would look like
if the item under point were inserted."
:type 'boolean
:package-version '(cider . "0.15.0"))
(defvar cider-repl-history-repl-window nil
"The window in which chosen command history data will be inserted.
It is probably not a good idea to set this variable directly; simply
call `cider-repl-history' again.")
(defvar cider-repl-history-repl-buffer nil
"The buffer in which chosen command history data will be inserted.
It is probably not a good idea to set this variable directly; simply
call `cider-repl-history' again.")
(defvar cider-repl-history-preview-overlay nil
"Overlay used to preview what would happen if the user inserted the given text.")
(defvar cider-repl-history-previous-overlay nil
"Previous overlay within *cider-repl-history* buffer.")
(defun cider-repl-history-get-history ()
"Function to retrieve history from the REPL buffer."
(if cider-repl-history-repl-buffer
(buffer-local-value
'cider-repl-input-history
cider-repl-history-repl-buffer)
(error "Variable `cider-repl-history-repl-buffer' not bound to a buffer")))
(defun cider-repl-history-resize-window ()
"Resize the *cider-repl-history* window if needed.
Controlled by variable `cider-repl-history-resize-window'."
(when cider-repl-history-resize-window
(apply #'fit-window-to-buffer (selected-window)
(if (consp cider-repl-history-resize-window)
(list (car cider-repl-history-resize-window)
(or (cdr cider-repl-history-resize-window)
window-min-height))
(list nil window-min-height)))))
(defun cider-repl-history-read-regexp (msg use-default-p)
"Get a regular expression from the user.
Prompts with MSG; previous entry is default if USE-DEFAULT-P."
(let* ((default (car regexp-history))
(prompt (if (and default use-default-p)
(format "%s for regexp (default `%s'): "
msg
default)
(format "%s (regexp): " msg)))
(input
(read-from-minibuffer prompt nil nil nil 'regexp-history
(if use-default-p nil default))))
(if (equal input "")
(if use-default-p default nil)
input)))
(defun cider-repl-history-clear-preview ()
"Clear the preview, if one is present."
(interactive)
(when cider-repl-history-preview-overlay
(cl-assert (overlayp cider-repl-history-preview-overlay))
(delete-overlay cider-repl-history-preview-overlay)))
(defun cider-repl-history-cleanup-on-exit ()
"Function called when the user is finished with `cider-repl-history'.
This function performs any cleanup that is required when the user
has finished interacting with the *cider-repl-history* buffer. For now
the only cleanup performed is to remove the preview overlay, if
it's turned on."
(cider-repl-history-clear-preview))
(defun cider-repl-history-quit ()
"Take the action specified by `cider-repl-history-quit-action'."
(interactive)
(cider-repl-history-cleanup-on-exit)
(pcase cider-repl-history-quit-action
(`delete-and-restore
(quit-restore-window (selected-window) 'kill))
(`quit-window
(quit-window))
(`kill-and-delete-window
(kill-buffer (current-buffer))
(unless (= (count-windows) 1)
(delete-window)))
(`bury-and-delete-window
(bury-buffer)
(unless (= (count-windows) 1)
(delete-window)))
(_
(funcall cider-repl-history-quit-action))))
(defun cider-repl-history-preview-overlay-setup (orig-buf)
"Setup the preview overlay in ORIG-BUF."
(when cider-repl-history-show-preview
(with-current-buffer orig-buf
(let* ((will-replace (region-active-p))
(start (if will-replace
(min (point) (mark))
(point)))
(end (if will-replace
(max (point) (mark))
(point))))
(cider-repl-history-clear-preview)
(setq cider-repl-history-preview-overlay
(make-overlay start end orig-buf))
(overlay-put cider-repl-history-preview-overlay
'invisible t)))))
(defun cider-repl-history-highlight-inserted (start end)
"Insert the text between START and END."
(pcase cider-repl-history-highlight-inserted-item
((or `pulse `t)
(let ((pulse-delay .05) (pulse-iterations 10))
(with-no-warnings
(pulse-momentary-highlight-region
start end cider-repl-history-inserted-item-face))))
(`solid
(let ((o (make-overlay start end)))
(overlay-put o 'face cider-repl-history-inserted-item-face)
(sit-for 0.5)
(delete-overlay o)))))
(defun cider-repl-history-insert-and-highlight (str)
"Helper function to insert STR at point, highlighting it if appropriate."
(let ((before-insert (point)))
(let (deactivate-mark)
(insert-for-yank str))
(cider-repl-history-highlight-inserted
before-insert
(point))))
(defun cider-repl-history-target-overlay-at (_position &optional no-error)
"Return overlay at POSITION that has property `cider-repl-history-target'.
If no such overlay, raise an error unless NO-ERROR is true, in which
case return nil."
(let ((ovs (overlays-at (point))))
(catch 'cider-repl-history-target-overlay-at
(dolist (ov ovs)
(when (overlay-get ov 'cider-repl-history-target)
(throw 'cider-repl-history-target-overlay-at ov)))
(unless no-error
(error "No CIDER history item here")))))
(defun cider-repl-history-current-string (pt &optional no-error)
"Find the string to insert into the REPL by looking for the overlay at PT.
Might error unless NO-ERROR set."
(let ((o (cider-repl-history-target-overlay-at pt t)))
(if o
(overlay-get o 'cider-repl-history-target)
(unless no-error
(error "No CIDER history item in this buffer")))))
(defun cider-repl-history-do-insert (_buf pt)
"Helper function to insert text from BUF at PT into the REPL buffer.
Also kills *cider-repl-history*."
;; Note: as mentioned at the top, this file is based on browse-kill-ring,
;; which has numerous insertion options. The functionality of
;; browse-kill-ring allows users to insert at point, and move point to the end
;; of the inserted text; or insert at the beginning or end of the buffer,
;; while leaving point alone. And each of these had the option of leaving the
;; history buffer in place, or getting rid of it. That was appropriate for a
;; generic paste tool, but for inserting a previous command into an
;; interpreter, I felt the only useful option would be inserting it at the end
;; and quitting the history buffer, so that is all that's provided.
(let ((str (cider-repl-history-current-string pt)))
(cider-repl-history-quit)
(with-selected-window cider-repl-history-repl-window
(with-current-buffer cider-repl-history-repl-buffer
(let ((max (point-max)))
(if (= max (point))
(cider-repl-history-insert-and-highlight str)
(save-excursion
(goto-char max)
(cider-repl-history-insert-and-highlight str))))))))
(defun cider-repl-history-insert-and-quit ()
"Insert the item into the REPL buffer, and close *cider-repl-history*.
The text is always inserted at the very bottom of the REPL buffer. If your
cursor is already at the bottom, it is advanced to the end of the inserted
text. If your cursor is somewhere else, the cursor is not moved, but the
text is still inserted at the end."
(interactive)
(cider-repl-history-do-insert (current-buffer) (point)))
(defun cider-repl-history-mouse-insert (e)
"Insert the item at E into the REPL buffer, and close *cider-repl-history*.
The text is always inserted at the very bottom of the REPL buffer. If your
cursor is already at the bottom, it is advanced to the end of the inserted
text. If your cursor is somewhere else, the cursor is not moved, but the
text is still inserted at the end."
(interactive "e")
(let* ((data (save-excursion
(mouse-set-point e)
(cons (current-buffer) (point))))
(buf (car data))
(pt (cdr data)))
(cider-repl-history-do-insert buf pt)))
(defun cider-repl-history-clear-highlighted-entry ()
"Clear the highlighted entry, when one exists."
(when cider-repl-history-previous-overlay
(cl-assert (overlayp cider-repl-history-previous-overlay)
nil "not an overlay")
(overlay-put cider-repl-history-previous-overlay 'face nil)))
(defun cider-repl-history-update-highlighted-entry ()
"Update highlighted entry, when feature is turned on."
(when cider-repl-history-highlight-current-entry
(if-let* ((current-overlay (cider-repl-history-target-overlay-at (point) t)))
(unless (equal cider-repl-history-previous-overlay current-overlay)
;; We've changed overlay. Clear current highlighting,
;; and highlight the new overlay.
(cl-assert (overlay-get current-overlay 'cider-repl-history-target) t)
(cider-repl-history-clear-highlighted-entry)
(setq cider-repl-history-previous-overlay current-overlay)
(overlay-put current-overlay 'face
cider-repl-history-current-entry-face))
;; No overlay at point. Just clear all current highlighting.
(cider-repl-history-clear-highlighted-entry))))
(defun cider-repl-history-forward (&optional arg)
"Move forward by ARG command history entries."
(interactive "p")
(beginning-of-line)
(while (not (zerop arg))
(let ((o (cider-repl-history-target-overlay-at (point) t)))
(cond
((>= arg 0)
(setq arg (1- arg))
;; We're on a cider-repl-history overlay, skip to the end of it.
(when o
(goto-char (overlay-end o))
(setq o nil))
(while (not (or o (eobp)))
(goto-char (next-overlay-change (point)))
(setq o (cider-repl-history-target-overlay-at (point) t))))
(t
(setq arg (1+ arg))
(when o
(goto-char (overlay-start o))
(setq o nil))
(while (not (or o (bobp)))
(goto-char (previous-overlay-change (point)))
(setq o (cider-repl-history-target-overlay-at (point) t)))))))
(when cider-repl-history-recenter
(recenter 1)))
(defun cider-repl-history-previous (&optional arg)
"Move backward by ARG command history entries."
(interactive "p")
(cider-repl-history-forward (- arg)))
(defun cider-repl-history-search-forward (regexp &optional backwards)
"Move to the next command history entry matching REGEXP from point.
If optional arg BACKWARDS is non-nil, move to the previous matching
entry."
(interactive
(list (cider-repl-history-read-regexp "Search forward" t)
current-prefix-arg))
(let ((orig (point)))
(cider-repl-history-forward (if backwards -1 1))
(let ((over (cider-repl-history-target-overlay-at (point) t)))
(while (and over
(not (if backwards (bobp) (eobp)))
(not (string-match regexp
(overlay-get over
'cider-repl-history-target))))
(cider-repl-history-forward (if backwards -1 1))
(setq over (cider-repl-history-target-overlay-at (point) t)))
(unless (and over
(string-match regexp
(overlay-get over
'cider-repl-history-target)))
(goto-char orig)
(message "No more command history entries matching %s" regexp)))))
(defun cider-repl-history-search-backward (regexp)
"Move to the previous command history entry matching REGEXP from point."
(interactive
(list (cider-repl-history-read-regexp "Search backward" t)))
(cider-repl-history-search-forward regexp t))
(defun cider-repl-history-elide (str)
;; FIXME: Use `truncate-string-to-width'?
"If STR is too long, abbreviate it with an ellipsis.
Otherwise, return it unchanged."
(if (and cider-repl-history-maximum-display-length
(> (length str)
cider-repl-history-maximum-display-length))
(concat (substring str 0 (- cider-repl-history-maximum-display-length 3))
(propertize "..." 'cider-repl-history-extra t))
str))
(defmacro cider-repl-history-add-overlays-for (item &rest body)
"Add overlays for ITEM, and execute BODY."
(let ((beg (cl-gensym "cider-repl-history-add-overlays-"))
(end (cl-gensym "cider-repl-history-add-overlays-")))
`(let ((,beg (point))
(,end
(progn
,@body
(point))))
(let ((o (make-overlay ,beg ,end)))
(overlay-put o 'cider-repl-history-target ,item)
(overlay-put o 'mouse-face 'highlight)))))
(defun cider-repl-history-insert-as-separated (items)
"Insert ITEMS into the current buffer, with separators between items."
(while items
(let* ((origitem (car items))
(item (cider-repl-history-elide origitem))
) ;; (len (length item))
(cider-repl-history-add-overlays-for origitem (insert item))
;; When the command history has items with read-only text property at
;; **the end of** string, cider-repl-history-setup fails with error
;; `Text is read-only'. So inhibit-read-only here.
;; See http://bugs.debian.org/225082
(let ((inhibit-read-only t))
(insert "\n")
(when (cdr items)
(insert (propertize cider-repl-history-separator
'cider-repl-history-extra t
'cider-repl-history-separator t))
(insert "\n"))))
(setq items (cdr items))))
(defun cider-repl-history-insert-as-one-line (items)
"Insert ITEMS into the current buffer, formatting each item as a single line.
An explicit newline character will replace newlines so that the text retains its
spacing when it's actually inserted into the REPL buffer."
(dolist (item items)
(cider-repl-history-add-overlays-for
item
(let* ((item (cider-repl-history-elide item))
(len (length item))
(start 0)
(newl (propertize "\\n" 'cider-repl-history-extra t)))
(while (and (< start len)
(string-match "\n" item start))
(insert (substring item start (match-beginning 0))
newl)
(setq start (match-end 0)))
(insert (substring item start len))))
(insert "\n")))
(defun cider-repl-history-preview-update-text (preview-text)
"Update `cider-repl-history-preview-overlay' to show `PREVIEW-TEXT`."
;; If preview-text is nil, replacement should be nil too.
(cl-assert (overlayp cider-repl-history-preview-overlay))
(let ((replacement (when preview-text
(propertize preview-text 'face 'highlight))))
(overlay-put cider-repl-history-preview-overlay
'before-string replacement)))
(defun cider-repl-history-preview-update-by-position (&optional pt)
"Update `cider-repl-history-preview-overlay' to match item at PT.
This function is called whenever the selection in the *cider-repl-history*
buffer is adjusted, the `cider-repl-history-preview-overlay'
is updated to preview the text of the selection at PT (or the
current point if not specified)."
(let ((new-text (cider-repl-history-current-string
(or pt (point)) t)))
(cider-repl-history-preview-update-text new-text)))
(defun cider-repl-history-undo-other-window ()
"Undo the most recent change in the other window's buffer.
You most likely want to use this command for undoing an insertion of
text from the *cider-repl-history* buffer."
(interactive)
(with-current-buffer cider-repl-history-repl-buffer
(undo)))
(defun cider-repl-history-setup (repl-win repl-buf history-buf &optional regexp)
"Setup.
REPL-WIN and REPL-BUF are where to insert commands;
HISTORY-BUF is the history, and optional arg REGEXP is a filter."
(cider-repl-history-preview-overlay-setup repl-buf)
(with-current-buffer history-buf
(unwind-protect
(progn
(cider-repl-history-mode)
(setq buffer-read-only nil)
(when (eq 'one-line cider-repl-history-display-style)
(setq truncate-lines t))
(let ((inhibit-read-only t))
(erase-buffer))
(setq cider-repl-history-repl-buffer repl-buf)
(setq cider-repl-history-repl-window repl-win)
(let* ((cider-repl-history-maximum-display-length
(if (and cider-repl-history-maximum-display-length
(<= cider-repl-history-maximum-display-length 3))
4
cider-repl-history-maximum-display-length))
(cider-command-history (cider-repl-history-get-history))
(items (mapcar
(if cider-repl-history-text-properties
#'copy-sequence
#'substring-no-properties)
cider-command-history)))
(unless cider-repl-history-display-duplicates
;; display highest or lowest duplicate.
;; if `cider-repl-history-display-duplicate-highest' is t,
;; display highest (most recent) duplicate.
(cl-delete-duplicates
items
:test #'equal
:from-end cider-repl-history-display-duplicate-highest))
(when (stringp regexp)
(setq items (delq nil
(mapcar
#'(lambda (item)
(when (string-match regexp item)
item))
items))))
(funcall (or (cdr (assq cider-repl-history-display-style
cider-repl-history-display-styles))
(error "Invalid `cider-repl-history-display-style': %s"
cider-repl-history-display-style))
items)
(when cider-repl-history-show-preview
(cider-repl-history-preview-update-by-position (point-min))
;; Local post-command-hook, only happens in *cider-repl-history*
(add-hook 'post-command-hook
#'cider-repl-history-preview-update-by-position
nil t)
(add-hook 'kill-buffer-hook
#'cider-repl-history-cleanup-on-exit
nil t))
(when cider-repl-history-highlight-current-entry
(add-hook 'post-command-hook
#'cider-repl-history-update-highlighted-entry
nil t))
(message
(let ((entry (if (= 1 (length cider-command-history))
"entry"
"entries")))
(concat
(if (and (not regexp)
cider-repl-history-display-duplicates)
(format "%s %s in the command history."
(length cider-command-history) entry)
(format "%s (of %s) %s in the command history shown."
(length items) (length cider-command-history) entry))
(substitute-command-keys
(concat " Type \\[cider-repl-history-quit] to quit. "
"\\[describe-mode] for help.")))))
(set-buffer-modified-p nil)
(goto-char (point-min))
(cider-repl-history-forward 0)
(setq mode-name (if regexp
(concat "History [" regexp "]")
"History"))
(run-hooks 'cider-repl-history-hook)))
(setq buffer-read-only t))))
(defun cider-repl-history-update ()
"Update the history buffer to reflect the latest state of the command history."
(interactive)
(cl-assert (eq major-mode 'cider-repl-history-mode))
(cider-repl-history-setup cider-repl-history-repl-window
cider-repl-history-repl-buffer
(current-buffer))
(cider-repl-history-resize-window))
(defun cider-repl-history-occur (regexp)
"Display all command history entries matching REGEXP."
(interactive
(list (cider-repl-history-read-regexp
"Display command history entries matching" nil)))
(cl-assert (eq major-mode 'cider-repl-history-mode))
(cider-repl-history-setup cider-repl-history-repl-window
cider-repl-history-repl-buffer
(current-buffer)
regexp)
(cider-repl-history-resize-window))
(defvar cider-repl-history-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "n") #'cider-repl-history-forward)
(define-key map (kbd "p") #'cider-repl-history-previous)
(define-key map (kbd "SPC") #'cider-repl-history-insert-and-quit)
(define-key map (kbd "RET") #'cider-repl-history-insert-and-quit)
(define-key map [(mouse-2)] #'cider-repl-history-mouse-insert)
(define-key map (kbd "l") #'cider-repl-history-occur)
(define-key map (kbd "s") #'cider-repl-history-search-forward)
(define-key map (kbd "r") #'cider-repl-history-search-backward)
(define-key map (kbd "g") #'cider-repl-history-update)
(define-key map (kbd "q") #'cider-repl-history-quit)
(define-key map (kbd "U") #'cider-repl-history-undo-other-window)
(define-key map (kbd "?") #'describe-mode)
(define-key map (kbd "h") #'describe-mode)
map))
(put 'cider-repl-history-mode 'mode-class 'special)
(define-derived-mode cider-repl-history-mode clojure-mode "History"
"Major mode for browsing the entries in the command input history."
(setq-local sesman-system 'CIDER))
;;;###autoload
(defun cider-repl-history ()
"Display items in the CIDER command history in another buffer."
(interactive)
(when (eq major-mode 'cider-repl-history-mode)
(user-error "Already viewing the CIDER command history"))
(let* ((repl-win (selected-window))
(repl-buf (window-buffer repl-win))
(buf (get-buffer-create cider-repl-history-buffer)))
(cider-repl-history-setup repl-win repl-buf buf)
(pop-to-buffer buf)
(cider-repl-history-resize-window)))
(provide 'cider-repl-history)
;;; cider-repl-history.el ends here
;;; cider-repl.el --- CIDER REPL mode interactions -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; Reid McKenzie <me@arrdem.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; This functionality concerns `cider-repl-mode' and REPL interaction. For
;; REPL/connection life-cycle management see cider-connection.el.
;;; Code:
(require 'cl-lib)
(require 'easymenu)
(require 'image)
(require 'map)
(require 'seq)
(require 'subr-x)
(require 'clojure-mode)
(require 'sesman)
(require 'cider-client)
(require 'cider-doc)
(require 'cider-test)
(require 'cider-eldoc) ; for cider-eldoc-setup
(require 'cider-common)
(require 'cider-util)
(require 'cider-resolve)
(declare-function cider-inspect "cider-inspector")
(defgroup cider-repl nil
"Interaction with the REPL."
:prefix "cider-repl-"
:group 'cider)
(defface cider-repl-prompt-face
'((t (:inherit font-lock-keyword-face)))
"Face for the prompt in the REPL buffer.")
(defface cider-repl-stdout-face
'((t (:inherit font-lock-string-face)))
"Face for STDOUT output in the REPL buffer.")
(defface cider-repl-stderr-face
'((t (:inherit font-lock-warning-face)))
"Face for STDERR output in the REPL buffer."
:package-version '(cider . "0.6.0"))
(defface cider-repl-input-face
'((t (:bold t)))
"Face for previous input in the REPL buffer.")
(defface cider-repl-result-face
'((t ()))
"Face for the result of an evaluation in the REPL buffer.")
(defcustom cider-repl-pop-to-buffer-on-connect t
"Controls whether to pop to the REPL buffer on connect.
When set to nil the buffer will only be created, and not displayed. When
set to `display-only' the buffer will be displayed, but it will not become
focused. Otherwise the buffer is displayed and focused."
:type '(choice (const :tag "Create the buffer, but don't display it" nil)
(const :tag "Create and display the buffer, but don't focus it"
display-only)
(const :tag "Create, display, and focus the buffer" t)))
(defcustom cider-repl-display-in-current-window nil
"Controls whether the REPL buffer is displayed in the current window."
:type 'boolean)
(make-obsolete-variable 'cider-repl-scroll-on-output 'scroll-conservatively "0.21")
(defcustom cider-repl-use-pretty-printing t
"Control whether results in the REPL are pretty-printed or not.
The REPL will use the printer specified in `cider-print-fn'.
The `cider-toggle-pretty-printing' command can be used to interactively
change the setting's value."
:type 'boolean)
(make-obsolete-variable 'cider-repl-pretty-print-width 'cider-print-options "0.21")
(defcustom cider-repl-use-content-types nil
"Control whether REPL results are presented using content-type information.
The `cider-repl-toggle-content-types' command can be used to interactively
change the setting's value."
:type 'boolean
:package-version '(cider . "0.17.0"))
(defcustom cider-repl-auto-detect-type t
"Control whether to auto-detect the REPL type using track-state information.
If you disable this you'll have to manually change the REPL type between
Clojure and ClojureScript when invoking REPL type changing forms.
Use `cider-set-repl-type' to manually change the REPL type."
:type 'boolean
:safe #'booleanp
:package-version '(cider . "0.18.0"))
(defcustom cider-repl-use-clojure-font-lock t
"Non-nil means to use Clojure mode font-locking for input and result.
Nil means that `cider-repl-input-face' and `cider-repl-result-face'
will be used."
:type 'boolean
:package-version '(cider . "0.10.0"))
(defcustom cider-repl-require-ns-on-set nil
"Controls whether to require the ns before setting it in the REPL."
:type 'boolean
:package-version '(cider . "0.22.0"))
(defcustom cider-repl-result-prefix ""
"The prefix displayed in the REPL before a result value.
By default there's no prefix, but you can specify something
like \"=>\" if want results to stand out more."
:type 'string
:group 'cider
:package-version '(cider . "0.5.0"))
(defcustom cider-repl-tab-command 'cider-repl-indent-and-complete-symbol
"Select the command to be invoked by the TAB key.
The default option is `cider-repl-indent-and-complete-symbol'. If
you'd like to use the default Emacs behavior use
`indent-for-tab-command'."
:type 'symbol)
(make-obsolete-variable 'cider-repl-print-length 'cider-print-options "0.21")
(make-obsolete-variable 'cider-repl-print-level 'cider-print-options "0.21")
(defvar cider-repl-require-repl-utils-code
'((clj . "(clojure.core/apply clojure.core/require clojure.main/repl-requires)")
(cljs . "(require '[cljs.repl :refer [apropos dir doc find-doc print-doc pst source]])")))
(defcustom cider-repl-init-code (list (cdr (assoc 'clj cider-repl-require-repl-utils-code)))
"Clojure code to evaluate when starting a REPL.
Will be evaluated with bindings for set!-able vars in place."
:type '(list string)
:package-version '(cider . "0.21.0"))
(defcustom cider-repl-display-help-banner t
"When non-nil a bit of help text will be displayed on REPL start."
:type 'boolean
:package-version '(cider . "0.11.0"))
;;;; REPL buffer local variables
(defvar-local cider-repl-input-start-mark nil)
(defvar-local cider-repl-prompt-start-mark nil)
(defvar-local cider-repl-old-input-counter 0
"Counter used to generate unique `cider-old-input' properties.
This property value must be unique to avoid having adjacent inputs be
joined together.")
(defvar-local cider-repl-input-history '()
"History list of strings read from the REPL buffer.")
(defvar-local cider-repl-input-history-items-added 0
"Variable counting the items added in the current session.")
(defvar-local cider-repl-output-start nil
"Marker for the start of output.
Currently its only purpose is to facilitate `cider-repl-clear-buffer'.")
(defvar-local cider-repl-output-end nil
"Marker for the end of output.
Currently its only purpose is to facilitate `cider-repl-clear-buffer'.")
(defun cider-repl-tab ()
"Invoked on TAB keystrokes in `cider-repl-mode' buffers."
(interactive)
(funcall cider-repl-tab-command))
(defun cider-repl-reset-markers ()
"Reset all REPL markers."
(dolist (markname '(cider-repl-output-start
cider-repl-output-end
cider-repl-prompt-start-mark
cider-repl-input-start-mark))
(set markname (make-marker))
(set-marker (symbol-value markname) (point))))
;;; REPL init
(defvar-local cider-repl-ns-cache nil
"A dict holding information about all currently loaded namespaces.
This cache is stored in the connection buffer.")
(defvar cider-mode)
(declare-function cider-refresh-dynamic-font-lock "cider-mode")
(defun cider-repl--state-handler (response)
"Handle server state contained in RESPONSE."
(with-demoted-errors "Error in `cider-repl--state-handler': %s"
(when (member "state" (nrepl-dict-get response "status"))
(nrepl-dbind-response response (repl-type changed-namespaces)
(when (and repl-type cider-repl-auto-detect-type)
(cider-set-repl-type repl-type))
(unless (nrepl-dict-empty-p changed-namespaces)
(setq cider-repl-ns-cache (nrepl-dict-merge cider-repl-ns-cache changed-namespaces))
(dolist (b (buffer-list))
(with-current-buffer b
;; Metadata changed, so signatures may have changed too.
(setq cider-eldoc-last-symbol nil)
(when (or cider-mode (derived-mode-p 'cider-repl-mode))
(when-let* ((ns-dict (or (nrepl-dict-get changed-namespaces (cider-current-ns))
(let ((ns-dict (cider-resolve--get-in (cider-current-ns))))
(when (seq-find (lambda (ns) (nrepl-dict-get changed-namespaces ns))
(nrepl-dict-get ns-dict "aliases"))
ns-dict)))))
(cider-refresh-dynamic-font-lock ns-dict))))))))))
(defun cider-repl-require-repl-utils ()
"Require standard REPL util functions into the current REPL."
(interactive)
(let* ((current-repl (cider-current-repl nil 'ensure))
(require-code (cdr (assoc (cider-repl-type current-repl) cider-repl-require-repl-utils-code))))
(nrepl-send-sync-request
(lax-plist-put
(nrepl--eval-request require-code (cider-current-ns))
"inhibit-cider-middleware" "true")
current-repl)))
(defun cider-repl-init-eval-handler (&optional callback)
"Make an nREPL evaluation handler for use during REPL init.
Run CALLBACK once the evaluation is complete."
(nrepl-make-response-handler (current-buffer)
(lambda (_buffer _value))
(lambda (buffer out)
(cider-repl-emit-stdout buffer out))
(lambda (buffer err)
(cider-repl-emit-stderr buffer err))
(lambda (buffer)
(cider-repl-emit-prompt buffer)
(when callback
(funcall callback)))))
(defun cider-repl-eval-init-code (&optional callback)
"Evaluate `cider-repl-init-code' in the current REPL.
Run CALLBACK once the evaluation is complete."
(interactive)
(let* ((request (map-merge 'hash-table
(cider--repl-request-map fill-column)
'(("inhibit-cider-middleware" "true")))))
(cider-nrepl-request:eval
;; Ensure we evaluate _something_ so the initial namespace is correctly set
(thread-first (or cider-repl-init-code '("nil"))
(string-join "\n"))
(cider-repl-init-eval-handler callback)
nil
(line-number-at-pos (point))
(cider-column-number-at-pos (point))
(thread-last
request
(map-pairs)
(seq-mapcat #'identity)))))
(defun cider-repl-init (buffer &optional callback)
"Initialize the REPL in BUFFER.
BUFFER must be a REPL buffer with `cider-repl-mode' and a running
client process connection. CALLBACK will be run once the REPL is
fully initialized."
(when cider-repl-display-in-current-window
(add-to-list 'same-window-buffer-names (buffer-name buffer)))
(pcase cider-repl-pop-to-buffer-on-connect
(`display-only
(let ((orig-buffer (current-buffer)))
(display-buffer buffer)
;; User popup-rules (specifically `:select nil') can cause the call to
;; `display-buffer' to reset the current Emacs buffer to the clj/cljs
;; buffer that the user ran `jack-in' from - we need the current-buffer
;; to be the repl to initialize, so reset it back here to be resilient
;; against user config
(set-buffer orig-buffer)))
((pred identity) (pop-to-buffer buffer)))
(with-current-buffer buffer
(cider-repl--insert-banner)
(cider-repl--insert-startup-commands)
(when-let* ((window (get-buffer-window buffer t)))
(with-selected-window window
(recenter (- -1 scroll-margin))))
(cider-repl-eval-init-code callback))
buffer)
(defun cider-repl--insert-banner ()
"Insert the banner in the current REPL buffer."
(insert-before-markers
(propertize (cider-repl--banner) 'font-lock-face 'font-lock-comment-face))
(when cider-repl-display-help-banner
(insert-before-markers
(propertize (cider-repl--help-banner) 'font-lock-face 'font-lock-comment-face))))
(defun cider-repl--insert-startup-commands ()
"Insert the values from params specified in PARAM-TUPLES.
PARAM-TUPLES are tuples of (param-key description) or (param-key
description transform) where transform is called with the param-value if
present."
(cl-labels
((emit-comment
(contents)
(insert-before-markers
(propertize
(if (string-blank-p contents) ";;\n" (concat ";; " contents "\n"))
'font-lock-face 'font-lock-comment-face))))
(let ((jack-in-command (plist-get cider-launch-params :jack-in-cmd))
(cljs-repl-type (plist-get cider-launch-params :cljs-repl-type))
(cljs-init-form (plist-get cider-launch-params :repl-init-form)))
(when jack-in-command
;; spaces to align with the banner
(emit-comment (concat " Startup: " jack-in-command)))
(when (or cljs-repl-type cljs-init-form)
(emit-comment "")
(when cljs-repl-type
(emit-comment (concat "ClojureScript REPL type: " (symbol-name cljs-repl-type))))
(when cljs-init-form
(emit-comment (concat "ClojureScript REPL init form: " cljs-init-form)))
(emit-comment "")))))
(defun cider-repl--banner ()
"Generate the welcome REPL buffer banner."
(cond
((cider--clojure-version) (cider-repl--clojure-banner))
((cider--babashka-version) (cider-repl--babashka-banner))
(t (cider-repl--basic-banner))))
(defun cider-repl--clojure-banner ()
"Generate the welcome REPL buffer banner for Clojure(Script)."
(format ";; Connected to nREPL server - nrepl://%s:%s
;; CIDER %s, nREPL %s
;; Clojure %s, Java %s
;; Docs: (doc function-name)
;; (find-doc part-of-name)
;; Source: (source function-name)
;; Javadoc: (javadoc java-object-or-class)
;; Exit: <C-c C-q>
;; Results: Stored in vars *1, *2, *3, an exception in *e;
"
(plist-get nrepl-endpoint :host)
(plist-get nrepl-endpoint :port)
(cider--version)
(cider--nrepl-version)
(cider--clojure-version)
(cider--java-version)))
(defun cider-repl--babashka-banner ()
"Generate the welcome REPL buffer banner for Babashka."
(format ";; Connected to nREPL server - nrepl://%s:%s
;; CIDER %s, babashka.nrepl %s
;; Babashka %s
;; Docs: (doc function-name)
;; (find-doc part-of-name)
;; Source: (source function-name)
;; Javadoc: (javadoc java-object-or-class)
;; Exit: <C-c C-q>
;; Results: Stored in vars *1, *2, *3, an exception in *e;
"
(plist-get nrepl-endpoint :host)
(plist-get nrepl-endpoint :port)
(cider--version)
(cider--babashka-nrepl-version)
(cider--babashka-version)))
(defun cider-repl--basic-banner ()
"Generate a basic banner with minimal info."
(format ";; Connected to nREPL server - nrepl://%s:%s
;; CIDER %s
"
(plist-get nrepl-endpoint :host)
(plist-get nrepl-endpoint :port)
(cider--version)))
(defun cider-repl--help-banner ()
"Generate the help banner."
(substitute-command-keys
";; ======================================================================
;; If you're new to CIDER it is highly recommended to go through its
;; user manual first. Type <M-x cider-view-manual> to view it.
;; In case you're seeing any warnings you should consult the manual's
;; \"Troubleshooting\" section.
;;
;; Here are a few tips to get you started:
;;
;; * Press <\\[describe-mode]> to see a list of the keybindings available (this
;; will work in every Emacs buffer)
;; * Press <\\[cider-repl-handle-shortcut]> to quickly invoke some REPL command
;; * Press <\\[cider-switch-to-last-clojure-buffer]> to switch between the REPL and a Clojure file
;; * Press <\\[cider-find-var]> to jump to the source of something (e.g. a var, a
;; Java method)
;; * Press <\\[cider-doc]> to view the documentation for something (e.g.
;; a var, a Java method)
;; * Print CIDER's refcard and keep it close to your keyboard.
;;
;; CIDER is super customizable - try <M-x customize-group cider> to
;; get a feel for this. If you're thirsty for knowledge you should try
;; <M-x cider-drink-a-sip>.
;;
;; If you think you've encountered a bug (or have some suggestions for
;; improvements) use <M-x cider-report-bug> to report it.
;;
;; Above all else - don't panic! In case of an emergency - procure
;; some (hard) cider and enjoy it responsibly!
;;
;; You can remove this message with the <M-x cider-repl-clear-help-banner> command.
;; You can disable it from appearing on start by setting
;; `cider-repl-display-help-banner' to nil.
;; ======================================================================
"))
;;; REPL interaction
(defun cider-repl--in-input-area-p ()
"Return t if in input area."
(<= cider-repl-input-start-mark (point)))
(defun cider-repl--current-input (&optional until-point-p)
"Return the current input as string.
The input is the region from after the last prompt to the end of
buffer. If UNTIL-POINT-P is non-nil, the input is until the current
point."
(buffer-substring-no-properties cider-repl-input-start-mark
(if until-point-p
(point)
(point-max))))
(defun cider-repl-previous-prompt ()
"Move backward to the previous prompt."
(interactive)
(cider-repl--find-prompt t))
(defun cider-repl-next-prompt ()
"Move forward to the next prompt."
(interactive)
(cider-repl--find-prompt))
(defun cider-repl--find-prompt (&optional backward)
"Find the next prompt.
If BACKWARD is non-nil look backward."
(let ((origin (point))
(cider-repl-prompt-property 'field))
(while (progn
(cider-search-property-change cider-repl-prompt-property backward)
(not (or (cider-end-of-proprange-p cider-repl-prompt-property) (bobp) (eobp)))))
(unless (cider-end-of-proprange-p cider-repl-prompt-property)
(goto-char origin))))
(defun cider-search-property-change (prop &optional backward)
"Search forward for a property change to PROP.
If BACKWARD is non-nil search backward."
(cond (backward
(goto-char (previous-single-char-property-change (point) prop)))
(t
(goto-char (next-single-char-property-change (point) prop)))))
(defun cider-end-of-proprange-p (property)
"Return t if at the the end of a property range for PROPERTY."
(and (get-char-property (max (point-min) (1- (point))) property)
(not (get-char-property (point) property))))
(defun cider-repl--mark-input-start ()
"Mark the input start."
(set-marker cider-repl-input-start-mark (point) (current-buffer)))
(defun cider-repl--mark-output-start ()
"Mark the output start."
(set-marker cider-repl-output-start (point))
(set-marker cider-repl-output-end (point)))
(defun cider-repl-mode-beginning-of-defun (&optional arg)
"Move to the beginning of defun.
If given a negative value of ARG, move to the end of defun."
(if (and arg (< arg 0))
(cider-repl-mode-end-of-defun (- arg))
(dotimes (_ (or arg 1))
(cider-repl-previous-prompt))))
(defun cider-repl-mode-end-of-defun (&optional arg)
"Move to the end of defun.
If given a negative value of ARG, move to the beginning of defun."
(if (and arg (< arg 0))
(cider-repl-mode-beginning-of-defun (- arg))
(dotimes (_ (or arg 1))
(cider-repl-next-prompt))))
(defun cider-repl-beginning-of-defun ()
"Move to beginning of defun."
(interactive)
;; We call `beginning-of-defun' if we're at the start of a prompt
;; already, to trigger `cider-repl-mode-beginning-of-defun' by means
;; of the locally bound `beginning-of-defun-function', in order to
;; jump to the start of the previous prompt.
(if (and (not (cider-repl--at-prompt-start-p))
(cider-repl--in-input-area-p))
(goto-char cider-repl-input-start-mark)
(beginning-of-defun)))
(defun cider-repl-end-of-defun ()
"Move to end of defun."
(interactive)
;; C.f. `cider-repl-beginning-of-defun'
(if (and (not (= (point) (point-max)))
(cider-repl--in-input-area-p))
(goto-char (point-max))
(end-of-defun)))
(defun cider-repl-bol-mark ()
"Set the mark and go to the beginning of line or the prompt."
(interactive)
(unless mark-active
(set-mark (point)))
(move-beginning-of-line 1))
(defun cider-repl--at-prompt-start-p ()
"Return t if point is at the start of prompt.
This will not work on non-current prompts."
(= (point) cider-repl-input-start-mark))
(defmacro cider-save-marker (marker &rest body)
"Save MARKER and execute BODY."
(declare (debug t))
(let ((pos (make-symbol "pos")))
`(let ((,pos (marker-position ,marker)))
(prog1 (progn . ,body)
(set-marker ,marker ,pos)))))
(put 'cider-save-marker 'lisp-indent-function 1)
(defun cider-repl-prompt-default (namespace)
"Return a prompt string that mentions NAMESPACE."
(format "%s> " namespace))
(defun cider-repl-prompt-abbreviated (namespace)
"Return a prompt string that abbreviates NAMESPACE."
(format "%s> " (cider-abbreviate-ns namespace)))
(defun cider-repl-prompt-lastname (namespace)
"Return a prompt string with the last name in NAMESPACE."
(format "%s> " (cider-last-ns-segment namespace)))
(defcustom cider-repl-prompt-function #'cider-repl-prompt-default
"A function that returns a prompt string.
Takes one argument, a namespace name.
For convenience, three functions are already provided for this purpose:
`cider-repl-prompt-lastname', `cider-repl-prompt-abbreviated', and
`cider-repl-prompt-default'."
:type '(choice (const :tag "Full namespace" cider-repl-prompt-default)
(const :tag "Abbreviated namespace" cider-repl-prompt-abbreviated)
(const :tag "Last name in namespace" cider-repl-prompt-lastname)
(function :tag "Custom function"))
:package-version '(cider . "0.9.0"))
(defun cider-repl--insert-prompt (namespace)
"Insert the prompt (before markers!), taking into account NAMESPACE.
Set point after the prompt.
Return the position of the prompt beginning."
(goto-char cider-repl-input-start-mark)
(cider-save-marker cider-repl-output-start
(cider-save-marker cider-repl-output-end
(unless (bolp) (insert-before-markers "\n"))
(let ((prompt-start (point))
(prompt (funcall cider-repl-prompt-function namespace)))
(cider-propertize-region
'(font-lock-face cider-repl-prompt-face read-only t intangible t
field cider-repl-prompt
rear-nonsticky (field read-only font-lock-face intangible))
(insert-before-markers prompt))
(set-marker cider-repl-prompt-start-mark prompt-start)
prompt-start))))
(defun cider-repl--ansi-color-apply (string)
"Like `ansi-color-apply', but does not withhold non-SGR seqs found in STRING.
Workaround for Emacs bug#53808 whereby partial ANSI control seqs present in
the input stream may block the whole colorization process."
(let* ((result (ansi-color-apply string))
;; The STRING may end with a possible incomplete ANSI control seq which
;; the call to `ansi-color-apply' stores in the `ansi-color-context'
;; fragment. If the fragment is not an incomplete ANSI color control
;; sequence (aka SGR seq) though then flush it out and appended it to
;; the result.
(fragment-flush?
(when-let (fragment (and ansi-color-context (cadr ansi-color-context)))
(save-match-data
;; Check if fragment is indeed an SGR seq in the making. The SGR
;; seq is defined as starting with ESC followed by [ followed by
;; zero or more [:digit:]+; followed by one or more digits and
;; ending with m.
(when (string-match
(rx (sequence ?\e
(? (and (or ?\[ eol)
(or (+ (any (?0 . ?9))) eol)
(* (sequence ?\; (+ (any (?0 . ?9)))))
(or ?\; eol)))))
fragment)
(let* ((sgr-end-pos (match-end 0))
(fragment-matches-whole? (or (= sgr-end-pos 0)
(= sgr-end-pos (length fragment)))))
(when (not fragment-matches-whole?)
;; Definitely not an partial SGR seq, flush it out of
;; `ansi-color-context'.
t)))))))
(if (not fragment-flush?)
result
(progn
;; Temporarily replace the ESC char in the fragment so that is flushed
;; out of `ansi-color-context' by `ansi-color-apply' and append it to
;; the result.
(aset (cadr ansi-color-context) 0 ?\0)
(let ((result-fragment (ansi-color-apply "")))
(aset result-fragment 0 ?\e)
(concat result result-fragment))))))
(defvar-local cider-repl--ns-forms-plist nil
"Plist holding ns->ns-form mappings within each connection.")
(defun cider-repl--ns-form-changed-p (ns-form connection)
"Return non-nil if NS-FORM for CONNECTION changed since last eval."
(when-let* ((ns (cider-ns-from-form ns-form)))
(not (string= ns-form
(lax-plist-get
(buffer-local-value 'cider-repl--ns-forms-plist connection)
ns)))))
(defvar cider-repl--root-ns-highlight-template "\\_<\\(%s\\)[^$/: \t\n()]+"
"Regexp used to highlight root ns in REPL buffers.")
(defvar-local cider-repl--root-ns-regexp nil
"Cache of root ns regexp in REPLs.")
(defvar-local cider-repl--ns-roots nil
"List holding all past root namespaces seen during interactive eval.")
(defun cider-repl--cache-ns-form (ns-form connection)
"Given NS-FORM cache root ns in CONNECTION."
(with-current-buffer connection
(when-let* ((ns (cider-ns-from-form ns-form)))
;; cache ns-form
(setq cider-repl--ns-forms-plist
(lax-plist-put cider-repl--ns-forms-plist ns ns-form))
;; cache ns roots regexp
(when (string-match "\\([^.]+\\)" ns)
(let ((root (match-string-no-properties 1 ns)))
(unless (member root cider-repl--ns-roots)
(push root cider-repl--ns-roots)
(let ((roots (mapconcat
;; Replace _ or - with regexp pattern to accommodate "raw" namespaces
(lambda (r) (replace-regexp-in-string "[_-]+" "[_-]+" r))
cider-repl--ns-roots "\\|")))
(setq cider-repl--root-ns-regexp
(format cider-repl--root-ns-highlight-template roots)))))))))
(defvar cider-repl-spec-keywords-regexp
(concat
(regexp-opt '("In:" " val:"
" at:" "fails at:"
" spec:" "fails spec:"
" predicate:" "fails predicate:"))
"\\|^"
(regexp-opt '(":clojure.spec.alpha/spec"
":clojure.spec.alpha/value")
"\\("))
"Regexp matching clojure.spec `explain` keywords.")
(defun cider-repl-highlight-spec-keywords (string)
"Highlight clojure.spec `explain` keywords in STRING.
Foreground of `clojure-keyword-face' is used for highlight."
(cider-add-face cider-repl-spec-keywords-regexp
'clojure-keyword-face t nil string)
string)
(defun cider-repl-highlight-current-project (string)
"Fontify project's root namespace to make stacktraces more readable.
Foreground of `cider-stacktrace-ns-face' is used to propertize matched
namespaces. STRING is REPL's output."
(cider-add-face cider-repl--root-ns-regexp 'cider-stacktrace-ns-face
t nil string)
string)
(defun cider-repl-add-locref-help-echo (string)
"Set help-echo property of STRING to `cider-locref-help-echo'."
(put-text-property 0 (length string) 'help-echo 'cider-locref-help-echo string)
string)
(defvar cider-repl-preoutput-hook `(,(if (< emacs-major-version 29)
'cider-repl--ansi-color-apply
'ansi-color-apply)
cider-repl-highlight-current-project
cider-repl-highlight-spec-keywords
cider-repl-add-locref-help-echo)
"Hook run on output string before it is inserted into the REPL buffer.
Each functions takes a string and must return a modified string. Also see
`cider-run-chained-hook'.")
(defcustom cider-repl-buffer-size-limit nil
"The max size of the REPL buffer.
Setting this to nil removes the limit."
:group 'cider
:type 'integer
:package-version '(cider . "0.26.0"))
(defun cider-start-of-next-prompt (point)
"Return the position of the first char of the next prompt from POINT."
(let ((next-prompt-or-input (next-single-char-property-change point 'field)))
(if (eq (get-char-property next-prompt-or-input 'field) 'cider-repl-prompt)
next-prompt-or-input
(next-single-char-property-change next-prompt-or-input 'field))))
(defun cider-repl-trim-top-of-buffer (buffer)
"Trims REPL output from beginning of BUFFER.
Trims by one fifth of `cider-repl-buffer-size-limit'.
Also clears remaining partial input or results."
(with-current-buffer buffer
(let* ((to-trim (ceiling (* cider-repl-buffer-size-limit 0.2)))
(start-of-next-prompt (cider-start-of-next-prompt to-trim))
(inhibit-read-only t))
(cider-repl--clear-region (point-min) start-of-next-prompt))))
(defun cider-repl-trim-buffer ()
"Trim the currently visited REPL buffer partially from the top.
See also `cider-repl-clear-buffer'."
(interactive)
(if cider-repl-buffer-size-limit
(cider-repl-trim-top-of-buffer (current-buffer))
(user-error "The variable `cider-repl-buffer-size-limit' is not set")))
(defun cider-repl-maybe-trim-buffer (buffer)
"Clear portion of printed output in BUFFER.
Clear the part where `cider-repl-buffer-size-limit' is exceeded."
(when (> (buffer-size) cider-repl-buffer-size-limit)
(cider-repl-trim-top-of-buffer buffer)))
(defun cider-repl--emit-output (buffer string face)
"Using BUFFER, emit STRING as output font-locked using FACE.
Before inserting, run `cider-repl-preoutput-hook' on STRING."
(with-current-buffer buffer
(save-excursion
(cider-save-marker cider-repl-output-start
(goto-char cider-repl-output-end)
(setq string (propertize string
'font-lock-face face
'rear-nonsticky '(font-lock-face)))
(setq string (cider-run-chained-hook 'cider-repl-preoutput-hook string))
(insert-before-markers string))
(when (and (= (point) cider-repl-prompt-start-mark)
(not (bolp)))
(insert-before-markers "\n")
(set-marker cider-repl-output-end (1- (point))))))
(when-let* ((window (get-buffer-window buffer t)))
;; If the prompt is on the first line of the window, then scroll the window
;; down by a single line to make the emitted output visible.
(when (and (pos-visible-in-window-p cider-repl-prompt-start-mark window)
(< 1 cider-repl-prompt-start-mark)
(not (pos-visible-in-window-p (1- cider-repl-prompt-start-mark) window)))
(with-selected-window window
(scroll-down 1)))))
(defun cider-repl--emit-interactive-output (string face)
"Emit STRING as interactive output using FACE."
(cider-repl--emit-output (cider-current-repl) string face))
(defun cider-repl-emit-interactive-stdout (string)
"Emit STRING as interactive output."
(cider-repl--emit-interactive-output string 'cider-repl-stdout-face))
(defun cider-repl-emit-interactive-stderr (string)
"Emit STRING as interactive err output."
(cider-repl--emit-interactive-output string 'cider-repl-stderr-face))
(defun cider-repl-emit-stdout (buffer string)
"Using BUFFER, emit STRING as standard output."
(cider-repl--emit-output buffer string 'cider-repl-stdout-face))
(defun cider-repl-emit-stderr (buffer string)
"Using BUFFER, emit STRING as error output."
(cider-repl--emit-output buffer string 'cider-repl-stderr-face))
(defun cider-repl-emit-prompt (buffer)
"Emit the REPL prompt into BUFFER."
(with-current-buffer buffer
(save-excursion
(cider-repl--insert-prompt cider-buffer-ns))))
(defun cider-repl-emit-result (buffer string show-prefix &optional bol)
"Emit into BUFFER the result STRING and mark it as an evaluation result.
If SHOW-PREFIX is non-nil insert `cider-repl-result-prefix' at the beginning
of the line. If BOL is non-nil insert at the beginning of the line."
(with-current-buffer buffer
(save-excursion
(cider-save-marker cider-repl-output-start
(goto-char cider-repl-output-end)
(when (and bol (not (bolp)))
(insert-before-markers "\n"))
(when show-prefix
(insert-before-markers (propertize cider-repl-result-prefix 'font-lock-face 'font-lock-comment-face)))
(if cider-repl-use-clojure-font-lock
(insert-before-markers (cider-font-lock-as-clojure string))
(cider-propertize-region
'(font-lock-face cider-repl-result-face rear-nonsticky (font-lock-face))
(insert-before-markers string)))))))
(defun cider-repl-newline-and-indent ()
"Insert a newline, then indent the next line.
Restrict the buffer from the prompt for indentation, to avoid being
confused by strange characters (like unmatched quotes) appearing
earlier in the buffer."
(interactive)
(save-restriction
(narrow-to-region cider-repl-prompt-start-mark (point-max))
(insert "\n")
(lisp-indent-line)))
(defun cider-repl-indent-and-complete-symbol ()
"Indent the current line and perform symbol completion.
First indent the line. If indenting doesn't move point, complete
the symbol."
(interactive)
(let ((pos (point)))
(lisp-indent-line)
(when (= pos (point))
(if (save-excursion (re-search-backward "[^() \n\t\r]+\\=" nil t))
(completion-at-point)))))
(defun cider-repl-kill-input ()
"Kill all text from the prompt to point."
(interactive)
(cond ((< (marker-position cider-repl-input-start-mark) (point))
(kill-region cider-repl-input-start-mark (point)))
((= (point) (marker-position cider-repl-input-start-mark))
(cider-repl-delete-current-input))))
(defun cider-repl--input-complete-p (start end)
"Return t if the region from START to END is a complete sexp."
(save-excursion
(goto-char start)
(cond ((looking-at-p "\\s *[@'`#]?[(\"]")
(ignore-errors
(save-restriction
(narrow-to-region start end)
;; Keep stepping over blanks and sexps until the end of
;; buffer is reached or an error occurs. Tolerate extra
;; close parens.
(cl-loop do (skip-chars-forward " \t\r\n)")
until (eobp)
do (forward-sexp))
t)))
(t t))))
(defun cider-repl--display-image (buffer image &optional show-prefix bol)
"Insert IMAGE into BUFFER at the current point.
For compatibility with the rest of CIDER's REPL machinery, supports
SHOW-PREFIX and BOL."
(with-current-buffer buffer
(save-excursion
(cider-save-marker cider-repl-output-start
(goto-char cider-repl-output-end)
(when (and bol (not (bolp)))
(insert-before-markers "\n"))
(when show-prefix
(insert-before-markers
(propertize cider-repl-result-prefix 'font-lock-face 'font-lock-comment-face)))
;; The below is inlined from `insert-image' and changed to use
;; `insert-before-markers' rather than `insert'
(let ((start (point))
(props (nconc `(display ,image rear-nonsticky (display))
(when (boundp 'image-map)
`(keymap ,image-map)))))
(insert-before-markers " ")
(add-text-properties start (point) props)))))
t)
(defcustom cider-repl-image-margin 10
"Specifies the margin to be applied to images displayed in the REPL.
Either a single number of pixels - interpreted as a symmetric margin, or
pair of numbers `(x . y)' encoding an arbitrary margin."
:type '(choice integer (vector integer integer))
:package-version '(cider . "0.17.0"))
(defun cider-repl--image (data type datap)
"A helper for creating images with CIDER's image options.
DATA is either the path to an image or its base64 coded data. TYPE is a
symbol indicating the image type. DATAP indicates whether the image is the
raw image data or a filename. Returns an image instance with a margin per
`cider-repl-image-margin'."
(create-image data type datap
:margin cider-repl-image-margin))
(defun cider-repl-handle-jpeg (_type buffer image &optional show-prefix bol)
"A handler for inserting a jpeg IMAGE into a repl BUFFER.
Part of the default `cider-repl-content-type-handler-alist'."
(cider-repl--display-image buffer
(cider-repl--image image 'jpeg t)
show-prefix bol))
(defun cider-repl-handle-png (_type buffer image &optional show-prefix bol)
"A handler for inserting a png IMAGE into a repl BUFFER.
Part of the default `cider-repl-content-type-handler-alist'."
(cider-repl--display-image buffer
(cider-repl--image image 'png t)
show-prefix bol))
(defun cider-repl-handle-svg (_type buffer image &optional show-prefix bol)
"A handler for inserting an svg IMAGE into a repl BUFFER.
Part of the default `cider-repl-content-type-handler-alist'."
(cider-repl--display-image buffer
(cider-repl--image image 'svg t)
show-prefix bol))
(defun cider-repl-handle-external-body (type buffer _ &optional _show-prefix _bol)
"Handler for slurping external content into BUFFER.
Handles an external-body TYPE by issuing a slurp request to fetch the content."
(if-let* ((args (cadr type))
(access-type (nrepl-dict-get args "access-type")))
(nrepl-send-request
(list "op" "slurp" "url" (nrepl-dict-get args access-type))
(cider-repl-handler buffer)
(cider-current-repl)))
nil)
(defvar cider-repl-content-type-handler-alist
`(("message/external-body" . ,#'cider-repl-handle-external-body)
("image/jpeg" . ,#'cider-repl-handle-jpeg)
("image/png" . ,#'cider-repl-handle-png)
("image/svg+xml" . ,#'cider-repl-handle-svg))
"Association list from content-types to handlers.
Handlers must be functions of two required and two optional arguments - the
REPL buffer to insert into, the value of the given content type as a raw
string, the REPL's show prefix as any and an `end-of-line' flag.
The return value of the handler should be a flag, indicating whether or not
the REPL is ready for a prompt to be displayed. Most handlers should return
t, as the content-type response is (currently) an alternative to the
value response. However for handlers which themselves issue subsequent
nREPL ops, it may be convenient to prevent inserting a prompt.")
(defun cider-repl-handler (buffer)
"Make an nREPL evaluation handler for the REPL BUFFER."
(let ((show-prompt t))
(nrepl-make-response-handler
buffer
(lambda (buffer value)
(cider-repl-emit-result buffer value t))
(lambda (buffer out)
(cider-repl-emit-stdout buffer out))
(lambda (buffer err)
(cider-repl-emit-stderr buffer err))
(lambda (buffer)
(when show-prompt
(cider-repl-emit-prompt buffer))
(when cider-repl-buffer-size-limit
(cider-repl-maybe-trim-buffer buffer)))
nrepl-err-handler
(lambda (buffer value content-type)
(if-let* ((content-attrs (cadr content-type))
(content-type* (car content-type))
(handler (cdr (assoc content-type*
cider-repl-content-type-handler-alist))))
(setq show-prompt (funcall handler content-type buffer value nil t))
(cider-repl-emit-result buffer value t t)))
(lambda (buffer warning)
(cider-repl-emit-stderr buffer warning)))))
(defun cider--repl-request-map (right-margin)
"Map to be merged into REPL eval requests.
RIGHT-MARGIN is as in `cider--nrepl-print-request-map'."
(map-merge 'hash-table
(cider--nrepl-print-request-map right-margin)
(unless cider-repl-use-pretty-printing
'(("nrepl.middleware.print/print" "cider.nrepl.pprint/pr")))
(when cider-repl-use-content-types
(cider--nrepl-content-type-map))))
(defun cider-repl--send-input (&optional newline)
"Go to the end of the input and send the current input.
If NEWLINE is true then add a newline at the end of the input."
(unless (cider-repl--in-input-area-p)
(error "No input at point"))
(let ((input (cider-repl--current-input)))
(if (string-blank-p input)
;; don't evaluate a blank string, but erase it and emit
;; a fresh prompt to acknowledge to the user.
(progn
(cider-repl--replace-input "")
(cider-repl-emit-prompt (current-buffer)))
;; otherwise evaluate the input
(goto-char (point-max))
(let ((end (point))) ; end of input, without the newline
(cider-repl--add-to-input-history input)
(when newline
(insert "\n"))
(let ((inhibit-modification-hooks t))
(add-text-properties cider-repl-input-start-mark
(point)
`(cider-old-input
,(cl-incf cider-repl-old-input-counter))))
(unless cider-repl-use-clojure-font-lock
(let ((overlay (make-overlay cider-repl-input-start-mark end)))
;; These properties are on an overlay so that they won't be taken
;; by kill/yank.
(overlay-put overlay 'read-only t)
(overlay-put overlay 'font-lock-face 'cider-repl-input-face))))
(let ((input-start (save-excursion (cider-repl-beginning-of-defun) (point))))
(goto-char (point-max))
(cider-repl--mark-input-start)
(cider-repl--mark-output-start)
(cider-nrepl-request:eval
input
(cider-repl-handler (current-buffer))
(cider-current-ns)
(line-number-at-pos input-start)
(cider-column-number-at-pos input-start)
(thread-last
(cider--repl-request-map fill-column)
(map-pairs)
(seq-mapcat #'identity)))))))
(defun cider-repl-return (&optional end-of-input)
"Evaluate the current input string, or insert a newline.
Send the current input only if a whole expression has been entered,
i.e. the parenthesis are matched.
When END-OF-INPUT is non-nil, send the input even if the parentheses
are not balanced."
(interactive "P")
(cond
(end-of-input
(cider-repl--send-input))
((and (get-text-property (point) 'cider-old-input)
(< (point) cider-repl-input-start-mark))
(cider-repl--grab-old-input end-of-input))
((cider-repl--input-complete-p cider-repl-input-start-mark (point-max))
(cider-repl--send-input t))
(t
(cider-repl-newline-and-indent)
(message "[input not complete]"))))
(defun cider-repl--grab-old-input (replace)
"Resend the old REPL input at point.
If REPLACE is non-nil the current input is replaced with the old
input; otherwise the new input is appended. The old input has the
text property `cider-old-input'."
(cl-multiple-value-bind (beg end) (cider-property-bounds 'cider-old-input)
(let ((old-input (buffer-substring beg end)) ;;preserve
;;properties, they will be removed later
(offset (- (point) beg)))
;; Append the old input or replace the current input
(cond (replace (goto-char cider-repl-input-start-mark))
(t (goto-char (point-max))
(unless (eq (char-before) ?\ )
(insert " "))))
(delete-region (point) (point-max))
(save-excursion
(insert old-input)
(when (equal (char-before) ?\n)
(delete-char -1)))
(forward-char offset))))
(defun cider-repl-closing-return ()
"Evaluate the current input string after closing input.
Closes all open parentheses or bracketed expressions."
(interactive)
(goto-char (point-max))
(save-restriction
(narrow-to-region cider-repl-input-start-mark (point))
(let ((matching-delimiter nil))
(while (ignore-errors
(save-excursion
(backward-up-list 1)
(setq matching-delimiter (cdr (syntax-after (point)))))
t)
(insert-char matching-delimiter))))
(cider-repl-return))
(defun cider-repl-toggle-pretty-printing ()
"Toggle pretty-printing in the REPL."
(interactive)
(setq cider-repl-use-pretty-printing (not cider-repl-use-pretty-printing))
(message "Pretty printing in REPL %s."
(if cider-repl-use-pretty-printing "enabled" "disabled")))
(defun cider-repl-toggle-content-types ()
"Toggle content-type rendering in the REPL."
(interactive)
(setq cider-repl-use-content-types (not cider-repl-use-content-types))
(message "Content-type support in REPL %s."
(if cider-repl-use-content-types "enabled" "disabled")))
(defun cider-repl-toggle-clojure-font-lock ()
"Toggle pretty-printing in the REPL."
(interactive)
(setq cider-repl-use-clojure-font-lock (not cider-repl-use-clojure-font-lock))
(message "Clojure font-locking in REPL %s."
(if cider-repl-use-clojure-font-lock "enabled" "disabled")))
(defun cider-repl-switch-to-other ()
"Switch between the Clojure and ClojureScript REPLs for the current project."
(interactive)
;; FIXME: implement cycling as session can hold more than two REPLs
(let* ((this-repl (cider-current-repl nil 'ensure))
(other-repl (car (seq-remove (lambda (r) (eq r this-repl)) (cider-repls nil t)))))
(if other-repl
(switch-to-buffer other-repl)
(user-error "No other REPL in current session (%s)"
(car (sesman-current-session 'CIDER))))))
(defvar cider-repl-clear-buffer-hook)
(defun cider-repl--clear-region (start end)
"Delete the output and its overlays between START and END."
(mapc #'delete-overlay (overlays-in start end))
(delete-region start end))
(defun cider-repl-clear-buffer ()
"Clear the currently visited REPL buffer completely.
See also the related commands `cider-repl-clear-output' and
`cider-find-and-clear-repl-output'."
(interactive)
(let ((inhibit-read-only t))
(cider-repl--clear-region (point-min) cider-repl-prompt-start-mark)
(cider-repl--clear-region cider-repl-output-start cider-repl-output-end)
(when (< (point) cider-repl-input-start-mark)
(goto-char cider-repl-input-start-mark))
(recenter t))
(run-hooks 'cider-repl-clear-buffer-hook))
(defun cider-repl-clear-output (&optional clear-repl)
"Delete the output inserted since the last input.
With a prefix argument CLEAR-REPL it will clear the entire REPL buffer instead."
(interactive "P")
(if clear-repl
(cider-repl-clear-buffer)
(let ((inhibit-read-only t))
(cider-repl--clear-region cider-repl-output-start cider-repl-output-end)
(save-excursion
(goto-char cider-repl-output-end)
(insert-before-markers
(propertize ";; output cleared\n" 'font-lock-face 'font-lock-comment-face))))))
(defun cider-repl-clear-banners ()
"Delete the REPL banners."
(interactive)
;; TODO: Improve the boundaries detecting logic
;; probably it should be based on text properties
;; the current implementation will clear warnings as well
(let ((start (point-min))
(end (save-excursion
(goto-char (point-min))
(cider-repl-next-prompt)
(forward-line -1)
(end-of-line)
(point))))
(when (< start end)
(let ((inhibit-read-only t))
(cider-repl--clear-region start (1+ end))))))
(defun cider-repl-clear-help-banner ()
"Delete the help REPL banner."
(interactive)
;; TODO: Improve the boundaries detecting logic
;; probably it should be based on text properties
(let ((start (save-excursion
(goto-char (point-min))
(search-forward ";; =")
(beginning-of-line)
(point)))
(end (save-excursion
(goto-char (point-min))
(cider-repl-next-prompt)
(search-backward ";; =")
(end-of-line)
(point))))
(when (< start end)
(let ((inhibit-read-only t))
(cider-repl--clear-region start (1+ end))))))
(defun cider-repl-switch-ns-handler (buffer)
"Make an nREPL evaluation handler for the REPL BUFFER's ns switching."
(nrepl-make-response-handler buffer
(lambda (_buffer _value))
(lambda (buffer out)
(cider-repl-emit-stdout buffer out))
(lambda (buffer err)
(cider-repl-emit-stderr buffer err))
(lambda (buffer)
(cider-repl-emit-prompt buffer))))
(defun cider-repl-set-ns (ns)
"Switch the namespace of the REPL buffer to NS.
If called from a cljc buffer act on both the Clojure and ClojureScript REPL
if there are more than one REPL present. If invoked in a REPL buffer the
command will prompt for the name of the namespace to switch to."
(interactive (list (if (or (derived-mode-p 'cider-repl-mode)
(null (cider-ns-form)))
(completing-read "Switch to namespace: "
(cider-sync-request:ns-list))
(cider-current-ns))))
(when (or (not ns) (equal ns ""))
(user-error "No namespace selected"))
(cider-map-repls :auto
(lambda (connection)
;; NOTE: `require' and `in-ns' are special forms in ClojureScript.
;; That's why we eval them separately instead of combining them with `do'.
(when cider-repl-require-ns-on-set
(cider-sync-tooling-eval (format "(require '%s)" ns) nil connection))
(cider-tooling-eval (format "(in-ns '%s)" ns)
(cider-repl-switch-ns-handler connection)))))
;;; Location References
(defcustom cider-locref-regexp-alist
'((stdout-stacktrace "[ \t]\\(at \\([^$(]+\\).*(\\([^:()]+\\):\\([0-9]+\\))\\)" 1 2 3 4)
(aviso-stacktrace "^[ \t]*\\(\\([^$/ \t]+\\).*? +\\([^:]+\\): +\\([0-9]+\\)\\)" 1 2 3 4)
(print-stacktrace "\\[\\([^][$ \t]+\\).* +\\([^ \t]+\\) +\\([0-9]+\\)\\]" 0 1 2 3)
(timbre-log "\\(TRACE\\|INFO\\|DEBUG\\|WARN\\|ERROR\\) +\\(\\[\\([^:]+\\):\\([0-9]+\\)\\]\\)" 2 3 nil 4)
(cljs-message "at line \\([0-9]+\\) +\\(.*\\)$" 0 nil 2 1)
(warning "warning,? +\\(\\([^\n:]+\\):\\([0-9]+\\):[0-9]+\\)" 1 nil 2 3)
(compilation ".*compiling:(\\([^\n:)]+\\):\\([0-9]+\\):[0-9]+)" 0 nil 1 2))
"Alist holding regular expressions for inline location references.
Each element in the alist has the form (NAME REGEXP HIGHLIGHT VAR FILE
LINE), where NAME is the identifier of the regexp, REGEXP - regexp matching
a location, HIGHLIGHT - sub-expression matching region to highlight on
mouse-over, VAR - sub-expression giving Clojure VAR to look up. FILE is
currently only used when VAR is nil and must be full resource path in that
case."
:type '(alist :key-type sexp)
:package-version '(cider. "0.16.0"))
(defun cider--locref-at-point-1 (reg-list)
"Workhorse for getting locref at point.
REG-LIST is an entry in `cider-locref-regexp-alist'."
(beginning-of-line)
(when (re-search-forward (nth 1 reg-list) (point-at-eol) t)
(let ((ix-highlight (or (nth 2 reg-list) 0))
(ix-var (nth 3 reg-list))
(ix-file (nth 4 reg-list))
(ix-line (nth 5 reg-list)))
(list
:type (car reg-list)
:highlight (cons (match-beginning ix-highlight) (match-end ix-highlight))
:var (and ix-var
(replace-regexp-in-string "_" "-"
(match-string-no-properties ix-var)
nil t))
:file (and ix-file (match-string-no-properties ix-file))
:line (and ix-line (string-to-number (match-string-no-properties ix-line)))))))
(defun cider-locref-at-point (&optional pos)
"Return a plist of components of the location reference at POS.
Limit search to current line only and return nil if no location has been
found. Returned keys are :type, :highlight, :var, :file, :line, where
:highlight is a cons of positions, :var and :file are strings or nil, :line
is a number. See `cider-locref-regexp-alist' for how to specify regexes
for locref look up."
(save-excursion
(goto-char (or pos (point)))
;; Regexp lookup on long lines can result in significant hangs #2532. We
;; assume that lines longer than 300 don't contain source references.
(when (< (- (point-at-eol) (point-at-bol)) 300)
(seq-some (lambda (rl) (cider--locref-at-point-1 rl))
cider-locref-regexp-alist))))
(defun cider-jump-to-locref-at-point (&optional pos)
"Identify location reference at POS and navigate to it.
This function is used from help-echo property inside REPL buffers and uses
regexes from `cider-locref-regexp-alist' to infer locations at point."
(interactive)
(if-let* ((loc (cider-locref-at-point pos)))
(let* ((var (plist-get loc :var))
(line (plist-get loc :line))
(file (or
;; 1) retrieve from info middleware
(when var
(or (cider-sync-request:ns-path var)
(nrepl-dict-get (cider-sync-request:info var) "file")))
(when-let* ((file (plist-get loc :file)))
;; 2) file detected by the regexp
(or
(if (file-name-absolute-p file)
file
;; when not absolute, expand within the current project
(when-let* ((proj (clojure-project-dir)))
(let ((path (expand-file-name file proj)))
(when (file-exists-p path)
path))))
;; 3) infer ns from the abbreviated path (common in
;; reflection warnings)
(let ((ns (cider-path-to-ns file)))
(cider-sync-request:ns-path ns)))))))
(if file
(cider--jump-to-loc-from-info (nrepl-dict "file" file "line" line) t)
(error "No source location for %s" var)))
(user-error "No location reference at point")))
(defvar cider-locref-hoover-overlay
(let ((o (make-overlay 1 1)))
(overlay-put o 'category 'cider-error-hoover)
;; (overlay-put o 'face 'highlight)
(overlay-put o 'pointer 'hand)
(overlay-put o 'mouse-face 'highlight)
(overlay-put o 'follow-link 'mouse)
(overlay-put o 'keymap
(let ((map (make-sparse-keymap)))
(define-key map [return] #'cider-jump-to-locref-at-point)
(define-key map [mouse-2] #'cider-jump-to-locref-at-point)
map))
o)
"Overlay used during hoovering on location references in REPL buffers.
One for all REPLs.")
(defun cider-locref-help-echo (_win buffer pos)
"Function for help-echo property in REPL buffers.
WIN, BUFFER and POS are the window, buffer and point under mouse position."
(with-current-buffer buffer
(if-let* ((hl (plist-get (cider-locref-at-point pos) :highlight)))
(move-overlay cider-locref-hoover-overlay (car hl) (cdr hl) buffer)
(delete-overlay cider-locref-hoover-overlay))
nil))
;;; History
(defcustom cider-repl-wrap-history nil
"T to wrap history around when the end is reached."
:type 'boolean)
;; These two vars contain the state of the last history search. We
;; only use them if `last-command' was `cider-repl--history-replace',
;; otherwise we reinitialize them.
(defvar cider-repl-input-history-position -1
"Newer items have smaller indices.")
(defvar cider-repl-history-pattern nil
"The regexp most recently used for finding input history.")
(defun cider-repl--add-to-input-history (string)
"Add STRING to the input history.
Empty strings and duplicates are ignored."
(unless (or (equal string "")
(equal string (car cider-repl-input-history)))
(push string cider-repl-input-history)
(cl-incf cider-repl-input-history-items-added)))
(defun cider-repl-delete-current-input ()
"Delete all text after the prompt."
(goto-char (point-max))
(delete-region cider-repl-input-start-mark (point-max)))
(defun cider-repl--replace-input (string)
"Replace the current REPL input with STRING."
(cider-repl-delete-current-input)
(insert-and-inherit string))
(defun cider-repl--position-in-history (start-pos direction regexp)
"Return the position of the history item starting at START-POS.
Search in DIRECTION for REGEXP.
Return -1 resp the length of the history if no item matches."
;; Loop through the history list looking for a matching line
(let* ((step (cl-ecase direction
(forward -1)
(backward 1)))
(history cider-repl-input-history)
(len (length history)))
(cl-loop for pos = (+ start-pos step) then (+ pos step)
if (< pos 0) return -1
if (<= len pos) return len
if (string-match-p regexp (nth pos history)) return pos)))
(defun cider-repl--history-replace (direction &optional regexp)
"Replace the current input with the next line in DIRECTION.
DIRECTION is 'forward' or 'backward' (in the history list).
If REGEXP is non-nil, only lines matching REGEXP are considered."
(setq cider-repl-history-pattern regexp)
(let* ((min-pos -1)
(max-pos (length cider-repl-input-history))
(pos0 (cond ((cider-history-search-in-progress-p)
cider-repl-input-history-position)
(t min-pos)))
(pos (cider-repl--position-in-history pos0 direction (or regexp "")))
(msg nil))
(cond ((and (< min-pos pos) (< pos max-pos))
(cider-repl--replace-input (nth pos cider-repl-input-history))
(setq msg (format "History item: %d" pos)))
((not cider-repl-wrap-history)
(setq msg (cond ((= pos min-pos) "End of history")
((= pos max-pos) "Beginning of history"))))
(cider-repl-wrap-history
(setq pos (if (= pos min-pos) max-pos min-pos))
(setq msg "Wrapped history")))
(when (or (<= pos min-pos) (<= max-pos pos))
(when regexp
(setq msg (concat msg "; no matching item"))))
(message "%s%s" msg (cond ((not regexp) "")
(t (format "; current regexp: %s" regexp))))
(setq cider-repl-input-history-position pos)
(setq this-command 'cider-repl--history-replace)))
(defun cider-history-search-in-progress-p ()
"Return t if a current history search is in progress."
(eq last-command 'cider-repl--history-replace))
(defun cider-terminate-history-search ()
"Terminate the current history search."
(setq last-command this-command))
(defun cider-repl-previous-input ()
"Cycle backwards through input history.
If the `last-command' was a history navigation command use the
same search pattern for this command.
Otherwise use the current input as search pattern."
(interactive)
(cider-repl--history-replace 'backward (cider-repl-history-pattern t)))
(defun cider-repl-next-input ()
"Cycle forwards through input history.
See `cider-previous-input'."
(interactive)
(cider-repl--history-replace 'forward (cider-repl-history-pattern t)))
(defun cider-repl-forward-input ()
"Cycle forwards through input history."
(interactive)
(cider-repl--history-replace 'forward (cider-repl-history-pattern)))
(defun cider-repl-backward-input ()
"Cycle backwards through input history."
(interactive)
(cider-repl--history-replace 'backward (cider-repl-history-pattern)))
(defun cider-repl-previous-matching-input (regexp)
"Find the previous input matching REGEXP."
(interactive "sPrevious element matching (regexp): ")
(cider-terminate-history-search)
(cider-repl--history-replace 'backward regexp))
(defun cider-repl-next-matching-input (regexp)
"Find then next input matching REGEXP."
(interactive "sNext element matching (regexp): ")
(cider-terminate-history-search)
(cider-repl--history-replace 'forward regexp))
(defun cider-repl-history-pattern (&optional use-current-input)
"Return the regexp for the navigation commands.
If USE-CURRENT-INPUT is non-nil, use the current input."
(cond ((cider-history-search-in-progress-p)
cider-repl-history-pattern)
(use-current-input
(cl-assert (<= cider-repl-input-start-mark (point)))
(let ((str (cider-repl--current-input t)))
(cond ((string-match-p "^[ \n]*$" str) nil)
(t (concat "^" (regexp-quote str))))))
(t nil)))
;;; persistent history
(defcustom cider-repl-history-size 500
"The maximum number of items to keep in the REPL history."
:type 'integer
:safe #'integerp)
(defcustom cider-repl-history-file nil
"File to save the persistent REPL history to."
:type 'string
:safe #'stringp)
(defun cider-repl--history-read-filename ()
"Ask the user which file to use, defaulting `cider-repl-history-file'."
(read-file-name "Use CIDER REPL history file: "
cider-repl-history-file))
(defun cider-repl--history-read (filename)
"Read history from FILENAME and return it.
It does not yet set the input history."
(if (file-readable-p filename)
(with-temp-buffer
(insert-file-contents filename)
(when (> (buffer-size (current-buffer)) 0)
(read (current-buffer))))
'()))
(defun cider-repl-history-load (&optional filename)
"Load history from FILENAME into current session.
FILENAME defaults to the value of `cider-repl-history-file' but user
defined filenames can be used to read special history files.
The value of `cider-repl-input-history' is set by this function."
(interactive (list (cider-repl--history-read-filename)))
(let ((f (or filename cider-repl-history-file)))
;; TODO: probably need to set cider-repl-input-history-position as well.
;; in a fresh connection the newest item in the list is currently
;; not available. After sending one input, everything seems to work.
(setq cider-repl-input-history (cider-repl--history-read f))))
(defun cider-repl--history-write (filename)
"Write history to FILENAME.
Currently coding system for writing the contents is hardwired to
utf-8-unix."
(let* ((mhist (cider-repl--histories-merge cider-repl-input-history
cider-repl-input-history-items-added
(cider-repl--history-read filename)))
;; newest items are at the beginning of the list, thus 0
(hist (cl-subseq mhist 0 (min (length mhist) cider-repl-history-size))))
(unless (file-writable-p filename)
(error (format "History file not writable: %s" filename)))
(let ((print-length nil) (print-level nil))
(with-temp-file filename
;; TODO: really set cs for output
;; TODO: does cs need to be customizable?
(insert ";; -*- coding: utf-8-unix -*-\n")
(insert ";; Automatically written history of CIDER REPL session\n")
(insert ";; Edit at your own risk\n\n")
(prin1 (mapcar #'substring-no-properties hist) (current-buffer))))))
(defun cider-repl-history-save (&optional filename)
"Save the current REPL input history to FILENAME.
FILENAME defaults to the value of `cider-repl-history-file'."
(interactive (list (cider-repl--history-read-filename)))
(let* ((file (or filename cider-repl-history-file)))
(cider-repl--history-write file)))
(defun cider-repl-history-just-save ()
"Just save the history to `cider-repl-history-file'.
This function is meant to be used in hooks to avoid lambda
constructs."
(cider-repl-history-save cider-repl-history-file))
;; SLIME has different semantics and will not save any duplicates.
;; we keep track of how many items were added to the history in the
;; current session in `cider-repl--add-to-input-history' and merge only the
;; new items with the current history found in the file, which may
;; have been changed in the meantime by another session.
(defun cider-repl--histories-merge (session-hist n-added-items file-hist)
"Merge histories from SESSION-HIST adding N-ADDED-ITEMS into FILE-HIST."
(append (cl-subseq session-hist 0 n-added-items)
file-hist))
;;; REPL shortcuts
(defcustom cider-repl-shortcut-dispatch-char ?\,
"Character used to distinguish REPL commands from Lisp forms."
:type '(character))
(defvar cider-repl-shortcuts (make-hash-table :test 'equal))
(defun cider-repl-add-shortcut (name handler)
"Add a REPL shortcut command, defined by NAME and HANDLER."
(puthash name handler cider-repl-shortcuts))
(declare-function cider-toggle-trace-ns "cider-tracing")
(declare-function cider-undef "cider-eval")
(declare-function cider-browse-ns "cider-browse-ns")
(declare-function cider-classpath "cider-classpath")
(declare-function cider-repl-history "cider-repl-history")
(declare-function cider-run "cider-mode")
(declare-function cider-ns-refresh "cider-ns")
(declare-function cider-ns-reload "cider-ns")
(declare-function cider-find-var "cider-find")
(declare-function cider-version "cider")
(declare-function cider-test-run-loaded-tests "cider-test")
(declare-function cider-test-run-project-tests "cider-test")
(declare-function cider-sideloader-start "cider-eval")
(cider-repl-add-shortcut "clear-output" #'cider-repl-clear-output)
(cider-repl-add-shortcut "clear" #'cider-repl-clear-buffer)
(cider-repl-add-shortcut "clear-banners" #'cider-repl-clear-banners)
(cider-repl-add-shortcut "clear-help-banner" #'cider-repl-clear-help-banner)
(cider-repl-add-shortcut "trim" #'cider-repl-trim-buffer)
(cider-repl-add-shortcut "ns" #'cider-repl-set-ns)
(cider-repl-add-shortcut "toggle-pprint" #'cider-repl-toggle-pretty-printing)
(cider-repl-add-shortcut "toggle-font-lock" #'cider-repl-toggle-clojure-font-lock)
(cider-repl-add-shortcut "toggle-content-types" #'cider-repl-toggle-content-types)
(cider-repl-add-shortcut "browse-ns" (lambda () (interactive) (cider-browse-ns (cider-current-ns))))
(cider-repl-add-shortcut "classpath" #'cider-classpath)
(cider-repl-add-shortcut "history" #'cider-repl-history)
(cider-repl-add-shortcut "trace-ns" #'cider-toggle-trace-ns)
(cider-repl-add-shortcut "sideloader-start" #'cider-sideloader-start)
(cider-repl-add-shortcut "undef" #'cider-undef)
(cider-repl-add-shortcut "refresh" #'cider-ns-refresh)
(cider-repl-add-shortcut "reload" #'cider-ns-reload)
(cider-repl-add-shortcut "find-var" #'cider-find-var)
(cider-repl-add-shortcut "doc" #'cider-doc)
(cider-repl-add-shortcut "help" #'cider-repl-shortcuts-help)
(cider-repl-add-shortcut "test-ns" #'cider-test-run-ns-tests)
(cider-repl-add-shortcut "test-all" #'cider-test-run-loaded-tests)
(cider-repl-add-shortcut "test-project" #'cider-test-run-project-tests)
(cider-repl-add-shortcut "test-ns-with-filters" #'cider-test-run-ns-tests-with-filters)
(cider-repl-add-shortcut "test-all-with-filters" (lambda () (interactive) (cider-test-run-loaded-tests 'prompt-for-filters)))
(cider-repl-add-shortcut "test-project-with-filters" (lambda () (interactive) (cider-test-run-project-tests 'prompt-for-filters)))
(cider-repl-add-shortcut "test-report" #'cider-test-show-report)
(cider-repl-add-shortcut "run" #'cider-run)
(cider-repl-add-shortcut "conn-info" #'cider-describe-connection)
(cider-repl-add-shortcut "version" #'cider-version)
(cider-repl-add-shortcut "require-repl-utils" #'cider-repl-require-repl-utils)
;; So many ways to quit :-)
(cider-repl-add-shortcut "adios" #'cider-quit)
(cider-repl-add-shortcut "sayonara" #'cider-quit)
(cider-repl-add-shortcut "quit" #'cider-quit)
(cider-repl-add-shortcut "restart" #'cider-restart)
(defconst cider-repl-shortcuts-help-buffer "*CIDER REPL Shortcuts Help*")
(defun cider-repl-shortcuts-help ()
"Display a help buffer."
(interactive)
(ignore-errors (kill-buffer cider-repl-shortcuts-help-buffer))
(with-current-buffer (get-buffer-create cider-repl-shortcuts-help-buffer)
(insert "CIDER REPL shortcuts:\n\n")
(maphash (lambda (k v) (insert (format "%s:\n\t%s\n" k v))) cider-repl-shortcuts)
(goto-char (point-min))
(help-mode)
(display-buffer (current-buffer) t))
(cider-repl-handle-shortcut)
(current-buffer))
(defun cider-repl--available-shortcuts ()
"Return the available REPL shortcuts."
(cider-util--hash-keys cider-repl-shortcuts))
(defun cider-repl-handle-shortcut ()
"Execute a REPL shortcut."
(interactive)
(if (> (point) cider-repl-input-start-mark)
(insert (string cider-repl-shortcut-dispatch-char))
(let ((command (completing-read "Command: "
(cider-repl--available-shortcuts))))
(if (not (equal command ""))
(let ((command-func (gethash command cider-repl-shortcuts)))
(if command-func
(call-interactively command-func)
(error "Unknown command %S. Available commands: %s"
command-func
(mapconcat #'identity (cider-repl--available-shortcuts) ", "))))
(error "No command selected")))))
;;;;; CIDER REPL mode
(defvar cider-repl-mode-hook nil
"Hook executed when entering `cider-repl-mode'.")
(defvar cider-repl-mode-syntax-table
(copy-syntax-table clojure-mode-syntax-table))
(declare-function cider-eval-last-sexp "cider-eval")
(declare-function cider-toggle-trace-ns "cider-tracing")
(declare-function cider-toggle-trace-var "cider-tracing")
(declare-function cider-find-resource "cider-find")
(declare-function cider-find-ns "cider-find")
(declare-function cider-find-keyword "cider-find")
(declare-function cider-find-var "cider-find")
(declare-function cider-switch-to-last-clojure-buffer "cider-mode")
(declare-function cider-macroexpand-1 "cider-macroexpansion")
(declare-function cider-macroexpand-all "cider-macroexpansion")
(declare-function cider-selector "cider-selector")
(declare-function cider-jack-in-clj "cider")
(declare-function cider-jack-in-cljs "cider")
(declare-function cider-connect-clj "cider")
(declare-function cider-connect-cljs "cider")
(defvar cider-repl-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-d") 'cider-doc-map)
(define-key map (kbd "C-c ,") 'cider-test-commands-map)
(define-key map (kbd "C-c C-t") 'cider-test-commands-map)
(define-key map (kbd "M-.") #'cider-find-var)
(define-key map (kbd "C-c C-.") #'cider-find-ns)
(define-key map (kbd "C-c C-:") #'cider-find-keyword)
(define-key map (kbd "M-,") #'cider-pop-back)
(define-key map (kbd "C-c M-.") #'cider-find-resource)
(define-key map (kbd "RET") #'cider-repl-return)
(define-key map (kbd "TAB") #'cider-repl-tab)
(define-key map (kbd "C-<return>") #'cider-repl-closing-return)
(define-key map (kbd "C-j") #'cider-repl-newline-and-indent)
(define-key map (kbd "C-c C-o") #'cider-repl-clear-output)
(define-key map (kbd "C-c M-n") #'cider-repl-set-ns)
(define-key map (kbd "C-c C-u") #'cider-repl-kill-input)
(define-key map (kbd "C-S-a") #'cider-repl-bol-mark)
(define-key map [S-home] #'cider-repl-bol-mark)
(define-key map (kbd "C-<up>") #'cider-repl-backward-input)
(define-key map (kbd "C-<down>") #'cider-repl-forward-input)
(define-key map (kbd "M-p") #'cider-repl-previous-input)
(define-key map (kbd "M-n") #'cider-repl-next-input)
(define-key map (kbd "M-r") #'cider-repl-previous-matching-input)
(define-key map (kbd "M-s") #'cider-repl-next-matching-input)
(define-key map (kbd "C-c C-n") #'cider-repl-next-prompt)
(define-key map (kbd "C-c C-p") #'cider-repl-previous-prompt)
(define-key map (kbd "C-c C-b") #'cider-interrupt)
(define-key map (kbd "C-c C-c") #'cider-interrupt)
(define-key map (kbd "C-c C-m") #'cider-macroexpand-1)
(define-key map (kbd "C-c M-m") #'cider-macroexpand-all)
(define-key map (kbd "C-c C-s") #'sesman-map)
(define-key map (kbd "C-c C-z") #'cider-switch-to-last-clojure-buffer)
(define-key map (kbd "C-c M-o") #'cider-repl-switch-to-other)
(define-key map (kbd "C-c M-s") #'cider-selector)
(define-key map (kbd "C-c M-d") #'cider-describe-connection)
(define-key map (kbd "C-c C-q") #'cider-quit)
(define-key map (kbd "C-c M-r") #'cider-restart)
(define-key map (kbd "C-c M-i") #'cider-inspect)
(define-key map (kbd "C-c M-p") #'cider-repl-history)
(define-key map (kbd "C-c M-t v") #'cider-toggle-trace-var)
(define-key map (kbd "C-c M-t n") #'cider-toggle-trace-ns)
(define-key map (kbd "C-c C-x") 'cider-start-map)
(define-key map (kbd "C-x C-e") #'cider-eval-last-sexp)
(define-key map (kbd "C-c C-r") 'clojure-refactor-map)
(define-key map (kbd "C-c C-v") 'cider-eval-commands-map)
(define-key map (kbd "C-c M-j") #'cider-jack-in-clj)
(define-key map (kbd "C-c M-J") #'cider-jack-in-cljs)
(define-key map (kbd "C-c M-c") #'cider-connect-clj)
(define-key map (kbd "C-c M-C") #'cider-connect-cljs)
(define-key map (string cider-repl-shortcut-dispatch-char) #'cider-repl-handle-shortcut)
(easy-menu-define cider-repl-mode-menu map
"Menu for CIDER's REPL mode"
`("REPL"
["Complete symbol" complete-symbol]
"--"
,cider-doc-menu
"--"
("Find"
["Find definition" cider-find-var]
["Find namespace" cider-find-ns]
["Find resource" cider-find-resource]
["Find keyword" cider-find-keyword]
["Go back" cider-pop-back])
"--"
["Switch to Clojure buffer" cider-switch-to-last-clojure-buffer]
["Switch to other REPL" cider-repl-switch-to-other]
"--"
("Macroexpand"
["Macroexpand-1" cider-macroexpand-1]
["Macroexpand-all" cider-macroexpand-all])
"--"
,cider-test-menu
"--"
["Run project (-main function)" cider-run]
["Inspect" cider-inspect]
["Toggle var tracing" cider-toggle-trace-var]
["Toggle ns tracing" cider-toggle-trace-ns]
["Refresh loaded code" cider-ns-refresh]
"--"
["Set REPL ns" cider-repl-set-ns]
["Toggle pretty printing" cider-repl-toggle-pretty-printing]
["Toggle Clojure font-lock" cider-repl-toggle-clojure-font-lock]
["Toggle rich content types" cider-repl-toggle-content-types]
["Require REPL utils" cider-repl-require-repl-utils]
"--"
["Browse classpath" cider-classpath]
["Browse classpath entry" cider-open-classpath-entry]
["Browse namespace" cider-browse-ns]
["Browse all namespaces" cider-browse-ns-all]
["Browse spec" cider-browse-spec]
["Browse all specs" cider-browse-spec-all]
"--"
["Next prompt" cider-repl-next-prompt]
["Previous prompt" cider-repl-previous-prompt]
["Clear output" cider-repl-clear-output]
["Clear buffer" cider-repl-clear-buffer]
["Trim buffer" cider-repl-trim-buffer]
["Clear banners" cider-repl-clear-banners]
["Clear help banner" cider-repl-clear-help-banner]
["Kill input" cider-repl-kill-input]
"--"
["Interrupt evaluation" cider-interrupt]
"--"
["Connection info" cider-describe-connection]
"--"
["Close ancillary buffers" cider-close-ancillary-buffers]
["Quit" cider-quit]
["Restart" cider-restart]
"--"
["Clojure Cheatsheet" cider-cheatsheet]
"--"
["A sip of CIDER" cider-drink-a-sip]
["View user manual" cider-view-manual]
["View quick reference card" cider-view-refcard]
["Report a bug" cider-report-bug]
["Version info" cider-version]))
map))
(sesman-install-menu cider-repl-mode-map)
(defun cider-repl-wrap-fontify-function (func)
"Return a function that will call FUNC narrowed to input region."
(lambda (beg end &rest rest)
(when (and cider-repl-input-start-mark
(> end cider-repl-input-start-mark))
(save-restriction
(narrow-to-region cider-repl-input-start-mark (point-max))
(let ((font-lock-dont-widen t))
(apply func (max beg cider-repl-input-start-mark) end rest))))))
(declare-function cider-complete-at-point "cider-completion")
(defvar cider--static-font-lock-keywords)
(define-derived-mode cider-repl-mode fundamental-mode "REPL"
"Major mode for Clojure REPL interactions.
\\{cider-repl-mode-map}"
(clojure-mode-variables)
(clojure-font-lock-setup)
(font-lock-add-keywords nil cider--static-font-lock-keywords)
(setq-local sesman-system 'CIDER)
(setq-local font-lock-fontify-region-function
(cider-repl-wrap-fontify-function font-lock-fontify-region-function))
(setq-local font-lock-unfontify-region-function
(cider-repl-wrap-fontify-function font-lock-unfontify-region-function))
(set-syntax-table cider-repl-mode-syntax-table)
(cider-eldoc-setup)
;; At the REPL, we define beginning-of-defun and end-of-defun to be
;; the start of the previous prompt or next prompt respectively.
;; Notice the interplay with `cider-repl-beginning-of-defun'.
(setq-local beginning-of-defun-function #'cider-repl-mode-beginning-of-defun)
(setq-local end-of-defun-function #'cider-repl-mode-end-of-defun)
(setq-local prettify-symbols-alist clojure--prettify-symbols-alist)
;; apply dir-local variables to REPL buffers
(hack-dir-local-variables-non-file-buffer)
(when cider-repl-history-file
(cider-repl-history-load cider-repl-history-file)
(add-hook 'kill-buffer-hook #'cider-repl-history-just-save t t)
(add-hook 'kill-emacs-hook #'cider-repl-history-just-save))
(add-hook 'completion-at-point-functions #'cider-complete-at-point nil t)
(add-hook 'paredit-mode-hook (lambda () (clojure-paredit-setup cider-repl-mode-map))))
(provide 'cider-repl)
;;; cider-repl.el ends here
;;; cider-resolve.el --- Resolve clojure symbols according to current nREPL connection -*- lexical-binding: t; -*-
;; Copyright © 2015-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; The ns cache is a dict of namespaces stored in the connection buffer. This
;; file offers functions to easily get information about variables from this
;; cache, given the variable's name and the file's namespace. This
;; functionality is similar to that offered by the `cider-var-info' function
;; (and others). The difference is that all functions in this file operate
;; without contacting the server (they still rely on an active connection
;; buffer, but no messages are actually exchanged).
;; For this reason, the functions here are well suited for very
;; performance-sentitive operations, such as font-locking or
;; indentation. Meanwhile, operations like code-jumping are better off
;; communicating with the middleware, just in the off chance that the cache is
;; outdated.
;; Below is a typical entry on this cache dict. Note that clojure.core symbols
;; are excluded from the refers to save space.
;; "cider.nrepl.middleware.track-state"
;; (dict "aliases"
;; (dict "cljs" "cider.nrepl.middleware.util.cljs"
;; "misc" "cider.nrepl.middleware.util.misc"
;; "set" "clojure.set")
;; "interns" (dict a
;; "assoc-state" (dict "arglists"
;; (("response"
;; (dict "as" "msg" "keys"
;; ("session")))))
;; "filter-core" (dict "arglists"
;; (("refers")))
;; "make-transport" (dict "arglists"
;; (((dict "as" "msg" "keys"
;; ("transport")))))
;; "ns-as-map" (dict "arglists"
;; (("ns")))
;; "ns-cache" (dict)
;; "relevant-meta" (dict "arglists"
;; (("var")))
;; "update-vals" (dict "arglists"
;; (("m" "f")))
;; "wrap-tracker" (dict "arglists"
;; (("handler"))))
;; "refers" (dict "set-descriptor!" "#'nrepl.middleware/set-descriptor!"))
;;; Code:
(require 'cider-client)
(require 'nrepl-dict)
(require 'cider-util)
(defvar cider-repl-ns-cache)
(defun cider-resolve--get-in (&rest keys)
"Return (nrepl-dict-get-in cider-repl-ns-cache KEYS)."
(when-let* ((conn (cider-current-repl)))
(with-current-buffer conn
(nrepl-dict-get-in cider-repl-ns-cache keys))))
(defun cider-resolve-alias (ns alias)
"Return the namespace that ALIAS refers to in namespace NS.
If it doesn't point anywhere, returns ALIAS."
(or (cider-resolve--get-in ns "aliases" alias)
alias))
(defconst cider-resolve--prefix-regexp "\\`\\(?:#'\\)?\\([^/]+\\)/")
(defun cider-resolve-var (ns var)
"Return a dict of the metadata of a clojure var VAR in namespace NS.
VAR is a string.
Return nil only if VAR cannot be resolved."
(let* ((var-ns (when (string-match cider-resolve--prefix-regexp var)
(cider-resolve-alias ns (match-string 1 var))))
(name (replace-regexp-in-string cider-resolve--prefix-regexp "" var)))
(or
(cider-resolve--get-in (or var-ns ns) "interns" name)
(unless var-ns
;; If the var had no prefix, it might be referred.
(if-let* ((referral (cider-resolve--get-in ns "refers" name)))
(cider-resolve-var ns referral)
;; Or it might be from core.
(unless (equal ns "clojure.core")
(cider-resolve-var "clojure.core" name)))))))
(defun cider-resolve-core-ns ()
"Return a dict of the core namespace for current connection.
This will be clojure.core or cljs.core depending on the return value of the
function `cider-repl-type'."
(when-let* ((repl (cider-current-repl)))
(with-current-buffer repl
(cider-resolve--get-in (if (eq cider-repl-type 'cljs)
"cljs.core"
"clojure.core")))))
(defun cider-resolve-ns-symbols (ns)
"Return a plist of all valid symbols in NS.
Each entry's value is the metadata of the var that the symbol refers to.
NS can be the namespace name, or a dict of the namespace itself."
(when-let* ((dict (if (stringp ns)
(cider-resolve--get-in ns)
ns)))
(nrepl-dbind-response dict (interns _refers aliases)
(append (cdr interns)
(nrepl-dict-flat-map (lambda (alias namespace)
(nrepl-dict-flat-map (lambda (sym meta)
(list (concat alias "/" sym) meta))
(cider-resolve--get-in namespace "interns")))
aliases)))))
(provide 'cider-resolve)
;;; cider-resolve.el ends here
;;; cider-scratch.el --- *scratch* buffer for Clojure -*- lexical-binding: t -*-
;; Copyright © 2014-2022 Bozhidar Batsov and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Imitate Emacs's *scratch* buffer.
;;; Code:
(require 'cider-eval)
(require 'clojure-mode)
(require 'easymenu)
(defcustom cider-scratch-initial-message
";; This buffer is for Clojure experiments and evaluation.\n
;; Press C-j to evaluate the last expression.\n
;; You can also press C-u C-j to evaluate the expression and pretty-print its result.\n\n"
"The initial message displayed in new scratch buffers."
:type 'string
:group 'cider
:package-version '(cider . "0.18.0"))
(defvar cider-clojure-interaction-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map clojure-mode-map)
(define-key map (kbd "C-j") #'cider-eval-print-last-sexp)
(define-key map [remap paredit-newline] #'cider-eval-print-last-sexp)
(easy-menu-define cider-clojure-interaction-mode-menu map
"Menu for Clojure Interaction mode"
'("Clojure Interaction"
(["Eval and print last sexp" #'cider-eval-print-last-sexp]
"--"
["Reset" #'cider-scratch-reset])))
map))
(defconst cider-scratch-buffer-name "*cider-scratch*")
;;;###autoload
(defun cider-scratch ()
"Go to the scratch buffer named `cider-scratch-buffer-name'."
(interactive)
(pop-to-buffer (cider-scratch-find-or-create-buffer)))
(defun cider-scratch-find-or-create-buffer ()
"Find or create the scratch buffer."
(or (get-buffer cider-scratch-buffer-name)
(cider-scratch--create-buffer)))
(define-derived-mode cider-clojure-interaction-mode clojure-mode "Clojure Interaction"
"Major mode for typing and evaluating Clojure forms.
Like `clojure-mode' except that \\[cider-eval-print-last-sexp] evals the Lisp expression
before point, and prints its value into the buffer, advancing point.
\\{cider-clojure-interaction-mode-map}"
(setq-local sesman-system 'CIDER))
(defun cider-scratch--insert-welcome-message ()
"Insert the welcome message for the scratch buffer."
(insert cider-scratch-initial-message))
(defun cider-scratch--create-buffer ()
"Create a new scratch buffer."
(with-current-buffer (get-buffer-create cider-scratch-buffer-name)
(cider-clojure-interaction-mode)
(cider-scratch--insert-welcome-message)
(current-buffer)))
(defun cider-scratch-reset ()
"Reset the current scratch buffer."
(interactive)
(erase-buffer)
(cider-scratch--insert-welcome-message))
(provide 'cider-scratch)
;;; cider-scratch.el ends here
;;; cider-selector.el --- Buffer selection command inspired by SLIME's selector -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Buffer selection command inspired by SLIME's selector.
;;; Code:
(require 'cider-client)
(require 'cider-eval)
(require 'cider-scratch)
(require 'cider-profile)
(defconst cider-selector-help-buffer "*CIDER Selector Help*"
"The name of the selector's help buffer.")
(defvar cider-selector-methods nil
"List of buffer-selection methods for the `cider-selector' command.
Each element is a list (KEY DESCRIPTION FUNCTION).
DESCRIPTION is a one-line description of what the key selects.")
(defvar cider-selector-other-window nil
"If non-nil use `switch-to-buffer-other-window'.
Not meant to be set by users. It's used internally
by `cider-selector'.")
(defun cider-selector--recently-visited-buffer (mode &optional consider-visible-p)
"Return the most recently visited buffer, deriving its `major-mode' from MODE.
CONSIDER-VISIBLE-P will allow handling of visible windows as well.
First pass only considers buffers that are not already visible.
Second pass will attempt one of visible ones for scenarios where the window
is visible, but not focused."
(cl-loop for buffer in (buffer-list)
when (and (with-current-buffer buffer
(derived-mode-p mode))
;; names starting with space are considered hidden by Emacs
(not (string-match-p "^ " (buffer-name buffer)))
(or consider-visible-p
(null (get-buffer-window buffer 'visible))))
return buffer
finally (if consider-visible-p
(error "Can't find unshown buffer in %S" mode)
(cider-selector--recently-visited-buffer mode t))))
;;;###autoload
(defun cider-selector (&optional other-window)
"Select a new buffer by type, indicated by a single character.
The user is prompted for a single character indicating the method by
which to choose a new buffer. The `?' character describes the
available methods. OTHER-WINDOW provides an optional target.
See `def-cider-selector-method' for defining new methods."
(interactive)
(message "Select [%s]: "
(apply #'string (mapcar #'car cider-selector-methods)))
(let* ((cider-selector-other-window other-window)
(ch (save-window-excursion
(select-window (minibuffer-window))
(read-char)))
(method (cl-find ch cider-selector-methods :key #'car)))
(cond (method
(funcall (cl-caddr method)))
(t
(message "No method for character: ?\\%c" ch)
(ding)
(sleep-for 1)
(discard-input)
(cider-selector)))))
(defmacro def-cider-selector-method (key description &rest body)
"Define a new `cider-select' buffer selection method.
KEY is the key the user will enter to choose this method.
DESCRIPTION is a one-line sentence describing how the method
selects a buffer.
BODY is a series of forms which are evaluated when the selector
is chosen. The returned buffer is selected with
`switch-to-buffer'."
(let ((method `(lambda ()
(let ((buffer (progn ,@body)))
(cond ((not (and buffer (get-buffer buffer)))
(message "No such buffer: %S" buffer)
(ding))
((get-buffer-window buffer)
(select-window (get-buffer-window buffer)))
(cider-selector-other-window
(switch-to-buffer-other-window buffer))
(t
(switch-to-buffer buffer)))))))
`(setq cider-selector-methods
(cl-sort (cons (list ,key ,description ,method)
(cl-remove ,key cider-selector-methods :key #'car))
#'< :key #'car))))
(def-cider-selector-method ?? "Selector help buffer."
(ignore-errors (kill-buffer cider-selector-help-buffer))
(with-current-buffer (get-buffer-create cider-selector-help-buffer)
(insert "CIDER Selector Methods:\n\n")
(cl-loop for (key line nil) in cider-selector-methods
do (insert (format "%c:\t%s\n" key line)))
(goto-char (point-min))
(help-mode)
(display-buffer (current-buffer) t))
(cider-selector)
(current-buffer))
(cl-pushnew (list ?4 "Select in other window" (lambda () (cider-selector t)))
cider-selector-methods :key #'car)
(def-cider-selector-method ?c
"Most recently visited clojure-mode buffer."
(cider-selector--recently-visited-buffer 'clojure-mode))
(def-cider-selector-method ?e
"Most recently visited emacs-lisp-mode buffer."
(cider-selector--recently-visited-buffer 'emacs-lisp-mode))
(def-cider-selector-method ?q "Abort."
(top-level))
(def-cider-selector-method ?r
"Current REPL buffer or as a fallback, the most recently
visited cider-repl-mode buffer."
(or (cider-current-repl)
(cider-selector--recently-visited-buffer 'cider-repl-mode)))
(def-cider-selector-method ?m
"Current connection's *nrepl-messages* buffer."
(nrepl-messages-buffer (cider-current-repl)))
(def-cider-selector-method ?x
"*cider-error* buffer."
cider-error-buffer)
(def-cider-selector-method ?p
"*cider-profile* buffer."
cider-profile-buffer)
(def-cider-selector-method ?d
"*cider-doc* buffer."
cider-doc-buffer)
(def-cider-selector-method ?s
"*cider-scratch* buffer."
(cider-scratch-find-or-create-buffer))
(provide 'cider-selector)
;;; cider-selector.el ends here
;;; cider-stacktrace.el --- Stacktrace navigator -*- lexical-binding: t -*-
;; Copyright © 2014-2022 Jeff Valk, Bozhidar Batsov and CIDER contributors
;; Author: Jeff Valk <jv@jeffvalk.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Stacktrace filtering and stack frame source navigation
;;; Code:
(require 'button)
(require 'cl-lib)
(require 'easymenu)
(require 'map)
(require 'seq)
(require 'subr-x)
(require 'cider-common)
(require 'cider-client)
(require 'cider-popup)
(require 'cider-util)
;; Variables
(defgroup cider-stacktrace nil
"Stacktrace filtering and navigation."
:prefix "cider-stacktrace-"
:group 'cider)
(defcustom cider-stacktrace-fill-column t
"Fill column for error messages in stacktrace display.
If nil, messages will not be wrapped. If truthy but non-numeric,
`fill-column' will be used."
:type 'list
:package-version '(cider . "0.7.0"))
(defcustom cider-stacktrace-default-filters '(tooling dup)
"Frame types to omit from initial stacktrace display."
:type 'list
:package-version '(cider . "0.6.0"))
(make-obsolete 'cider-stacktrace-print-length 'cider-stacktrace-print-options "0.20")
(make-obsolete 'cider-stacktrace-print-level 'cider-stacktrace-print-options "0.20")
(make-obsolete-variable 'cider-stacktrace-print-options 'cider-print-options "0.21")
(defvar cider-stacktrace-detail-max 2
"The maximum detail level for causes.")
(defvar-local cider-stacktrace-hidden-frame-count 0)
(defvar-local cider-stacktrace-filters nil)
(defvar-local cider-stacktrace-cause-visibility nil)
(defvar-local cider-stacktrace-positive-filters nil)
(defconst cider-error-buffer "*cider-error*")
(make-obsolete 'cider-visit-error-buffer 'cider-selector "0.18")
(defcustom cider-stacktrace-suppressed-errors '()
"Errors that won't make the stacktrace buffer 'pop-over' your active window.
The error types are represented as strings."
:type 'list
:package-version '(cider . "0.12.0"))
;; Faces
(defface cider-stacktrace-error-class-face
'((t (:inherit font-lock-warning-face)))
"Face for exception class names."
:package-version '(cider . "0.6.0"))
(defface cider-stacktrace-error-message-face
'((t (:inherit font-lock-doc-face)))
"Face for exception messages."
:package-version '(cider . "0.7.0"))
(defface cider-stacktrace-filter-active-face
'((t (:inherit button :underline t :weight normal)))
"Face for filter buttons representing frames currently visible."
:package-version '(cider . "0.6.0"))
(defface cider-stacktrace-filter-inactive-face
'((t (:inherit button :underline nil :weight normal)))
"Face for filter buttons representing frames currently filtered out."
:package-version '(cider . "0.6.0"))
(defface cider-stacktrace-face
'((t (:inherit default)))
"Face for stack frame text."
:package-version '(cider . "0.6.0"))
(defface cider-stacktrace-ns-face
'((t (:inherit font-lock-comment-face)))
"Face for stack frame namespace name."
:package-version '(cider . "0.6.0"))
(defface cider-stacktrace-fn-face
'((t (:inherit default :weight bold)))
"Face for stack frame function name."
:package-version '(cider . "0.6.0"))
(defface cider-stacktrace-promoted-button-face
'((((type graphic))
:box (:line-width 3 :style released-button)
:inherit error)
(t :inverse-video t))
"A button with this face represents a promoted (non-suppressed) error type."
:package-version '(cider . "0.12.0"))
(defface cider-stacktrace-suppressed-button-face
'((((type graphic))
:box (:line-width 3 :style pressed-button)
:inherit widget-inactive)
(t :inverse-video t))
"A button with this face represents a suppressed error type."
:package-version '(cider . "0.12.0"))
;; Colors & Theme Support
(defvar cider-stacktrace-frames-background-color
(cider-scale-background-color)
"Background color for stacktrace frames.")
(advice-add 'enable-theme :after #'cider--stacktrace-adapt-to-theme)
(advice-add 'disable-theme :after #'cider--stacktrace-adapt-to-theme)
(defun cider--stacktrace-adapt-to-theme (&rest _)
"When theme is changed, update `cider-stacktrace-frames-background-color'."
(setq cider-stacktrace-frames-background-color
(cider-scale-background-color)))
;; Mode & key bindings
(defvar cider-stacktrace-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "M-p") #'cider-stacktrace-previous-cause)
(define-key map (kbd "M-n") #'cider-stacktrace-next-cause)
(define-key map (kbd "M-.") #'cider-stacktrace-jump)
(define-key map "q" #'cider-popup-buffer-quit-function)
(define-key map "j" #'cider-stacktrace-toggle-java)
(define-key map "c" #'cider-stacktrace-toggle-clj)
(define-key map "r" #'cider-stacktrace-toggle-repl)
(define-key map "t" #'cider-stacktrace-toggle-tooling)
(define-key map "d" #'cider-stacktrace-toggle-duplicates)
(define-key map "p" #'cider-stacktrace-show-only-project)
(define-key map "a" #'cider-stacktrace-toggle-all)
(define-key map "1" #'cider-stacktrace-cycle-cause-1)
(define-key map "2" #'cider-stacktrace-cycle-cause-2)
(define-key map "3" #'cider-stacktrace-cycle-cause-3)
(define-key map "4" #'cider-stacktrace-cycle-cause-4)
(define-key map "5" #'cider-stacktrace-cycle-cause-5)
(define-key map "0" #'cider-stacktrace-cycle-all-causes)
(define-key map (kbd "TAB") #'cider-stacktrace-cycle-current-cause)
(define-key map [backtab] #'cider-stacktrace-cycle-all-causes)
(easy-menu-define cider-stacktrace-mode-menu map
"Menu for CIDER's stacktrace mode"
'("Stacktrace"
["Previous cause" cider-stacktrace-previous-cause]
["Next cause" cider-stacktrace-next-cause]
"--"
["Jump to frame source" cider-stacktrace-jump]
"--"
["Cycle current cause detail" cider-stacktrace-cycle-current-cause]
["Cycle cause #1 detail" cider-stacktrace-cycle-cause-1]
["Cycle cause #2 detail" cider-stacktrace-cycle-cause-2]
["Cycle cause #3 detail" cider-stacktrace-cycle-cause-3]
["Cycle cause #4 detail" cider-stacktrace-cycle-cause-4]
["Cycle cause #5 detail" cider-stacktrace-cycle-cause-5]
["Cycle all cause detail" cider-stacktrace-cycle-all-causes]
"--"
["Show/hide Java frames" cider-stacktrace-toggle-java]
["Show/hide Clojure frames" cider-stacktrace-toggle-clj]
["Show/hide REPL frames" cider-stacktrace-toggle-repl]
["Show/hide tooling frames" cider-stacktrace-toggle-tooling]
["Show/hide duplicate frames" cider-stacktrace-toggle-duplicates]
["Toggle only project frames" cider-stacktrace-show-only-project]
["Show/hide all frames" cider-stacktrace-toggle-all]))
map))
(define-derived-mode cider-stacktrace-mode special-mode "Stacktrace"
"Major mode for filtering and navigating CIDER stacktraces.
\\{cider-stacktrace-mode-map}"
(when cider-special-mode-truncate-lines
(setq-local truncate-lines t))
(setq-local sesman-system 'CIDER)
(setq-local electric-indent-chars nil)
(setq-local cider-stacktrace-hidden-frame-count 0)
(setq-local cider-stacktrace-filters cider-stacktrace-default-filters)
(setq-local cider-stacktrace-cause-visibility (make-vector 10 0))
(buffer-disable-undo))
;; Stacktrace filtering
(defvar cider-stacktrace--all-negative-filters
'(clj tooling dup java repl)
"Filters that remove stackframes.")
(defvar cider-stacktrace--all-positive-filters
'(project all)
"Filters that ensure stackframes are shown.")
(defun cider-stacktrace--face-for-filter (filter neg-filters pos-filters)
"Return whether we should mark the FILTER is active or not.
NEG-FILTERS and POS-FILTERS are lists of filters to check FILTER's type.
NEG-FILTERS dictate which frames should be hidden while POS-FILTERS can
override this and ensure that those frames are shown."
(cond ((member filter cider-stacktrace--all-negative-filters)
(if (member filter neg-filters)
'cider-stacktrace-filter-active-face
'cider-stacktrace-filter-inactive-face))
((member filter cider-stacktrace--all-positive-filters)
(if (member filter pos-filters)
'cider-stacktrace-filter-active-face
'cider-stacktrace-filter-inactive-face))))
(defun cider-stacktrace-indicate-filters (filters pos-filters)
"Update enabled state of filter buttons.
Find buttons with a 'filter property; if filter is a member of FILTERS, or
if filter is nil ('show all') and the argument list is non-nil, fontify the
button as disabled. Upon finding text with a 'hidden-count property, stop
searching and update the hidden count text. POS-FILTERS is the list of
positive filters to always include."
(with-current-buffer cider-error-buffer
(save-excursion
(goto-char (point-min))
(let ((inhibit-read-only t))
;; Toggle buttons
(while (not (or (get-text-property (point) 'hidden-count) (eobp)))
(let ((button (button-at (point))))
(when button
(let* ((filter (button-get button 'filter))
(face (cider-stacktrace--face-for-filter filter
filters
pos-filters)))
(button-put button 'face face)))
(goto-char (or (next-property-change (point))
(point-max)))))
;; Update hidden count
(when (and (get-text-property (point) 'hidden-count)
(re-search-forward "[0-9]+" (line-end-position) t))
(replace-match
(number-to-string cider-stacktrace-hidden-frame-count)))))))
(defun cider-stacktrace-frame-p ()
"Indicate if the text at point is a stack frame."
(get-text-property (point) 'cider-stacktrace-frame))
(defun cider-stacktrace-collapsed-p ()
"Indicate if the stackframe was collapsed."
(get-text-property (point) 'collapsed))
(defun cider-stacktrace--should-hide-p (neg-filters pos-filters flags)
"Decide whether a stackframe should be hidden or not.
NEG-FILTERS dictate which frames should be hidden while POS-FILTERS can
override this and ensure that those frames are shown.
Argument FLAGS are the flags set on the stackframe, ie: clj dup, etc."
(let ((neg (seq-intersection neg-filters flags))
(pos (seq-intersection pos-filters flags))
(all (memq 'all pos-filters)))
(cond (all nil) ;; if all filter is on then we should not hide
((and pos neg) nil) ;; if hidden and "resurrected" we should not hide
(pos nil)
(neg t)
(t nil))))
(defun cider-stacktrace--apply-filters (neg-filters pos-filters)
"Set visibility on stack frames.
Should be called by `cider-stacktrace-apply-filters' which has the logic of
how to interpret the combinations of the positive and negative filters.
For instance, the presence of the positive filter `project' requires all of
the other negative filters to be applied so that only project frames are
shown. NEG-FILTERS are the tags that should be hidden. POS-FILTERS are
the tags that must be shown."
(with-current-buffer cider-error-buffer
(save-excursion
(goto-char (point-min))
(let ((inhibit-read-only t)
(hidden 0))
(while (not (eobp))
(when (and (cider-stacktrace-frame-p)
(not (cider-stacktrace-collapsed-p)))
(let* ((flags (get-text-property (point) 'flags))
(hide (cider-stacktrace--should-hide-p neg-filters
pos-filters
flags)))
(when hide (cl-incf hidden))
(put-text-property (point) (line-beginning-position 2)
'invisible hide)))
(forward-line 1))
(setq cider-stacktrace-hidden-frame-count hidden)))
(cider-stacktrace-indicate-filters neg-filters pos-filters)))
(defun cider-stacktrace-apply-filters (filters)
"Takes a single list of filters and applies them.
Update `cider-stacktrace-hidden-frame-count' and indicate
filters applied. Currently collapsed stacktraces are ignored, and do not
contribute to the hidden count. FILTERS is the list of filters to be
applied, positive and negative all together. This function defines how
those choices interact and separates them into positive and negative
filters for the resulting machinery."
(let ((neg-filters (seq-intersection filters cider-stacktrace--all-negative-filters))
(pos-filters (seq-intersection filters cider-stacktrace--all-positive-filters)))
;; project and all are mutually exclusive. when both are present we check to
;; see the most recent one (as cons onto the list would put it) and use that
;; interaction.
(cond
((memq 'all (memq 'project pos-filters)) ;; project is most recent
(cider-stacktrace--apply-filters cider-stacktrace--all-negative-filters '(project)))
((memq 'project (memq 'all pos-filters)) ;; all is most recent
(cider-stacktrace--apply-filters nil '(all)))
((memq 'all pos-filters) (cider-stacktrace--apply-filters nil '(all)))
((memq 'project pos-filters) (cider-stacktrace--apply-filters cider-stacktrace--all-negative-filters
pos-filters))
(t (cider-stacktrace--apply-filters neg-filters pos-filters)))))
(defun cider-stacktrace-apply-cause-visibility ()
"Apply `cider-stacktrace-cause-visibility' to causes and reapply filters."
(with-current-buffer cider-error-buffer
(save-excursion
(goto-char (point-min))
(cl-flet ((next-detail (end)
(when-let* ((pos (next-single-property-change (point) 'detail)))
(when (< pos end)
(goto-char pos)))))
(let ((inhibit-read-only t))
;; For each cause...
(while (cider-stacktrace-next-cause)
(let* ((num (get-text-property (point) 'cause))
(level (elt cider-stacktrace-cause-visibility num))
(cause-end (cadr (cider-property-bounds 'cause))))
;; For each detail level within the cause, set visibility.
(while (next-detail cause-end)
(let* ((detail (get-text-property (point) 'detail))
(detail-end (cadr (cider-property-bounds 'detail)))
(hide (if (> detail level) t nil)))
(add-text-properties (point) detail-end
(list 'invisible hide
'collapsed hide))))))))
(cider-stacktrace-apply-filters cider-stacktrace-filters))))
;;; Internal/Middleware error suppression
(defun cider-stacktrace-some-suppressed-errors-p (error-types)
"Return intersection of ERROR-TYPES and CIDER-STACKTRACE-SUPPRESSED-ERRORS.
I.e, Return non-nil if the seq ERROR-TYPES shares any elements with
`cider-stacktrace-suppressed-errors'. This means that even a
'well-behaved' (ie, promoted) error type will be 'guilty by association' if
grouped with a suppressed error type."
(seq-intersection error-types cider-stacktrace-suppressed-errors))
(defun cider-stacktrace-suppress-error (error-type)
"Destructively add ERROR-TYPE to the `cider-stacktrace-suppressed-errors' set."
(setq cider-stacktrace-suppressed-errors
(cl-adjoin error-type cider-stacktrace-suppressed-errors :test 'equal)))
(defun cider-stacktrace-promote-error (error-type)
"Destructively remove ERROR-TYPE from `cider-stacktrace-suppressed-errors'."
(setq cider-stacktrace-suppressed-errors
(remove error-type cider-stacktrace-suppressed-errors)))
(defun cider-stacktrace-suppressed-error-p (error-type)
"Return non-nil if ERROR-TYPE is in `cider-stacktrace-suppressed-errors'."
(member error-type cider-stacktrace-suppressed-errors))
;; Interactive functions
(defun cider-stacktrace-previous-cause ()
"Move point to the previous exception cause, if one exists."
(interactive)
(with-current-buffer cider-error-buffer
(when-let* ((pos (previous-single-property-change (point) 'cause)))
(goto-char pos))))
(defun cider-stacktrace-next-cause ()
"Move point to the next exception cause, if one exists."
(interactive)
(with-current-buffer cider-error-buffer
(when-let* ((pos (next-single-property-change (point) 'cause)))
(goto-char pos))))
(defun cider-stacktrace-cycle-cause (num &optional level)
"Update element NUM of `cider-stacktrace-cause-visibility'.
If LEVEL is specified, it is used, otherwise its current value is incremented.
When it reaches 3, it wraps to 0."
(let ((level (or level (1+ (elt cider-stacktrace-cause-visibility num)))))
(aset cider-stacktrace-cause-visibility num (mod level 3))
(cider-stacktrace-apply-cause-visibility)))
(defun cider-stacktrace-cycle-all-causes ()
"Cycle the visibility of all exception causes."
(interactive)
(with-current-buffer cider-error-buffer
(save-excursion
;; Find nearest cause.
(unless (get-text-property (point) 'cause)
(cider-stacktrace-next-cause)
(unless (get-text-property (point) 'cause)
(cider-stacktrace-previous-cause)))
;; Cycle its level, and apply that to all causes.
(let* ((num (get-text-property (point) 'cause))
(level (1+ (elt cider-stacktrace-cause-visibility num))))
(setq-local cider-stacktrace-cause-visibility
(make-vector 10 (mod level 3)))
(cider-stacktrace-apply-cause-visibility)))))
(defun cider-stacktrace-cycle-current-cause ()
"Cycle the visibility of current exception at point, if any."
(interactive)
(with-current-buffer cider-error-buffer
(when-let* ((num (get-text-property (point) 'cause)))
(cider-stacktrace-cycle-cause num))))
(defun cider-stacktrace-cycle-cause-1 ()
"Cycle the visibility of exception cause #1."
(interactive)
(cider-stacktrace-cycle-cause 1))
(defun cider-stacktrace-cycle-cause-2 ()
"Cycle the visibility of exception cause #2."
(interactive)
(cider-stacktrace-cycle-cause 2))
(defun cider-stacktrace-cycle-cause-3 ()
"Cycle the visibility of exception cause #3."
(interactive)
(cider-stacktrace-cycle-cause 3))
(defun cider-stacktrace-cycle-cause-4 ()
"Cycle the visibility of exception cause #4."
(interactive)
(cider-stacktrace-cycle-cause 4))
(defun cider-stacktrace-cycle-cause-5 ()
"Cycle the visibility of exception cause #5."
(interactive)
(cider-stacktrace-cycle-cause 5))
(defun cider-stacktrace-toggle (flag)
"Update `cider-stacktrace-filters' to add or remove FLAG, and apply filters."
(cider-stacktrace-apply-filters
(setq cider-stacktrace-filters
(if (memq flag cider-stacktrace-filters)
(remq flag cider-stacktrace-filters)
(cons flag cider-stacktrace-filters)))))
(defun cider-stacktrace-toggle-all ()
"Toggle `all' in filter list."
(interactive)
(cider-stacktrace-toggle 'all))
(defun cider-stacktrace-show-only-project ()
"Display only the stackframes from the project."
(interactive)
(cider-stacktrace-toggle 'project))
(defun cider-stacktrace-toggle-java ()
"Toggle display of Java stack frames."
(interactive)
(cider-stacktrace-toggle 'java))
(defun cider-stacktrace-toggle-clj ()
"Toggle display of Clojure stack frames."
(interactive)
(cider-stacktrace-toggle 'clj))
(defun cider-stacktrace-toggle-repl ()
"Toggle display of REPL stack frames."
(interactive)
(cider-stacktrace-toggle 'repl))
(defun cider-stacktrace-toggle-tooling ()
"Toggle display of Tooling stack frames (compiler, nREPL middleware, etc)."
(interactive)
(cider-stacktrace-toggle 'tooling))
(defun cider-stacktrace-toggle-duplicates ()
"Toggle display of stack frames that are duplicates of their descendents."
(interactive)
(cider-stacktrace-toggle 'dup))
;; Text button functions
(defun cider-stacktrace-filter (button)
"Apply filter(s) indicated by the BUTTON."
(with-temp-message "Filters may also be toggled with the keyboard."
(let ((flag (button-get button 'filter)))
(cond ((member flag cider-stacktrace--all-negative-filters)
(cider-stacktrace-toggle flag))
((member flag cider-stacktrace--all-positive-filters)
(cider-stacktrace-show-only-project))
(t (cider-stacktrace-toggle-all))))
(sit-for 5)))
(defun cider-stacktrace-toggle-suppression (button)
"Toggle stacktrace pop-over/pop-under behavior for the `error-type' in BUTTON.
Achieved by destructively manipulating `cider-stacktrace-suppressed-errors'."
(with-current-buffer cider-error-buffer
(let ((inhibit-read-only t)
(suppressed (button-get button 'suppressed))
(error-type (button-get button 'error-type)))
(if suppressed
(progn
(cider-stacktrace-promote-error error-type)
(button-put button 'face 'cider-stacktrace-promoted-button-face)
(button-put button 'help-echo "Click to suppress these stacktraces."))
(cider-stacktrace-suppress-error error-type)
(button-put button 'face 'cider-stacktrace-suppressed-button-face)
(button-put button 'help-echo "Click to promote these stacktraces."))
(button-put button 'suppressed (not suppressed)))))
(defun cider-stacktrace-navigate (button)
"Navigate to the stack frame source represented by the BUTTON."
(let* ((var (button-get button 'var))
(class (button-get button 'class))
(method (button-get button 'method))
(info (or (and var (cider-var-info var))
(and class method (cider-member-info class method))
(nrepl-dict)))
;; Stacktrace returns more accurate line numbers, but if the function's
;; line was unreliable, then so is the stacktrace by the same amount.
;; Set `line-shift' to the number of lines from the beginning of defn.
(line-shift (- (or (button-get button 'line) 0)
(or (nrepl-dict-get info "line") 1)))
(file (or
(nrepl-dict-get info "file")
(button-get button 'file)))
;; give priority to `info` files as `info` returns full paths.
(info (nrepl-dict-put info "file" file)))
(cider--jump-to-loc-from-info info t)
(forward-line line-shift)
(back-to-indentation)))
(declare-function cider-find-var "cider-find")
(defun cider-stacktrace-jump (&optional arg)
"Find definition for stack frame at point, if available.
The prefix ARG and `cider-prompt-for-symbol' decide whether to
prompt and whether to use a new window. Similar to `cider-find-var'."
(interactive "P")
(let ((button (button-at (point))))
(if (and button (button-get button 'line))
(cider-stacktrace-navigate button)
(cider-find-var arg))))
;; Rendering
(defvar cider-use-tooltips)
(defun cider-stacktrace-tooltip (tooltip)
"Return TOOLTIP if `cider-use-tooltips' is set to true, nil otherwise."
(when cider-use-tooltips tooltip))
(defun cider-stacktrace-emit-indented (text &optional indent fill fontify)
"Insert TEXT, and optionally FILL and FONTIFY as clojure the entire block.
INDENT is a string to insert before each line. When INDENT is nil, first
line is not indented and INDENT defaults to a white-spaced string with
length given by `current-column'."
(let ((text (if fontify
(cider-font-lock-as-clojure text)
text))
(do-first indent)
(indent (or indent (make-string (current-column) ? )))
(beg (point)))
(insert text)
(goto-char beg)
(when do-first
(insert indent))
(forward-line)
(while (not (eobp))
(insert indent)
(forward-line))
(when (and fill cider-stacktrace-fill-column)
(when (and (numberp cider-stacktrace-fill-column))
(setq-local fill-column cider-stacktrace-fill-column))
(setq-local fill-prefix indent)
(fill-region beg (point)))))
(defun cider-stacktrace-render-filters (buffer special-filters filters)
"Emit into BUFFER toggle buttons for each of the FILTERS.
SPECIAL-FILTERS are filters that show stack certain stack frames, hiding
others."
(with-current-buffer buffer
(insert " Show: ")
(dolist (filter special-filters)
(insert-text-button (car filter)
'filter (cadr filter)
'follow-link t
'action #'cider-stacktrace-filter
'help-echo (cider-stacktrace-tooltip
(format "Toggle %s stack frames"
(car filter))))
(insert " "))
(insert "\n")
(insert " Hide: ")
(dolist (filter filters)
(insert-text-button (car filter)
'filter (cadr filter)
'follow-link t
'action #'cider-stacktrace-filter
'help-echo (cider-stacktrace-tooltip
(format "Toggle %s stack frames"
(car filter))))
(insert " "))
(let ((hidden "(0 frames hidden)"))
(put-text-property 0 (length hidden) 'hidden-count t hidden)
(insert " " hidden "\n"))))
(defun cider-stacktrace-render-suppression-toggle (buffer error-types)
"Emit toggle buttons for each of the ERROR-TYPES leading this stacktrace BUFFER."
(with-current-buffer buffer
(when error-types
(insert " This is an unexpected CIDER middleware error.\n Please submit a bug report via `")
(insert-text-button "M-x cider-report-bug"
'follow-link t
'action (lambda (_button) (cider-report-bug))
'help-echo (cider-stacktrace-tooltip
"Report bug to the CIDER team."))
(insert "`.\n\n")
(insert "\
If these stacktraces are occurring frequently, consider using the
button(s) below to suppress these types of errors for the duration of
your current CIDER session. The stacktrace buffer will still be
generated, but it will \"pop under\" your current buffer instead of
\"popping over\". The button toggles this behavior.\n\n ")
(dolist (error-type error-types)
(let ((suppressed (cider-stacktrace-suppressed-error-p error-type)))
(insert-text-button (format "%s %s" (if suppressed "Promote" "Suppress") error-type)
'follow-link t
'error-type error-type
'action #'cider-stacktrace-toggle-suppression
'suppressed suppressed
'face (if suppressed
'cider-stacktrace-suppressed-button-face
'cider-stacktrace-promoted-button-face)
'help-echo (cider-stacktrace-tooltip
(format "Click to %s these stacktraces."
(if suppressed "promote" "suppress")))))
(insert " ")))))
(defun cider-stacktrace-render-frame (buffer frame)
"Emit into BUFFER function call site info for the stack FRAME.
This associates text properties to enable filtering and source navigation."
(with-current-buffer buffer
(if (null frame) ;; Probably caused by OmitStackTraceInFastThrow
(let ((url "https://docs.cider.mx/cider/troubleshooting.html#empty-java-stacktraces"))
(insert " No stacktrace available!\n Please see ")
(insert-text-button url
'url url
'follow-link t
'action (lambda (x) (browse-url (button-get x 'url)))))
(nrepl-dbind-response frame (file line flags class method name var ns fn)
(let ((flags (mapcar #'intern flags))) ; strings -> symbols
(insert-text-button (format "%26s:%5d %s/%s"
(if (member 'repl flags) "REPL" file) line
(if (member 'clj flags) ns class)
(if (member 'clj flags) fn method))
'var var 'class class 'method method
'name name 'file file 'line line
'flags flags 'follow-link t
'action #'cider-stacktrace-navigate
'help-echo (cider-stacktrace-tooltip
"View source at this location")
'font-lock-face 'cider-stacktrace-face
'type 'cider-plain-button)
(save-excursion
(let ((p4 (point))
(p1 (search-backward " "))
(p2 (search-forward "/"))
(p3 (search-forward-regexp "[^/$]+")))
(put-text-property p1 p4 'font-lock-face 'cider-stacktrace-ns-face)
(put-text-property p2 p3 'font-lock-face 'cider-stacktrace-fn-face)
(put-text-property (line-beginning-position) (line-end-position)
'cider-stacktrace-frame t)))
(insert "\n"))))))
(defun cider-stacktrace-render-compile-error (buffer cause)
"Emit into BUFFER the compile error CAUSE, and enable jumping to it."
(with-current-buffer buffer
(nrepl-dbind-response cause (file path line column)
(let ((indent " ")
(message-face 'cider-stacktrace-error-message-face))
(insert indent)
(insert (propertize "Error compiling " 'font-lock-face message-face))
(insert-text-button path 'compile-error t
'file file 'line line 'column column 'follow-link t
'action (lambda (_button)
(cider-jump-to (cider-find-file file)
(cons line column)))
'help-echo (cider-stacktrace-tooltip
"Jump to the line that caused the error"))
(insert (propertize (format " at (%d:%d)" line column)
'font-lock-face message-face))))))
(defun cider-stacktrace--toggle-visibility (id)
"Toggle visibility of the region with ID invisibility prop.
ID can also be a button, in which case button's property :id is used
instead. This function can be used directly in button actions."
(let ((id (if (or (numberp id) (symbolp id))
;; There is no proper way to identify buttons. Assuming that
;; id's can be either numbers or symbols.
id
(button-get id :id))))
(if (and (consp buffer-invisibility-spec)
(assoc id buffer-invisibility-spec))
(remove-from-invisibility-spec (cons id t))
(add-to-invisibility-spec (cons id t)))))
(defun cider-stacktrace--insert-named-group (indent name &rest vals)
"Insert named group with the ability to toggle visibility.
NAME is a string naming the group. VALS are strings to be inserted after
the NAME. The whole group is prefixed by string INDENT."
(let* ((str (and vals (replace-regexp-in-string "\n+\\'" "" (apply #'concat vals))))
(id (and str
(string-match "\n" str)
(cl-gensym name))))
(insert indent)
(if id
(let* ((beg-link (string-match "[^ :]" name))
(end-link (string-match "[ :]" name (1+ beg-link))))
(insert (substring name 0 beg-link))
(insert-text-button (substring name beg-link end-link)
:id id
'face '((:weight bold) (:underline t))
'follow-link t
'help-echo "Toggle visibility"
'action #'cider-stacktrace--toggle-visibility)
(insert (substring name end-link)))
(insert (propertize name 'face '((:weight bold)))))
(let ((pos (point)))
(when str
(cider-stacktrace-emit-indented (concat str "\n") nil nil t)
(when id
(remove-from-invisibility-spec (cons id t))
(let ((hide-beg (save-excursion (goto-char pos) (point-at-eol)))
(hide-end (1- (point-at-bol))))
(overlay-put (make-overlay hide-beg hide-end) 'invisible id)))))))
(defun cider-stacktrace--emit-spec-problems (spec-data indent)
"Emit SPEC-DATA indented with INDENT."
(nrepl-dbind-response spec-data (spec value problems)
(insert "\n")
(cider-stacktrace--insert-named-group indent " Spec: " spec)
(cider-stacktrace--insert-named-group indent " Value: " value)
(insert "\n")
(cider-stacktrace--insert-named-group indent "Problems: \n")
(let ((indent2 (concat indent " ")))
(dolist (prob problems)
(nrepl-dbind-response prob (in val predicate reason spec at extra)
(insert "\n")
(when (not (string= val value))
(cider-stacktrace--insert-named-group indent2 " val: " val))
(when in
(cider-stacktrace--insert-named-group indent2 " in: " in))
(cider-stacktrace--insert-named-group indent2 "failed: " predicate)
(when spec
(cider-stacktrace--insert-named-group indent2 " spec: " spec))
(when at
(cider-stacktrace--insert-named-group indent2 " at: " at))
(when reason
(cider-stacktrace--insert-named-group indent2 "reason: " reason))
(when extra
(cider-stacktrace--insert-named-group indent2 "extras: \n")
(cider-stacktrace-emit-indented extra (concat indent2 " ") nil t)))))))
(defun cider-stacktrace-render-cause (buffer cause num note)
"Emit into BUFFER the CAUSE NUM, exception class, message, data, and NOTE."
(with-current-buffer buffer
(nrepl-dbind-response cause (class message data spec stacktrace)
(let ((indent " ")
(class-face 'cider-stacktrace-error-class-face)
(message-face 'cider-stacktrace-error-message-face))
(cider-propertize-region `(cause ,num)
;; Detail level 0: exception class
(cider-propertize-region '(detail 0)
(insert (format "%d. " num)
(propertize note 'font-lock-face 'font-lock-comment-face) " "
(propertize class 'font-lock-face class-face)
"\n"))
;; Detail level 1: message + ex-data
(cider-propertize-region '(detail 1)
(if (equal class "clojure.lang.Compiler$CompilerException")
(cider-stacktrace-render-compile-error buffer cause)
(cider-stacktrace-emit-indented
(propertize (or message "(No message)")
'font-lock-face message-face)
indent t))
(insert "\n")
(when spec
(cider-stacktrace--emit-spec-problems spec (concat indent " ")))
(when data
(cider-stacktrace-emit-indented data indent nil t)))
;; Detail level 2: stacktrace
(cider-propertize-region '(detail 2)
(insert "\n")
(let ((beg (point))
(bg `(:background ,cider-stacktrace-frames-background-color :extend t)))
(dolist (frame stacktrace)
(cider-stacktrace-render-frame buffer frame))
(overlay-put (make-overlay beg (point)) 'font-lock-face bg)))
;; Add line break between causes, even when collapsed.
(cider-propertize-region '(detail 0)
(insert "\n")))))))
(defun cider-stacktrace-initialize (causes)
"Set and apply CAUSES initial visibility, filters, and cursor position."
(nrepl-dbind-response (car causes) (class)
(let ((compile-error-p (equal class "clojure.lang.Compiler$CompilerException")))
;; Partially display outermost cause if it's a compiler exception (the
;; description reports reader location of the error).
(when compile-error-p
(cider-stacktrace-cycle-cause (length causes) 1))
;; Fully display innermost cause. This also applies visibility/filters.
(cider-stacktrace-cycle-cause 1 cider-stacktrace-detail-max)
;; Move point (DWIM) to the compile error location if present, or to the
;; first stacktrace frame in displayed cause otherwise. If the error
;; buffer is visible in a window, ensure that window is selected while moving
;; point, so as to move both the buffer's and the window's point.
(with-selected-window (or (get-buffer-window cider-error-buffer)
(selected-window))
(with-current-buffer cider-error-buffer
(goto-char (point-min))
(if compile-error-p
(goto-char (next-single-property-change (point) 'compile-error))
(progn
(while (cider-stacktrace-next-cause))
(goto-char (next-single-property-change (point) 'flags)))))))))
(defun cider-stacktrace-render (buffer causes &optional error-types)
"Emit into BUFFER useful stacktrace information for the CAUSES.
Takes an optional ERROR-TYPES list which will render a 'suppression' toggle
that alters the pop-over/pop-under behavorior of the stacktrace buffers
created by these types of errors. The suppressed errors set can be customized
through the `cider-stacktrace-suppressed-errors' variable."
(with-current-buffer buffer
(let ((inhibit-read-only t))
(erase-buffer)
(insert "\n")
;; Stacktrace filters
(cider-stacktrace-render-filters
buffer
`(("Project-Only" project) ("All" all))
`(("Clojure" clj) ("Java" java) ("REPL" repl)
("Tooling" tooling) ("Duplicates" dup)))
(insert "\n")
;; Option to suppress internal/middleware errors
(when error-types
(cider-stacktrace-render-suppression-toggle buffer error-types)
(insert "\n\n"))
;; Stacktrace exceptions & frames
(let ((num (length causes)))
(dolist (cause causes)
(let ((note (if (= num (length causes)) "Unhandled" "Caused by")))
(cider-stacktrace-render-cause buffer cause num note)
(setq num (1- num))))))
(cider-stacktrace-initialize causes)
(font-lock-refresh-defaults)))
(provide 'cider-stacktrace)
;;; cider-stacktrace.el ends here
;;; cider-test.el --- Test result viewer -*- lexical-binding: t -*-
;; Copyright © 2014-2022 Jeff Valk, Bozhidar Batsov and CIDER contributors
;; Author: Jeff Valk <jv@jeffvalk.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; This provides execution, reporting, and navigation support for Clojure tests,
;; specifically using the `clojure.test' machinery. This functionality replaces
;; the venerable `clojure-test-mode' (deprecated in June 2014), and relies on
;; nREPL middleware for report running and session support.
;;; Code:
(require 'ansi-color)
(require 'button)
(require 'cl-lib)
(require 'easymenu)
(require 'map)
(require 'seq)
(require 'subr-x)
(require 'cider-common)
(require 'cider-client)
(require 'cider-popup)
(require 'cider-stacktrace)
(require 'cider-overlays)
;;; Variables
(defgroup cider-test nil
"Presentation and navigation for test results."
:prefix "cider-test-"
:group 'cider)
(defcustom cider-test-show-report-on-success nil
"Whether to show the `*cider-test-report*` buffer on passing tests."
:type 'boolean
:package-version '(cider . "0.8.0"))
(defcustom cider-auto-select-test-report-buffer t
"Determines if the test-report buffer should be auto-selected."
:type 'boolean
:package-version '(cider . "0.9.0"))
(defcustom cider-test-defining-forms '("deftest" "defspec")
"Forms that define individual tests.
CIDER considers the \"top-level\" form around point to define a test if
the form starts with one of these forms.
Add to this list to have CIDER recognize additional test defining macros."
:type '(repeat string)
:package-version '(cider . "0.15.0"))
(defvar cider-test-last-summary nil
"The summary of the last run test.")
(defvar cider-test-last-results nil
"The results of the last run test.")
(defconst cider-test-report-buffer "*cider-test-report*"
"Buffer name in which to display test reports.")
;;; Faces
(defface cider-test-failure-face
'((((class color) (background light))
:background "orange red")
(((class color) (background dark))
:background "firebrick"))
"Face for failed tests."
:package-version '(cider . "0.7.0"))
(defface cider-test-error-face
'((((class color) (background light))
:background "orange1")
(((class color) (background dark))
:background "orange4"))
"Face for erring tests."
:package-version '(cider . "0.7.0"))
(defface cider-test-success-face
'((((class color) (background light))
:foreground "black"
:background "green")
(((class color) (background dark))
:foreground "black"
:background "green"))
"Face for passing tests."
:package-version '(cider . "0.7.0"))
;; Colors & Theme Support
(defvar cider-test-items-background-color
(cider-scale-background-color)
"Background color for test assertion items.")
(advice-add 'enable-theme :after #'cider--test-adapt-to-theme)
(advice-add 'disable-theme :after #'cider--test-adapt-to-theme)
(defun cider--test-adapt-to-theme (&rest _)
"When theme is changed, update `cider-test-items-background-color'."
(setq cider-test-items-background-color (cider-scale-background-color)))
;;; Report mode & key bindings
;;
;; The primary mode of interacting with test results is the report buffer, which
;; allows navigation among tests, jumping to test definitions, expected/actual
;; diff-ing, and cause/stacktrace inspection for test errors.
(defvar cider-test-commands-map
(let ((map (define-prefix-command 'cider-test-commands-map)))
;; Duplicates of keys below with C- for convenience
(define-key map (kbd "C-r") #'cider-test-rerun-failed-tests)
(define-key map (kbd "C-t") #'cider-test-run-test)
(define-key map (kbd "C-a") #'cider-test-rerun-test)
(define-key map (kbd "C-n") #'cider-test-run-ns-tests)
(define-key map (kbd "C-s") #'cider-test-run-ns-tests-with-filters)
(define-key map (kbd "C-l") #'cider-test-run-loaded-tests)
(define-key map (kbd "C-p") #'cider-test-run-project-tests)
(define-key map (kbd "C-b") #'cider-test-show-report)
;; Single-key bindings defined last for display in menu
(define-key map (kbd "r") #'cider-test-rerun-failed-tests)
(define-key map (kbd "t") #'cider-test-run-test)
(define-key map (kbd "a") #'cider-test-rerun-test)
(define-key map (kbd "n") #'cider-test-run-ns-tests)
(define-key map (kbd "s") #'cider-test-run-ns-tests-with-filters)
(define-key map (kbd "l") #'cider-test-run-loaded-tests)
(define-key map (kbd "p") #'cider-test-run-project-tests)
(define-key map (kbd "b") #'cider-test-show-report)
map))
(defconst cider-test-menu
'("Test"
["Run test" cider-test-run-test]
["Run namespace tests" cider-test-run-ns-tests]
["Run namespace tests with filters" cider-test-run-ns-tests-with-filters]
["Run all loaded tests" cider-test-run-loaded-tests]
["Run all loaded tests with filters" (apply-partially cider-test-run-loaded-tests 'prompt-for-filters)]
["Run all project tests" cider-test-run-project-tests]
["Run all project tests with filters" (apply-partially cider-test-run-project-tests 'prompt-for-filters)]
["Run tests after load-file" cider-auto-test-mode
:style toggle :selected cider-auto-test-mode]
"--"
["Interrupt running tests" cider-interrupt]
["Rerun failed/erring tests" cider-test-rerun-failed-tests]
["Show test report" cider-test-show-report]
"--"
["Configure testing" (customize-group 'cider-test)])
"CIDER test submenu.")
(defvar cider-test-report-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c ,") 'cider-test-commands-map)
(define-key map (kbd "C-c C-t") 'cider-test-commands-map)
(define-key map (kbd "M-p") #'cider-test-previous-result)
(define-key map (kbd "M-n") #'cider-test-next-result)
(define-key map (kbd "M-.") #'cider-test-jump)
(define-key map (kbd "<backtab>") #'cider-test-previous-result)
(define-key map (kbd "TAB") #'cider-test-next-result)
(define-key map (kbd "RET") #'cider-test-jump)
(define-key map (kbd "t") #'cider-test-jump)
(define-key map (kbd "d") #'cider-test-ediff)
(define-key map (kbd "e") #'cider-test-stacktrace)
;; `f' for "run failed".
(define-key map "f" #'cider-test-rerun-failed-tests)
(define-key map "n" #'cider-test-run-ns-tests)
(define-key map "s" #'cider-test-run-ns-tests-with-filters)
(define-key map "l" #'cider-test-run-loaded-tests)
(define-key map "p" #'cider-test-run-project-tests)
;; `g' generally reloads the buffer. The closest thing we have to that is
;; "run the test at point". But it's not as nice as rerunning all tests in
;; this buffer.
(define-key map "g" #'cider-test-run-test)
(define-key map "q" #'cider-popup-buffer-quit-function)
(easy-menu-define cider-test-report-mode-menu map
"Menu for CIDER's test result mode"
'("Test-Report"
["Previous result" cider-test-previous-result]
["Next result" cider-test-next-result]
"--"
["Rerun current test" cider-test-run-test]
["Rerun failed/erring tests" cider-test-rerun-failed-tests]
["Run all ns tests" cider-test-run-ns-tests]
["Run all ns tests with filters" cider-test-run-ns-tests-with-filters]
["Run all loaded tests" cider-test-run-loaded-tests]
["Run all loaded tests with filters" (apply-partially cider-test-run-loaded-tests 'prompt-for-filters)]
["Run all project tests" cider-test-run-project-tests]
["Run all project tests with filters" (apply-partially cider-test-run-project-tests 'prompt-for-filters)]
"--"
["Jump to test definition" cider-test-jump]
["Display test error" cider-test-stacktrace]
["Display expected/actual diff" cider-test-ediff]))
map))
(define-derived-mode cider-test-report-mode fundamental-mode "Test Report"
"Major mode for presenting Clojure test results.
\\{cider-test-report-mode-map}"
(setq buffer-read-only t)
(when cider-special-mode-truncate-lines
(setq-local truncate-lines t))
(setq-local sesman-system 'CIDER)
(setq-local electric-indent-chars nil)
(buffer-disable-undo))
;; Report navigation
(defun cider-test-show-report ()
"Show the test report buffer, if one exists."
(interactive)
(if-let* ((report-buffer (get-buffer cider-test-report-buffer)))
(switch-to-buffer report-buffer)
(message "No test report buffer")))
(defun cider-test-previous-result ()
"Move point to the previous test result, if one exists."
(interactive)
(with-current-buffer (get-buffer cider-test-report-buffer)
(when-let* ((pos (previous-single-property-change (point) 'type)))
(if (get-text-property pos 'type)
(goto-char pos)
(when-let* ((pos (previous-single-property-change pos 'type)))
(goto-char pos))))))
(defun cider-test-next-result ()
"Move point to the next test result, if one exists."
(interactive)
(with-current-buffer (get-buffer cider-test-report-buffer)
(when-let* ((pos (next-single-property-change (point) 'type)))
(if (get-text-property pos 'type)
(goto-char pos)
(when-let* ((pos (next-single-property-change pos 'type)))
(goto-char pos))))))
(declare-function cider-find-var "cider-find")
(defun cider-test-jump (&optional arg)
"Find definition for test at point, if available.
The prefix ARG and `cider-prompt-for-symbol' decide whether to
prompt and whether to use a new window. Similar to `cider-find-var'."
(interactive "P")
(let ((ns (get-text-property (point) 'ns))
(var (get-text-property (point) 'var))
(line (get-text-property (point) 'line)))
(if (and ns var)
(cider-find-var arg (concat ns "/" var) line)
(cider-find-var arg))))
;;; Error stacktraces
(defvar cider-auto-select-error-buffer)
(defun cider-test-stacktrace-for (ns var index)
"Display stacktrace for the erring NS VAR test with the assertion INDEX."
(let (causes)
(cider-nrepl-send-request
(thread-last
(map-merge 'list
`(("op" "test-stacktrace")
("ns" ,ns)
("var" ,var)
("index" ,index))
(cider--nrepl-print-request-map fill-column))
(seq-mapcat #'identity))
(lambda (response)
(nrepl-dbind-response response (class status)
(cond (class (setq causes (cons response causes)))
(status (when causes
(cider-stacktrace-render
(cider-popup-buffer cider-error-buffer
cider-auto-select-error-buffer
#'cider-stacktrace-mode
'ancillary)
(reverse causes))))))))))
(defun cider-test-stacktrace ()
"Display stacktrace for the erring test at point."
(interactive)
(let ((ns (get-text-property (point) 'ns))
(var (get-text-property (point) 'var))
(index (get-text-property (point) 'index))
(err (get-text-property (point) 'error)))
(if (and err ns var index)
(cider-test-stacktrace-for ns var index)
(message "No test error at point"))))
;;; Expected vs actual diffing
(defvar cider-test-ediff-buffers nil
"The expected/actual buffers used to display diff.")
(defun cider-test--extract-from-actual (actual n)
"Extract form N from ACTUAL, ignoring outermost not.
ACTUAL is a string like \"(not (= 3 4))\", of the sort returned by
clojure.test.
N = 1 => 3, N = 2 => 4, etc."
(with-temp-buffer
(insert actual)
(clojure-mode)
(goto-char (point-min))
(re-search-forward "(" nil t 2)
(clojure-forward-logical-sexp n)
(forward-whitespace 1)
(let ((beg (point)))
(clojure-forward-logical-sexp)
(buffer-substring beg (point)))))
(defun cider-test-ediff ()
"Show diff of the expected vs actual value for the test at point.
With the actual value, the outermost '(not ...)' s-expression is removed."
(interactive)
(let* ((expected-buffer (generate-new-buffer " *expected*"))
(actual-buffer (generate-new-buffer " *actual*"))
(diffs (get-text-property (point) 'diffs))
(actual* (get-text-property (point) 'actual))
(expected (cond (diffs (get-text-property (point) 'expected))
(actual* (cider-test--extract-from-actual actual* 1))))
(actual (cond (diffs (caar diffs))
(actual* (cider-test--extract-from-actual actual* 2)))))
(if (not (and expected actual))
(message "No test failure at point")
(with-current-buffer expected-buffer
(insert expected)
(clojure-mode))
(with-current-buffer actual-buffer
(insert actual)
(clojure-mode))
(apply #'ediff-buffers
(setq cider-test-ediff-buffers
(list (buffer-name expected-buffer)
(buffer-name actual-buffer)))))))
(defun cider-test-ediff-cleanup ()
"Cleanup expected/actual buffers used for diff."
(interactive)
(mapc (lambda (b) (when (get-buffer b) (kill-buffer b)))
cider-test-ediff-buffers))
(add-hook 'ediff-cleanup-hook #'cider-test-ediff-cleanup)
;;; Report rendering
(defun cider-test-type-face (type)
"Return the font lock face for the test result TYPE."
(pcase type
("pass" 'cider-test-success-face)
("fail" 'cider-test-failure-face)
("error" 'cider-test-error-face)
(_ 'default)))
(defun cider-test-type-simple-face (type)
"Return a face for the test result TYPE using the highlight color as foreground."
(let ((face (cider-test-type-face type)))
`(:foreground ,(face-attribute face :background))))
(defun cider-test-render-summary (buffer summary)
"Emit into BUFFER the report SUMMARY statistics."
(with-current-buffer buffer
(nrepl-dbind-response summary (ns var test pass fail error)
(insert (format "Tested %d namespaces\n" ns))
(insert (format "Ran %d assertions, in %d test functions\n" test var))
(unless (zerop fail)
(cider-insert (format "%d failures" fail) 'cider-test-failure-face t))
(unless (zerop error)
(cider-insert (format "%d errors" error) 'cider-test-error-face t))
(when (zerop (+ fail error))
(cider-insert (format "%d passed" pass) 'cider-test-success-face t))
(insert "\n\n"))))
(defun cider-test-render-assertion (buffer test)
"Emit into BUFFER report detail for the TEST assertion."
(with-current-buffer buffer
(nrepl-dbind-response test (var context type message expected actual diffs error gen-input)
(cl-flet ((insert-label (s)
(cider-insert (format "%8s: " s) 'font-lock-comment-face))
(insert-align-label (s)
(insert (format "%12s" s)))
(insert-rect (s)
(let ((start (point)))
(insert-rectangle (thread-first
s
cider-font-lock-as-clojure
(split-string "\n")))
(ansi-color-apply-on-region start (point)))
(beginning-of-line)))
(cider-propertize-region (cider-intern-keys (cdr test))
(let ((beg (point))
(type-face (cider-test-type-simple-face type))
(bg `(:background ,cider-test-items-background-color :extend t)))
(cider-insert (capitalize type) type-face nil " in ")
(cider-insert var 'font-lock-function-name-face t)
(when context (cider-insert context 'font-lock-doc-face t))
(when message (cider-insert message 'font-lock-string-face t))
(when expected
(insert-label "expected")
(insert-rect expected)
(insert "\n"))
(if diffs
(dolist (d diffs)
(cl-destructuring-bind (actual (removed added)) d
(insert-label "actual")
(insert-rect actual)
(insert-label "diff")
(insert "- ")
(insert-rect removed)
(insert-align-label "+ ")
(insert-rect added)
(insert "\n")))
(when actual
(insert-label "actual")
(insert-rect actual)))
(when error
(insert-label "error")
(insert-text-button error
'follow-link t
'action '(lambda (_button) (cider-test-stacktrace))
'help-echo "View causes and stacktrace")
(insert "\n"))
(when gen-input
(insert-label "input")
(insert (cider-font-lock-as-clojure gen-input)))
(overlay-put (make-overlay beg (point)) 'font-lock-face bg))
(insert "\n"))))))
(defun cider-test-non-passing (tests)
"For a list of TESTS, each an `nrepl-dict`, return only those that did not pass."
(seq-filter (lambda (test)
(unless (equal (nrepl-dict-get test "type") "pass")
test))
tests))
(defun cider-test-render-report (buffer summary results)
"Emit into BUFFER the report for the SUMMARY, and test RESULTS."
(with-current-buffer buffer
(let ((inhibit-read-only t))
(cider-test-report-mode)
(cider-insert "Test Summary" 'bold t)
(dolist (ns (nrepl-dict-keys results))
(insert (cider-propertize ns 'ns) "\n"))
(cider-insert "\n")
(cider-test-render-summary buffer summary)
(nrepl-dbind-response summary (fail error)
(unless (zerop (+ fail error))
(cider-insert "Results" 'bold t "\n")
;; Results are a nested dict, keyed first by ns, then var. Within each
;; var is a sequence of test assertion results.
(nrepl-dict-map
(lambda (ns vars)
(nrepl-dict-map
(lambda (_var tests)
(let* ((problems (cider-test-non-passing tests))
(count (length problems)))
(when (< 0 count)
(insert (format "%s\n%d non-passing tests:\n\n"
(cider-propertize ns 'ns) count))
(dolist (test problems)
(cider-test-render-assertion buffer test)))))
vars))
results)))
(goto-char (point-min))
(current-buffer))))
;;; Message echo
(defun cider-test-echo-running (ns &optional test)
"Echo a running message for the test NS, which may be a keyword.
The optional arg TEST denotes an individual test name."
(if test
(message "Running test %s in %s..."
(cider-propertize test 'bold)
(cider-propertize ns 'ns))
(message "Running tests in %s..."
(concat (cider-propertize
(cond ((stringp ns) ns)
((eq :non-passing ns) "failing")
((eq :loaded ns) "all loaded")
((eq :project ns) "all project"))
'ns)
(unless (stringp ns) " namespaces")))))
(defun cider-test-echo-summary (summary results)
"Echo SUMMARY statistics for a test run returning RESULTS."
(nrepl-dbind-response summary (ns test var fail error)
(if (nrepl-dict-empty-p results)
(message (concat (propertize "No assertions (or no tests) were run." 'face 'cider-test-error-face)
"Did you forget to use `is' in your tests?"))
(message (propertize
"%sRan %d assertions, in %d test functions. %d failures, %d errors."
'face (cond ((not (zerop error)) 'cider-test-error-face)
((not (zerop fail)) 'cider-test-failure-face)
(t 'cider-test-success-face)))
(concat (if (= 1 ns) ; ns count from summary
(cider-propertize (car (nrepl-dict-keys results)) 'ns)
(propertize (format "%d namespaces" ns) 'face 'default))
(propertize ": " 'face 'default))
test var fail error))))
;;; Test definition highlighting
;;
;; On receipt of test results, failing/erring test definitions are highlighted.
;; Highlights are cleared on the next report run, and may be cleared manually
;; by the user.
;; NOTE If keybindings specific to test sources are desired, it would be
;; straightforward to turn this into a `cider-test-mode' minor mode, which we
;; enable on test sources, much like the legacy `clojure-test-mode'. At present,
;; though, there doesn't seem to be much value in this, since the report buffer
;; provides the primary means of interacting with test results.
(defun cider-test-highlight-problem (buffer test)
"Highlight the BUFFER test definition for the non-passing TEST."
(with-current-buffer buffer
;; we don't need the file name here, as we always operate on the current
;; buffer and the line data is correct even for vars that were
;; defined interactively
(nrepl-dbind-response test (type line message expected actual)
(when line
(save-excursion
(goto-char (point-min))
(forward-line (1- line))
(search-forward "(" nil t)
(let ((beg (point)))
(forward-sexp)
(cider--make-overlay beg (point) 'cider-test
'font-lock-face (cider-test-type-face type)
'type type
'help-echo message
'message message
'expected expected
'actual actual)))))))
(defun cider-find-var-file (ns var)
"Return the buffer visiting the file in which the NS VAR is defined.
Or nil if not found."
(when-let* ((info (cider-var-info (concat ns "/" var)))
(file (nrepl-dict-get info "file")))
(cider-find-file file)))
(defun cider-test-highlight-problems (results)
"Highlight all non-passing tests in the test RESULTS."
(nrepl-dict-map
(lambda (ns vars)
(nrepl-dict-map
(lambda (var tests)
(when-let* ((buffer (cider-find-var-file ns var)))
(dolist (test tests)
(nrepl-dbind-response test (type)
(unless (equal "pass" type)
(cider-test-highlight-problem buffer test))))))
vars))
results))
(defun cider-test-clear-highlights ()
"Clear highlighting of non-passing tests from the last test run."
(interactive)
(when cider-test-last-results
(nrepl-dict-map
(lambda (ns vars)
(dolist (var (nrepl-dict-keys vars))
(when-let* ((buffer (cider-find-var-file ns var)))
(with-current-buffer buffer
(remove-overlays nil nil 'category 'cider-test)))))
cider-test-last-results)))
;;; Test namespaces
;;
;; Test namespace inference exists to enable DWIM test running functions: the
;; same "run-tests" function should be able to be used in a source file, and in
;; its corresponding test namespace. To provide this, we need to map the
;; relationship between those namespaces.
(defcustom cider-test-infer-test-ns 'cider-test-default-test-ns-fn
"Function to infer the test namespace for NS.
The default implementation uses the simple Leiningen convention of appending
'-test' to the namespace name."
:type 'symbol
:package-version '(cider . "0.7.0"))
(defun cider-test-default-test-ns-fn (ns)
"For a NS, return the test namespace, which may be the argument itself.
This uses the Leiningen convention of appending '-test' to the namespace name."
(when ns
(let ((suffix "-test"))
(if (string-suffix-p suffix ns)
ns
(concat ns suffix)))))
;;; Test execution
(defcustom cider-test-default-include-selectors '()
"List of include selector strings to use when executing tests if none provided."
:type '(repeat string)
:package-version '(cider . "1.1.0"))
(defcustom cider-test-default-exclude-selectors '()
"List of exclude selector strings to use when executing tests if none provided."
:type '(repeat string)
:package-version '(cider . "1.1.0"))
(declare-function cider-emit-interactive-eval-output "cider-eval")
(declare-function cider-emit-interactive-eval-err-output "cider-eval")
(defun cider-test--prompt-for-selectors (message)
"Prompt for test selectors with MESSAGE.
The selectors can be either keywords or strings."
(mapcar
(lambda (string) (replace-regexp-in-string "^:+" "" string))
(split-string
(cider-read-from-minibuffer message))))
(defun cider-test-execute (ns &optional tests silent prompt-for-filters)
"Run tests for NS, which may be a keyword, optionally specifying TESTS.
This tests a single NS, or multiple namespaces when using keywords `:project',
`:loaded' or `:non-passing'. Optional TESTS are only honored when a single
namespace is specified. Upon test completion, results are echoed and a test
report is optionally displayed. When test failures/errors occur, their sources
are highlighted.
If SILENT is non-nil, suppress all messages other then test results.
If PROMPT-FOR-FILTERS is non-nil, prompt the user for a test selector filters.
The include/exclude selectors will be used to filter the tests before
running them."
(cider-test-clear-highlights)
(let ((include-selectors
(if prompt-for-filters
(cider-test--prompt-for-selectors
"Test selectors to include (space separated): ")
cider-test-default-include-selectors))
(exclude-selectors
(if prompt-for-filters
(cider-test--prompt-for-selectors
"Test selectors to exclude (space separated): ")
cider-test-default-exclude-selectors)))
(cider-map-repls :clj-strict
(lambda (conn)
(unless silent
(if (and tests (= (length tests) 1))
;; we generate a different message when running individual tests
(cider-test-echo-running ns (car tests))
(cider-test-echo-running ns)))
(let ((request `("op" ,(cond ((stringp ns) "test")
((eq :project ns) "test-all")
((eq :loaded ns) "test-all")
((eq :non-passing ns) "retest")))))
;; we add optional parts of the request only when relevant
(when (and (listp include-selectors) include-selectors)
(setq request (append request `("include" ,include-selectors))))
(when (and (listp exclude-selectors) exclude-selectors)
(setq request (append request `("exclude" ,exclude-selectors))))
(when (stringp ns)
(setq request (append request `("ns" ,ns))))
(when (stringp ns)
(setq request (append request `("tests" ,tests))))
(when (or (stringp ns) (eq :project ns))
(setq request (append request `("load?" ,"true"))))
(cider-nrepl-send-request
request
(lambda (response)
(nrepl-dbind-response response (summary results status out err)
(cond ((member "namespace-not-found" status)
(unless silent
(message "No test namespace: %s" (cider-propertize ns 'ns))))
(out (cider-emit-interactive-eval-output out))
(err (cider-emit-interactive-eval-err-output err))
(results
(nrepl-dbind-response summary (error fail)
(setq cider-test-last-summary summary)
(setq cider-test-last-results results)
(cider-test-highlight-problems results)
(cider-test-echo-summary summary results)
(if (or (not (zerop (+ error fail)))
cider-test-show-report-on-success)
(cider-test-render-report
(cider-popup-buffer
cider-test-report-buffer
cider-auto-select-test-report-buffer)
summary
results)
(when (get-buffer cider-test-report-buffer)
(with-current-buffer cider-test-report-buffer
(let ((inhibit-read-only t))
(erase-buffer)))
(cider-test-render-report
cider-test-report-buffer
summary results))))))))
conn))))))
(defun cider-test-rerun-failed-tests ()
"Rerun failed and erring tests from the last test run."
(interactive)
(if cider-test-last-summary
(nrepl-dbind-response cider-test-last-summary (fail error)
(if (not (zerop (+ error fail)))
(cider-test-execute :non-passing)
(message "No prior failures to retest")))
(message "No prior results to retest")))
(defun cider-test-run-loaded-tests (prompt-for-filters)
"Run all tests defined in currently loaded namespaces.
If PROMPT-FOR-FILTERS is non-nil, prompt the user for a test selectors to
filter the tests with."
(interactive "P")
(cider-test-execute :loaded nil nil prompt-for-filters))
(defun cider-test-run-project-tests (prompt-for-filters)
"Run all tests defined in all project namespaces, loading these as needed.
If PROMPT-FOR-FILTERS is non-nil, prompt the user for a test selectors to
filter the tests with."
(interactive "P")
(cider-test-execute :project nil nil prompt-for-filters))
(defun cider-test-run-ns-tests-with-filters (suppress-inference)
"Run tests filtered by selectors for the current Clojure namespace context.
With a prefix arg SUPPRESS-INFERENCE it will try to run the tests in the
current ns."
(interactive "P")
(cider-test-run-ns-tests suppress-inference nil 't))
(defun cider-test-run-ns-tests (suppress-inference &optional silent prompt-for-filters)
"Run all tests for the current Clojure namespace context.
If SILENT is non-nil, suppress all messages other then test results.
With a prefix arg SUPPRESS-INFERENCE it will try to run the tests in the
current ns. If PROMPT-FOR-FILTERS is non-nil, prompt the user for
test selectors to filter the tests with."
(interactive "P")
(if-let* ((ns (if suppress-inference
(cider-current-ns t)
(funcall cider-test-infer-test-ns (cider-current-ns t)))))
(cider-test-execute ns nil silent prompt-for-filters)
(if (eq major-mode 'cider-test-report-mode)
(when (y-or-n-p (concat "Test report does not define a namespace. "
"Rerun failed/erring tests?"))
(cider-test-rerun-failed-tests))
(unless silent
(message "No namespace to test in current context")))))
(defvar cider-test-last-test-ns nil
"The ns of the last test ran with `cider-test-run-test'.")
(defvar cider-test-last-test-var nil
"The var of the last test ran with `cider-test-run-test'.")
(defun cider-test-update-last-test (ns var)
"Update the last test by setting NS and VAR.
See `cider-test-rerun-test'."
(setq cider-test-last-test-ns ns
cider-test-last-test-var var))
(defun cider-test-run-test ()
"Run the test at point.
The test ns/var exist as text properties on report items and on highlighted
failed/erred test definitions. When not found, a test definition at point
is searched."
(interactive)
(let ((ns (get-text-property (point) 'ns))
(var (get-text-property (point) 'var)))
(if (and ns var)
;; we're in a `cider-test-report-mode' buffer
;; or on a highlighted failed/erred test definition
(progn
(cider-test-update-last-test ns var)
(cider-test-execute ns (list var)))
;; we're in a `clojure-mode' buffer
(let* ((ns (clojure-find-ns))
(def (clojure-find-def)) ; it's a list of the form (deftest something)
(deftype (car def))
(var (cadr def)))
(if (and ns (member deftype cider-test-defining-forms))
(progn
(cider-test-update-last-test ns (list var))
(cider-test-execute ns (list var)))
(message "No test at point"))))))
(defun cider-test-rerun-test ()
"Re-run the test that was previously ran."
(interactive)
(if (and cider-test-last-test-ns cider-test-last-test-var)
(cider-test-execute cider-test-last-test-ns cider-test-last-test-var)
(user-error "No test to re-run")))
;;; Auto-test mode
(defun cider--test-silently ()
"Like `cider-test-run-tests', but with less feedback.
Only notify the user if there actually were any tests to run and only after
the results are received."
(when (cider-connected-p)
(let ((cider-auto-select-test-report-buffer nil)
(cider-test-show-report-on-success nil))
(cider-test-run-ns-tests nil 'soft))))
;;;###autoload
(define-minor-mode cider-auto-test-mode
"Toggle automatic testing of Clojure files.
When enabled this reruns tests every time a Clojure file is loaded.
Only runs tests corresponding to the loaded file's namespace and does
nothing if no tests are defined or if the file failed to load."
:init-value nil :lighter (cider-mode " Test") :keymap nil
:global t
(if cider-auto-test-mode
(add-hook 'cider-file-loaded-hook #'cider--test-silently)
(remove-hook 'cider-file-loaded-hook #'cider--test-silently)))
(provide 'cider-test)
;;; cider-test.el ends here
;;; cider-tracing.el --- Executing tracing functionality -*- lexical-binding: t -*-
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; A couple of commands for tracing the execution of functions.
;;; Code:
(require 'cider-client)
(require 'cider-common) ; for `cider-prompt-for-symbol-function'
(require 'cider-util) ; for `cider-propertize'
(require 'cider-connection) ; for `cider-map-repls'
(require 'nrepl-dict)
(defun cider-sync-request:toggle-trace-var (sym)
"Toggle var tracing for SYM."
(thread-first `("op" "toggle-trace-var"
"ns" ,(cider-current-ns)
"sym" ,sym)
(cider-nrepl-send-sync-request)))
(defun cider--toggle-trace-var (sym)
"Toggle var tracing for SYM."
(let* ((trace-response (cider-sync-request:toggle-trace-var sym))
(var-name (nrepl-dict-get trace-response "var-name"))
(var-status (nrepl-dict-get trace-response "var-status")))
(pcase var-status
("not-found" (error "Var %s not found" (cider-propertize sym 'fn)))
("not-traceable" (error "Var %s can't be traced because it's not bound to a function" (cider-propertize var-name 'fn)))
(_ (message "Var %s %s" (cider-propertize var-name 'fn) var-status)))))
;;;###autoload
(defun cider-toggle-trace-var (arg)
"Toggle var tracing.
Prompts for the symbol to use, or uses the symbol at point, depending on
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
opposite of what that option dictates."
(interactive "P")
(cider-ensure-op-supported "toggle-trace-var")
(funcall (cider-prompt-for-symbol-function arg)
"Toggle trace for var"
#'cider--toggle-trace-var))
(defun cider-sync-request:toggle-trace-ns (ns)
"Toggle namespace tracing for NS."
(thread-first `("op" "toggle-trace-ns"
"ns" ,ns)
(cider-nrepl-send-sync-request)))
;;;###autoload
(defun cider-toggle-trace-ns (query)
"Toggle ns tracing.
Defaults to the current ns. With prefix arg QUERY, prompts for a ns."
(interactive "P")
(cider-map-repls :clj-strict
(lambda (conn)
(with-current-buffer conn
(cider-ensure-op-supported "toggle-trace-ns")
(let ((ns (if query
(completing-read "Toggle trace for ns: "
(cider-sync-request:ns-list))
(cider-current-ns))))
(let* ((trace-response (cider-sync-request:toggle-trace-ns ns))
(ns-status (nrepl-dict-get trace-response "ns-status")))
(pcase ns-status
("not-found" (error "Namespace %s not found" (cider-propertize ns 'ns)))
(_ (message "Namespace %s %s" (cider-propertize ns 'ns) ns-status)))))))))
(provide 'cider-tracing)
;;; cider-tracing.el ends here
;; cider-util.el --- Common utility functions that don't belong anywhere else -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Common utility functions that don't belong anywhere else.
;;; Code:
;; Built-ins
(require 'ansi-color)
(require 'color)
(require 'seq)
(require 'subr-x)
(require 'thingatpt)
;; clojure-mode and CIDER
(require 'clojure-mode)
(defalias 'cider-pop-back #'pop-tag-mark)
(defcustom cider-font-lock-max-length 10000
"The max length of strings to fontify in `cider-font-lock-as'.
Setting this to nil removes the fontification restriction."
:group 'cider
:type 'boolean
:package-version '(cider . "0.10.0"))
(defun cider-util--hash-keys (hashtable)
"Return a list of keys in HASHTABLE."
(let ((keys '()))
(maphash (lambda (k _v) (setq keys (cons k keys))) hashtable)
keys))
(defun cider-util--clojure-buffers ()
"Return a list of all existing `clojure-mode' buffers."
(seq-filter
(lambda (buffer) (with-current-buffer buffer (derived-mode-p 'clojure-mode)))
(buffer-list)))
(defun cider-current-dir ()
"Return the directory of the current buffer."
(if buffer-file-name
(file-name-directory buffer-file-name)
default-directory))
(defun cider-in-string-p ()
"Return non-nil if point is in a string."
(let ((beg (save-excursion (beginning-of-defun) (point))))
(nth 3 (parse-partial-sexp beg (point)))))
(defun cider-in-comment-p ()
"Return non-nil if point is in a comment."
(let ((beg (save-excursion (beginning-of-defun) (point))))
(nth 4 (parse-partial-sexp beg (point)))))
(defun cider--tooling-file-p (file-name)
"Return t if FILE-NAME is not a 'real' source file.
Currently, only check if the relative file name starts with 'form-init'
which nREPL uses for temporary evaluation file names."
(let ((fname (file-name-nondirectory file-name)))
(string-match-p "^form-init" fname)))
(defun cider--cljc-buffer-p (&optional buffer)
"Return non-nil if the current buffer is visiting a cljc file.
If BUFFER is provided act on that buffer instead."
(with-current-buffer (or buffer (current-buffer))
(or (derived-mode-p 'clojurec-mode))))
;;; Thing at point
(defun cider--text-or-limits (bounds start end)
"Returns the substring or the bounds of text.
If BOUNDS is non-nil, returns the list (START END) of character
positions. Else returns the substring from START to END."
(funcall (if bounds #'list #'buffer-substring-no-properties)
start end))
(defun cider-defun-at-point (&optional bounds)
"Return the text of the top level sexp at point.
If BOUNDS is non-nil, return a list of its starting and ending position
instead."
(save-excursion
(save-match-data
(end-of-defun)
(let ((end (point)))
(clojure-backward-logical-sexp 1)
(cider--text-or-limits bounds (point) end)))))
(defun cider-ns-form ()
"Retrieve the ns form."
(when (clojure-find-ns)
(save-excursion
(goto-char (match-beginning 0))
(cider-defun-at-point))))
(defun cider-symbol-at-point (&optional look-back)
"Return the name of the symbol at point, otherwise nil.
Ignores the REPL prompt. If LOOK-BACK is non-nil, move backwards trying to
find a symbol if there isn't one at point.
Does not strip the : from keywords, nor attempt to expand :: auto-resolved
keywords."
(or (when-let* ((str (thing-at-point 'symbol)))
(unless (text-property-any 0 (length str) 'field 'cider-repl-prompt str)
;; remove font-locking
(setq str (substring-no-properties str))
(if (member str '("." ".."))
str
;; Remove prefix quotes, and trailing . from constructors like Record.
(thread-last
str
;; constructors (Foo.)
(string-remove-suffix ".")
;; quoted symbols ('sym)
(string-remove-prefix "'")
;; var references (#'inc 2)
(string-remove-prefix "#'")))))
(when look-back
(save-excursion
(ignore-errors
(when (looking-at "(")
(forward-char 1))
(while (not (looking-at "\\sw\\|\\s_\\|\\`"))
(forward-sexp -1)))
(cider-symbol-at-point)))))
;;; sexp navigation
(defun cider-sexp-at-point (&optional bounds)
"Return the sexp at point as a string, otherwise nil.
If BOUNDS is non-nil, return a list of its starting and ending position
instead."
(when-let* ((b (or (and (equal (char-after) ?\()
(member (char-before) '(?\' ?\, ?\@))
;; hide stuff before ( to avoid quirks with '( etc.
(save-restriction
(narrow-to-region (point) (point-max))
(bounds-of-thing-at-point 'sexp)))
(bounds-of-thing-at-point 'sexp))))
(funcall (if bounds #'list #'buffer-substring-no-properties)
(car b) (cdr b))))
(defun cider-list-at-point (&optional bounds)
"Return the list (compound form) at point as a string, otherwise nil.
If BOUNDS is non-nil, return a list of its starting and ending position
instead."
(when-let* ((b (or (and (equal (char-after) ?\()
(member (char-before) '(?\' ?\, ?\@))
;; hide stuff before ( to avoid quirks with '( etc.
(save-restriction
(narrow-to-region (point) (point-max))
(bounds-of-thing-at-point 'list)))
(bounds-of-thing-at-point 'list))))
(funcall (if bounds #'list #'buffer-substring-no-properties)
(car b) (cdr b))))
(defun cider-last-sexp (&optional bounds)
"Return the sexp preceding the point.
If BOUNDS is non-nil, return a list of its starting and ending position
instead."
(apply (if bounds #'list #'buffer-substring-no-properties)
(save-excursion
(clojure-backward-logical-sexp 1)
(list (point)
(progn (clojure-forward-logical-sexp 1)
(point))))))
(defun cider-start-of-next-sexp (&optional skip)
"Move to the start of the next sexp.
Skip any non-logical sexps like ^metadata or #reader macros.
If SKIP is an integer, also skip that many logical sexps first.
Can only error if SKIP is non-nil."
(while (clojure--looking-at-non-logical-sexp)
(forward-sexp 1))
(when (and skip (> skip 0))
(dotimes (_ skip)
(forward-sexp 1)
(cider-start-of-next-sexp))))
(defun cider-second-sexp-in-list ()
"Return the second sexp in the list at point."
(condition-case nil
(save-excursion
(backward-up-list)
(forward-char)
(forward-sexp 2)
(cider-sexp-at-point))
(error nil)))
;;; Text properties
(defun cider-maybe-intern (name)
"If NAME is a symbol, return it; otherwise, intern it."
(if (symbolp name) name (intern name)))
(defun cider-intern-keys (plist)
"Copy PLIST, with any non-symbol keys replaced with symbols."
(when plist
(cons (cider-maybe-intern (pop plist))
(cons (pop plist) (cider-intern-keys plist)))))
(defmacro cider-propertize-region (props &rest body)
"Execute BODY and add PROPS to all the inserted text.
More precisely, PROPS are added to the region between the point's
positions before and after executing BODY."
(declare (indent 1)
(debug (sexp body)))
(let ((start (make-symbol "start")))
`(let ((,start (point)))
(prog1 (progn ,@body)
(add-text-properties ,start (point) ,props)))))
(put 'cider-propertize-region 'lisp-indent-function 1)
(defun cider-property-bounds (prop)
"Return the the positions of the previous and next change to PROP.
PROP is the name of a text property."
(let ((end (next-single-char-property-change (point) prop)))
(list (previous-single-char-property-change end prop) end)))
(defun cider-insert (text &optional face break more-text)
"Insert TEXT with FACE, optionally followed by a line BREAK and MORE-TEXT."
(insert (if face (propertize text 'font-lock-face face) text))
(when more-text (insert more-text))
(when break (insert "\n")))
;;; Hooks
(defun cider-run-chained-hook (hook arg)
"Like `run-hook-with-args' but pass intermediate return values through.
HOOK is a name of a hook (a symbol). You can use `add-hook' or
`remove-hook' to add functions to this variable. ARG is passed to first
function. Its return value is passed to the second function and so forth
till all functions are called or one of them returns nil. Return the value
return by the last called function."
(let ((functions (copy-sequence (symbol-value hook))))
(while (and functions arg)
(if (eq (car functions) t)
;; global value of the hook
(let ((functions (default-value hook)))
(while (and functions arg)
(setq arg (funcall (car functions) arg))
(setq functions (cdr functions))))
(setq arg (funcall (car functions) arg)))
(setq functions (cdr functions)))
arg))
;;; Font lock
(defvar cider--mode-buffers nil
"A list of buffers for different major modes.")
(defun cider--make-buffer-for-mode (mode)
"Return a temp buffer using `major-mode' MODE.
This buffer is not designed to display anything to the user. For that, use
`cider-make-popup-buffer' instead."
(setq cider--mode-buffers (seq-filter (lambda (x) (buffer-live-p (cdr x)))
cider--mode-buffers))
(or (cdr (assq mode cider--mode-buffers))
(let ((b (generate-new-buffer (format " *cider-temp %s*" mode))))
(push (cons mode b) cider--mode-buffers)
(with-current-buffer b
;; suppress major mode hooks as we care only about their font-locking
;; otherwise modes like whitespace-mode and paredit might interfere
(setq-local delay-mode-hooks t)
(setq delayed-mode-hooks nil)
(funcall mode))
b)))
(defun cider-ansi-color-string-p (string)
"Return non-nil if STRING is an ANSI string."
(string-match "^\\[" string))
(defun cider-font-lock-as (mode string)
"Use MODE to font-lock the STRING."
(let ((string (if (cider-ansi-color-string-p string)
(substring-no-properties (ansi-color-apply string))
string)))
(if (or (null cider-font-lock-max-length)
(< (length string) cider-font-lock-max-length))
(with-current-buffer (cider--make-buffer-for-mode mode)
(erase-buffer)
(insert string)
;; don't try to font-lock unbalanced Clojure code
(when (eq mode 'clojure-mode)
(check-parens))
(font-lock-fontify-region (point-min) (point-max))
(buffer-string))
string)))
(defun cider-font-lock-region-as (mode beg end &optional buffer)
"Use MODE to font-lock text between BEG and END.
Unless you specify a BUFFER it will default to the current one."
(with-current-buffer (or buffer (current-buffer))
(let ((text (buffer-substring beg end)))
(delete-region beg end)
(goto-char beg)
(insert (cider-font-lock-as mode text)))))
(defun cider-font-lock-as-clojure (string)
"Font-lock STRING as Clojure code."
;; If something goes wrong (e.g. the code is not balanced)
;; we simply return the string.
(condition-case nil
(cider-font-lock-as 'clojure-mode string)
(error string)))
;; Button allowing use of `font-lock-face', ignoring any inherited `face'
(define-button-type 'cider-plain-button
'face nil)
(defun cider-add-face (regexp face &optional foreground-only sub-expr object)
"Propertize all occurrences of REGEXP with FACE.
If FOREGROUND-ONLY is non-nil, change only the foreground of matched
regions. SUB-EXPR is a sub-expression of REGEXP to be
propertized (defaults to 0). OBJECT is an object to be
propertized (defaults to current buffer)."
(setq sub-expr (or sub-expr 0))
(when (and regexp face)
(let ((beg 0)
(end 0))
(with-current-buffer (or (and (bufferp object) object)
(current-buffer))
(while (if (stringp object)
(string-match regexp object end)
(re-search-forward regexp nil t))
(setq beg (match-beginning sub-expr)
end (match-end sub-expr))
(if foreground-only
(let ((face-spec (list (cons 'foreground-color
(face-attribute face :foreground nil t)))))
(font-lock-prepend-text-property beg end 'face face-spec object))
(put-text-property beg end 'face face object)))))))
;;; Colors
(defun cider-scale-background-color ()
"Scale the current background color to get a slighted muted version."
(let ((color (frame-parameter nil 'background-color))
(darkp (eq (frame-parameter nil 'background-mode) 'dark)))
(unless (equal "unspecified-bg" color)
(color-lighten-name color (if darkp 5 -5)))))
(defvar cider-version)
(defvar cider-codename)
(defun cider--pkg-version ()
"Extract CIDER's package version from its package metadata."
;; Use `cond' below to avoid a compiler unused return value warning
;; when `package-get-version' returns nil. See #3181.
;; FIXME: Inline the logic from package-get-version and adapt it
(cond ((fboundp 'package-get-version)
(package-get-version))))
(defun cider--version ()
"Retrieve CIDER's version.
A codename is added to stable versions."
(if (string-match-p "-snapshot" cider-version)
(let ((pkg-version (cider--pkg-version)))
(if pkg-version
;; snapshot versions include the MELPA package version
(format "%s (package: %s)" cider-version pkg-version)
cider-version))
;; stable versions include the codename of the release
(format "%s (%s)" cider-version cider-codename)))
;;; Strings
(defun cider-join-into-alist (candidates &optional separator)
"Make an alist from CANDIDATES.
The keys are the elements joined with SEPARATOR and values are the original
elements. Useful for `completing-read' when candidates are complex
objects."
(mapcar (lambda (el)
(if (listp el)
(cons (string-join el (or separator ":")) el)
(cons el el)))
candidates))
(defun cider-add-to-alist (symbol car cadr)
"Add '(CAR CADR) to the alist stored in SYMBOL.
If CAR already corresponds to an entry in the alist, destructively replace
the entry's second element with CADR.
This can be used, for instance, to update the version of an injected
plugin or dependency with:
(cider-add-to-alist 'cider-jack-in-lein-plugins
\"plugin/artifact-name\" \"THE-NEW-VERSION\")"
(let ((alist (symbol-value symbol)))
(if-let* ((cons (assoc car alist)))
(setcdr cons (list cadr))
(set symbol (cons (list car cadr) alist)))))
(defun cider-namespace-qualified-p (sym)
"Return t if SYM is namespace-qualified."
(string-match-p "[^/]+/" sym))
(defvar cider-version)
(defconst cider-manual-url "https://docs.cider.mx/cider/%s"
"The URL to CIDER's manual.")
(defun cider-version-sans-patch ()
"Return the version sans that patch."
(string-join (seq-take (split-string cider-version "\\.") 2) "."))
(defun cider--manual-version ()
"Convert the version to a ReadTheDocs-friendly version."
(if (string-match-p "-snapshot" cider-version)
""
(concat (cider-version-sans-patch) "/")))
(defun cider-manual-url ()
"The CIDER manual's url."
(format cider-manual-url (cider--manual-version)))
;;;###autoload
(defun cider-view-manual ()
"View the manual in your default browser."
(interactive)
(browse-url (cider-manual-url)))
(defun cider--manual-button (label section-id)
"Return a button string that links to the online manual.
LABEL is the displayed string, and SECTION-ID is where it points
to."
(with-temp-buffer
(insert-text-button
label
'follow-link t
'action (lambda (&rest _) (interactive)
(browse-url (concat (cider-manual-url)
section-id))))
(buffer-string)))
(defconst cider-refcard-url "https://github.com/clojure-emacs/cider/raw/%s/refcard/cider-refcard.pdf"
"The URL to CIDER's refcard.")
(defun cider--github-version ()
"Convert the version to a GitHub-friendly version."
(if (string-match-p "-snapshot" cider-version)
"master"
(concat "v" cider-version)))
(defun cider-refcard-url ()
"The CIDER manual's url."
(format cider-refcard-url (cider--github-version)))
(defun cider-view-refcard ()
"View the refcard in your default browser."
(interactive)
(browse-url (cider-refcard-url)))
(defconst cider-report-bug-url "https://github.com/clojure-emacs/cider/issues/new"
"The URL to report a CIDER issue.")
(defun cider-report-bug ()
"Report a bug in your default browser."
(interactive)
(browse-url cider-report-bug-url))
(defun cider--project-name (dir)
"Extracts a project name from DIR, possibly nil.
The project name is the final component of DIR if not nil."
(when dir
(file-name-nondirectory (directory-file-name dir))))
;;; Vectors
(defun cider--deep-vector-to-list (x)
"Convert vectors in X to lists.
If X is a sequence, return a list of `cider--deep-vector-to-list' applied to
each of its elements.
Any other value is just returned."
(if (sequencep x)
(mapcar #'cider--deep-vector-to-list x)
x))
;;; Help mode
;; Same as https://github.com/emacs-mirror/emacs/blob/86d083438dba60dc00e9e96414bf7e832720c05a/lisp/help-mode.el#L355
;; the original function uses some buffer local variables, but the buffer used
;; is not configurable. It defaults to (help-buffer)
(defun cider--help-setup-xref (item interactive-p buffer)
"Invoked from commands using the \"*Help*\" buffer to install some xref info.
ITEM is a (FUNCTION . ARGS) pair appropriate for recreating the help
buffer after following a reference. INTERACTIVE-P is non-nil if the
calling command was invoked interactively. In this case the stack of
items for help buffer \"back\" buttons is cleared. Use BUFFER for the
buffer local variables.
This should be called very early, before the output buffer is cleared,
because we want to record the \"previous\" position of point so we can
restore it properly when going back."
(with-current-buffer buffer
(when help-xref-stack-item
(push (cons (point) help-xref-stack-item) help-xref-stack)
(setq help-xref-forward-stack nil))
(when interactive-p
(let ((tail (nthcdr 10 help-xref-stack)))
;; Truncate the stack.
(if tail (setcdr tail nil))))
(setq help-xref-stack-item item)))
(defcustom cider-doc-xref-regexp
(eval-and-compile
(rx-to-string
`(or (: "`" (group-n 1 (+ (not space))) "`") ; `var`
(: "[[" (group-n 1 (+ (not space))) "]]") ; [[var]]
(group-n 1 (regexp ,clojure--sym-regexp) "/" (regexp ,clojure--sym-regexp))))) ;; Fully qualified
"The regexp used to search Clojure vars in doc buffers."
:type 'regexp
:safe #'stringp
:group 'cider
:package-version '(cider . "0.13.0"))
(defun cider--find-symbol-xref ()
"Parse and return the first clojure symbol in current buffer.
Use `cider-doc-xref-regexp' for the search. Set match data and return a
string of the Clojure symbol. Return nil if there are no more matches in
the buffer."
(when (re-search-forward cider-doc-xref-regexp nil t)
(match-string 1)))
(declare-function cider-doc-lookup "cider-doc")
(declare-function cider--eldoc-remove-dot "cider-eldoc")
;; Taken from: https://github.com/emacs-mirror/emacs/blob/65c8c7cb96c14f9c6accd03cc8851b5a3459049e/lisp/help-mode.el#L551-L565
(defun cider--make-back-forward-xrefs (&optional buffer)
"Insert special references `back' and `forward', as in `help-make-xrefs'.
Optional argument BUFFER is the buffer in which to insert references.
Default is current buffer."
(with-current-buffer (or buffer (current-buffer))
(insert "\n")
(when (or help-xref-stack help-xref-forward-stack)
(insert "\n"))
;; Make a back-reference in this buffer if appropriate.
(when help-xref-stack
(help-insert-xref-button help-back-label 'help-back
(current-buffer)))
;; Make a forward-reference in this buffer if appropriate.
(when help-xref-forward-stack
(when help-xref-stack
(insert "\t"))
(help-insert-xref-button help-forward-label 'help-forward
(current-buffer)))
(when (or help-xref-stack help-xref-forward-stack)
(insert "\n"))))
;; Similar to https://github.com/emacs-mirror/emacs/blob/65c8c7cb96c14f9c6accd03cc8851b5a3459049e/lisp/help-mode.el#L404
(defun cider--doc-make-xrefs ()
"Parse and hyperlink documentation cross-references in current buffer.
Find cross-reference information in a buffer and activate such cross
references for selection with `help-xref'. Cross-references are parsed
using `cider--find-symbol-xref'.
Special references `back' and `forward' are made to go back and forth
through a stack of help buffers. Variables `help-back-label' and
`help-forward-label' specify the text for that."
(interactive "b")
;; parse the docstring and create xrefs for symbols
(save-excursion
(goto-char (point-min))
(let ((symbol))
(while (setq symbol (cider--find-symbol-xref))
(replace-match "")
(insert-text-button symbol
'type 'help-xref
'help-function (apply-partially #'cider-doc-lookup
(cider--eldoc-remove-dot symbol))))))
(cider--make-back-forward-xrefs))
;;; Words of inspiration
(defun cider-user-first-name ()
"Find the current user's first name."
(let ((name (if (string= (user-full-name) "")
(user-login-name)
(user-full-name))))
(string-match "^[^ ]*" name)
(capitalize (match-string 0 name))))
(defvar cider-words-of-inspiration
`("The best way to predict the future is to invent it. -Alan Kay"
"A point of view is worth 80 IQ points. -Alan Kay"
"Lisp isn't a language, it's a building material. -Alan Kay"
"Simple things should be simple, complex things should be possible. -Alan Kay"
"Everything should be as simple as possible, but not simpler. -Albert Einstein"
"Measuring programming progress by lines of code is like measuring aircraft building progress by weight. -Bill Gates"
"Controlling complexity is the essence of computer programming. -Brian Kernighan"
"The unavoidable price of reliability is simplicity. -C.A.R. Hoare"
"You're bound to be unhappy if you optimize everything. -Donald Knuth"
"Simplicity is prerequisite for reliability. -Edsger W. Dijkstra"
"Elegance is not a dispensable luxury but a quality that decides between success and failure. -Edsger W. Dijkstra"
"Deleted code is debugged code. -Jeff Sickel"
"The key to performance is elegance, not battalions of special cases. -Jon Bentley and Doug McIlroy"
"First, solve the problem. Then, write the code. -John Johnson"
"Simplicity is the ultimate sophistication. -Leonardo da Vinci"
"Programming is not about typing... it's about thinking. -Rich Hickey"
"Design is about pulling things apart. -Rich Hickey"
"Programmers know the benefits of everything and the tradeoffs of nothing. -Rich Hickey"
"Code never lies, comments sometimes do. -Ron Jeffries"
"The true delight is in the finding out rather than in the knowing. -Isaac Asimov"
"If paredit is not for you, then you need to become the sort of person that paredit is for. -Phil Hagelberg"
"Express Yourself. -Madonna"
"Put on your red shoes and dance the blues. -David Bowie"
"Do. Or do not. There is no try. -Yoda"
"The enjoyment of one's tools is an essential ingredient of successful work. -Donald E. Knuth"
"Not all those who wander are lost. -J.R.R. Tolkien"
"The best way to learn is to do. -P.R. Halmos"
"If you wish to make an apple pie from scratch, you must first invent the universe. -Carl Sagan"
"Learn the rules like a pro, so you can break them like an artist. -Pablo Picasso"
"The only way of discovering the limits of the possible is to venture a little way past them into the impossible. -Arthur C. Clarke"
"Don't wish it were easier. Wish you were better. -Jim Rohn"
"One chord is fine. Two chords is pushing it. Three chords and you're into jazz. -Lou Reed"
"We are all apprentices in a craft where no one ever becomes a master. -Ernest Hemingway"
"A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away. -Antoine de Saint-Exupery"
"Clojure isn't a language, it's a building material."
"Think big!"
"Think bold!"
"Think fun!"
"Code big!"
"Code bold!"
"Code fun!"
"Take this REPL, fellow hacker, and may it serve you well."
"Let the hacking commence!"
"Hacks and glory await!"
"Hack and be merry!"
"Your hacking starts... NOW!"
"May the Source be with you!"
"May the Source shine upon thy REPL!"
"Code long and prosper!"
"Happy hacking!"
"nREPL server is up, CIDER REPL is online!"
"CIDER REPL operational!"
"Your imagination is the only limit to what you can do with this REPL!"
"This REPL is yours to command!"
"Fame is but a hack away!"
"The REPL is not enough, but it is such a perfect place to start..."
"Keep on codin' in the free world!"
"What we do in the REPL echoes in eternity!"
"Evaluating is believing."
"To infinity... and beyond."
"Showtime!"
"Unfortunately, no one can be told what CIDER is. You have to figure this out yourself."
"Procure a bottle of cider to achieve optimum programming results."
"In parentheses we trust!"
"Write you some Clojure for Great Good!"
"Oh, what a day... what a lovely day!"
"What a day! What cannot be accomplished on such a splendid day!"
"Home is where your REPL is."
"The worst day programming is better than the best day working."
"The only thing worse than a rebel without a cause is a REPL without a clause."
"In the absence of parentheses, chaos prevails."
"One REPL to rule them all, One REPL to find them, One REPL to bring them all, and in parentheses bind them!"
"A blank REPL promotes creativity."
"A blank REPL is infinitely better than a blank cheque."
,(format "%s, I've a feeling we're not in Kansas anymore."
(cider-user-first-name))
,(format "%s, this could be the start of a beautiful program."
(cider-user-first-name)))
"Scientifically-proven optimal words of hackerish encouragement.")
(defun cider-random-words-of-inspiration ()
"Select a random entry from `cider-words-of-inspiration'."
(nth (random (length cider-words-of-inspiration))
cider-words-of-inspiration))
(defun cider-inspire-me ()
"Display a random inspiration message."
(interactive)
(message (cider-random-words-of-inspiration)))
(defvar cider-tips
'("Press <\\[cider-connect]> to connect to a running nREPL server."
"Press <\\[cider-quit]> to quit the current connection."
"Press <\\[cider-view-manual]> to view CIDER's manual."
"Press <\\[cider-view-refcard]> to view CIDER's refcard."
"Press <\\[describe-mode]> to see a list of the keybindings available (this will work in every Emacs buffer)."
"Press <\\[cider-repl-handle-shortcut]> to quickly invoke some REPL command."
"Press <\\[cider-switch-to-last-clojure-buffer]> to switch between the REPL and a Clojure source buffer."
"Press <\\[cider-doc]> to view the documentation for something (e.g. a var, a Java method)."
"Press <\\[cider-find-resource]> to find a resource on the classpath."
"Press <\\[cider-find-var]> to jump to the source of something (e.g. a var, a Java method)."
"Press <\\[cider-selector]> to quickly select a CIDER buffer."
"Press <\\[cider-test-run-ns-tests]> to run the tests for the current namespace."
"Press <\\[cider-test-run-loaded-tests]> to run all loaded tests."
"Press <\\[cider-test-run-project-tests]> to run all tests for the current project."
"Press <\\[cider-apropos]> to look for a symbol by some search string."
"Press <\\[cider-apropos-documentation]> to look for a symbol that has some string in its docstring."
"Press <\\[cider-eval-defun-at-point]> to eval the top-level form at point."
"Press <\\[cider-eval-defun-up-to-point]> to eval the top-level form up to the point."
"Press <\\[cider-eval-sexp-up-to-point]> to eval the current form up to the point."
"Press <\\[cider-eval-sexp-at-point]> to eval the current form around the point."
"Press <\\[cider-eval-sexp-at-point-in-context]> to eval the current form around the point in a user-provided context."
"Press <\\[cider-eval-buffer]> to eval the entire source buffer."
"Press <\\[cider-scratch]> to create a Clojure scratchpad. Pretty handy for prototyping."
"Press <\\[cider-read-and-eval]> to evaluate some Clojure expression directly in the minibuffer."
"Press <\\[cider-drink-a-sip]> to get more CIDER tips."
"Press <\\[cider-browse-ns-all]> to start CIDER's namespace browser."
"Press <\\[cider-classpath]> to start CIDER's classpath browser."
"Press <\\[cider-repl-history]> to start CIDER's REPL input history browser."
"Press <\\[cider-macroexpand-1]> to expand the preceding macro."
"Press <\\[cider-inspect]> to inspect the preceding expression's result."
"Press <C-u \\[cider-inspect]> to inspect the defun at point's result."
"Press <C-u C-u \\[cider-inspect]> to read Clojure code from the minibuffer and inspect its result."
"Press <\\[cider-ns-refresh]> to reload modified and unloaded namespaces."
"You can define Clojure functions to be called before and after `cider-ns-refresh' (see `cider-ns-refresh-before-fn' and `cider-ns-refresh-after-fn'."
"Press <\\[cider-describe-connection]> to view information about the connection."
"Press <\\[cider-undef]> to undefine a symbol in the current namespace."
"Press <\\[cider-interrupt]> to interrupt an ongoing evaluation."
"Use <M-x customize-group RET cider RET> to see every possible setting you can customize."
"Use <M-x customize-group RET cider-repl RET> to see every possible REPL setting you can customize."
"Enable `eldoc-mode' to display function & method signatures in the minibuffer."
"Enable `cider-enlighten-mode' to display the locals of a function when it's executed."
"Use <\\[cider-close-ancillary-buffers]> to close all ancillary buffers created by CIDER (e.g. *cider-doc*)."
"Exploring CIDER's menu-bar entries is a great way to discover features."
"Keep in mind that some commands don't have a keybinding by default. Explore CIDER!"
"Tweak `cider-repl-prompt-function' to customize your REPL prompt."
"Tweak `cider-eldoc-ns-function' to customize the way namespaces are displayed by eldoc."
"For no middleware, low-tech and reliable namespace reloading use <\\[cider-ns-reload]>."
"Press <\\[cider-load-buffer-and-switch-to-repl-buffer]> to load the current buffer and switch to the REPL buffer afterwards.")
"Some handy CIDER tips."
)
(defun cider-random-tip ()
"Select a random tip from `cider-tips'."
(substitute-command-keys (nth (random (length cider-tips)) cider-tips)))
(defun cider-drink-a-sip ()
"Show a random tip."
(interactive)
(message (cider-random-tip)))
(defun cider-column-number-at-pos (pos)
"Analog to `line-number-at-pos'.
Return buffer column number at position POS."
(save-excursion
(goto-char pos)
;; we have to adjust the column number by 1 to account for the fact
;; that Emacs starts counting columns from 0 and Clojure from 1
(1+ (current-column))))
(defun cider-propertize (text kind)
"Propertize TEXT as KIND.
KIND can be the symbols `ns', `var', `emph', `fn', or a face name."
(propertize text 'face (pcase kind
(`fn 'font-lock-function-name-face)
(`method 'font-lock-function-name-face)
(`special-form 'font-lock-keyword-face)
(`macro 'font-lock-keyword-face)
(`var 'font-lock-variable-name-face)
(`ns 'font-lock-type-face)
(`emph 'font-lock-keyword-face)
(face face))))
(defun cider--menu-add-help-strings (menu-list)
"Add a :help entries to items in MENU-LIST."
(mapcar (lambda (x)
(cond
((listp x) (cider--menu-add-help-strings x))
((and (vectorp x)
(not (plist-get (append x nil) :help))
(functionp (elt x 1)))
(vconcat x `[:help ,(documentation (elt x 1))]))
(t x)))
menu-list))
(provide 'cider-util)
;;; cider-util.el ends here
;;; cider-xref.el --- Xref functionality for Clojure -*- lexical-binding: t -*-
;; Copyright © 2019-2022 Bozhidar Batsov and CIDER contributors
;;
;; Author: Bozhidar Batsov <bozhidar@batsov.dev>
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Xref (find usages) functionality for Clojure. The implementation is based on
;; ideas from this article https://metaredux.com/posts/2019/05/04/discovering-runtime-function-references-in-clojure.html.
;;
;; Keep in mind that you won't get references in namespaces that haven't been loaded yet.
;;; Code:
(require 'cider-doc)
(require 'cider-find)
(require 'cider-util)
(require 'subr-x)
(require 'cider-client)
(require 'cider-popup)
(require 'nrepl-dict)
(require 'clojure-mode)
(require 'apropos)
(require 'button)
(defconst cider-xref-buffer "*cider-xref*")
(defcustom cider-xref-actions '(("display-doc" . cider-doc-lookup)
("find-def" . cider--find-var)
("lookup-on-clojuredocs" . cider-clojuredocs-lookup))
"Controls the actions to be applied on the symbol found by an xref search.
The first action key in the list will be selected as default. If the list
contains only one action key, the associated action function will be
applied automatically. An action function can be any function that receives
the symbol found by the xref search as argument."
:type '(alist :key-type string :value-type function)
:group 'cider
:package-version '(cider . "0.22.0"))
(defun cider-xref-doc (button)
"Display documentation for the symbol represented at BUTTON."
(cider-doc-lookup (button-get button 'apropos-symbol)))
(defun cider-xref-result (result)
"Emit a RESULT into current buffer."
(let ((var-name (nrepl-dict-get result "name")))
(cider-propertize-region (list 'apropos-symbol var-name
'action #'cider-xref-doc
'help-echo "Display doc")
(insert-text-button var-name 'type 'apropos-symbol))
(insert "\n ")
(insert-text-button "Function" 'type 'apropos-function)
(insert ": ")
(let ((beg (point)))
(insert (nrepl-dict-get result "doc"))
(fill-region beg (point)))
(insert "\n")
(if-let* ((file (nrepl-dict-get result "file"))
(line (nrepl-dict-get result "line")))
(progn
(insert (propertize var-name
'font-lock-face 'font-lock-function-name-face)
" is defined in ")
(insert-text-button (cider--abbreviate-file-protocol file)
'follow-link t
'action (lambda (_x)
(cider-xref-source file line var-name)))
(insert "."))
(insert "Definition location unavailable."))
(insert "\n")))
(defun cider-xref-source (file line name)
"Find source for FILE, LINE and NAME."
(interactive)
(if file
(if-let* ((buffer (and (not (cider--tooling-file-p file))
(cider-find-file file))))
(cider-jump-to buffer (if line
(cons line nil)
name)
nil)
(user-error
(substitute-command-keys
"Can't find the source because it wasn't defined with `cider-eval-buffer'")))
(error "No source location for %s" name)))
(declare-function cider-mode "cider-mode")
(defun cider-show-xref (summary results)
"Show SUMMARY and RESULTS in a pop-up buffer."
(with-current-buffer (cider-popup-buffer cider-xref-buffer 'select 'apropos-mode 'ancillary)
(let ((inhibit-read-only t))
(if (boundp 'header-line-format)
(setq-local header-line-format summary)
(insert summary "\n\n"))
(dolist (result results)
(cider-xref-result result))
(goto-char (point-min)))))
;;;###autoload
(defun cider-xref-fn-refs (&optional ns symbol)
"Show all functions that reference the var matching NS and SYMBOL."
(interactive)
(cider-ensure-connected)
(cider-ensure-op-supported "fn-refs")
(if-let* ((ns (or ns (cider-current-ns)))
(symbol (or symbol (cider-symbol-at-point)))
(results (cider-sync-request:fn-refs ns symbol)))
(cider-show-xref (format "Showing %d functions that reference %s in currently loaded namespaces" (length results) symbol) results)
(message "No references found for %S in currently loaded namespaces" symbol)))
;;;###autoload
(defun cider-xref-fn-deps (&optional ns symbol)
"Show all functions referenced by the var matching NS and SYMBOL."
(interactive)
(cider-ensure-connected)
(cider-ensure-op-supported "fn-deps")
(if-let* ((ns (or ns (cider-current-ns)))
(symbol (or symbol (cider-symbol-at-point)))
(results (cider-sync-request:fn-deps ns symbol)))
(cider-show-xref (format "Showing %d function dependencies for %s" (length results) symbol) results)
(message "No dependencies found for %S" symbol)))
(defun cider-xref-act-on-symbol (symbol)
"Apply selected action on SYMBOL."
(let* ((first-action-key (car (car cider-xref-actions)))
(action-key (if (= 1 (length cider-xref-actions))
first-action-key
(completing-read (format "Choose action to apply to `%s` (default %s): "
symbol first-action-key)
cider-xref-actions nil nil nil nil first-action-key)))
(action-fn (cdr (assoc action-key cider-xref-actions))))
(if action-fn
(funcall action-fn symbol)
(user-error "Unknown action `%s`" action-key))))
;;;###autoload
(defun cider-xref-fn-refs-select (&optional ns symbol)
"Displays the references for NS and SYMBOL using completing read."
(interactive)
(cider-ensure-connected)
(cider-ensure-op-supported "fn-refs")
(if-let* ((ns (or ns (cider-current-ns)))
(symbol (or symbol (cider-symbol-at-point)))
(results (mapcar (lambda (d) (nrepl-dict-get d "name")) (cider-sync-request:fn-refs ns symbol)))
(summary (format "References for %s" symbol)))
(cider-xref-act-on-symbol (completing-read (concat summary ": ") results))
(message "No references for %S found" symbol)))
;;;###autoload
(defun cider-xref-fn-deps-select (&optional ns symbol)
"Displays the function dependencies for NS and SYMBOL using completing read."
(interactive)
(cider-ensure-connected)
(cider-ensure-op-supported "fn-deps")
(if-let* ((ns (or ns (cider-current-ns)))
(symbol (or symbol (cider-symbol-at-point)))
(results (mapcar (lambda (d) (nrepl-dict-get d "name")) (cider-sync-request:fn-deps ns symbol)))
(summary (format "Dependencies for %s" symbol)))
(cider-xref-act-on-symbol (completing-read (concat summary ": ") results))
(message "No dependencies for %S found" symbol)))
(provide 'cider-xref)
;;; cider-xref.el ends here
;;; cider.el --- Clojure Interactive Development Environment that Rocks -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; Maintainer: Bozhidar Batsov <bozhidar@batsov.dev>
;; URL: http://www.github.com/clojure-emacs/cider
;; Version: 1.6.0-snapshot
;; Package-Requires: ((emacs "26") (clojure-mode "5.15.1") (parseedn "1.0.6") (queue "0.2") (spinner "1.7") (seq "2.22") (sesman "0.3.2"))
;; Keywords: languages, clojure, cider
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Provides a Clojure interactive development environment for Emacs, built on
;; top of nREPL. See https://docs.cider.mx for more details.
;;; Installation:
;; CIDER is available as a package in melpa.org and stable.melpa.org. First, make sure you've
;; enabled one of the repositories in your Emacs config:
;; (add-to-list 'package-archives
;; '("melpa" . "https://melpa.org/packages/"))
;;
;; or
;;
;; (add-to-list 'package-archives
;; '("melpa-stable" . "https://stable.melpa.org/packages/") t)
;; Afterwards, installing CIDER is as easy as:
;; M-x package-install cider
;;; Usage:
;; You can start CIDER with one of the following commands:
;; M-x cider-jack-in-clj
;; M-x cider-jack-in-cljs
;;
;; M-x cider-connect-sibling-clj
;; M-x cider-connect-sibling-cljs
;;
;; M-x cider-connect-clj
;; M-x cider-connect-cljs
;;; Code:
(defgroup cider nil
"Clojure Interactive Development Environment that Rocks."
:prefix "cider-"
:group 'applications
:link '(url-link :tag "GitHub" "https://github.com/clojure-emacs/cider")
:link '(url-link :tag "Homepage" "https://cider.mx")
:link '(url-link :tag "Documentation" "https://docs.cider.mx")
:link '(emacs-commentary-link :tag "Commentary" "cider"))
(require 'cider-client)
(require 'cider-eldoc)
(require 'cider-repl)
(require 'cider-repl-history)
(require 'cider-connection)
(require 'cider-mode)
(require 'cider-common)
(require 'cider-debug)
(require 'cider-util)
(require 'tramp-sh)
(require 'subr-x)
(require 'seq)
(require 'sesman)
(require 'package)
(defconst cider-version "1.6.0-snapshot"
"The current version of CIDER.")
(defconst cider-codename "Strasbourg"
"Codename used to denote stable releases.")
(defcustom cider-lein-command
"lein"
"The command used to execute Leiningen."
:type 'string)
(defcustom cider-lein-global-options
nil
"Command global options used to execute Leiningen (e.g.: -o for offline)."
:type 'string
:safe #'stringp)
(defcustom cider-lein-parameters
"repl :headless :host localhost"
"Params passed to Leiningen to start an nREPL server via `cider-jack-in'."
:type 'string
:safe #'stringp)
(defcustom cider-boot-command
"boot"
"The command used to execute Boot."
:type 'string
:package-version '(cider . "0.9.0"))
(defcustom cider-boot-global-options
nil
"Command global options used to execute Boot (e.g.: -c for checkouts)."
:type 'string
:safe #'stringp
:package-version '(cider . "0.14.0"))
(defcustom cider-boot-parameters
"repl -s -b localhost wait"
"Params passed to boot to start an nREPL server via `cider-jack-in'."
:type 'string
:safe #'stringp
:package-version '(cider . "0.9.0"))
(defcustom cider-clojure-cli-command
(if (and (eq system-type 'windows-nt)
(null (executable-find "clojure")))
"powershell"
"clojure")
"The command used to execute clojure with tools.deps (requires Clojure 1.9+).
Don't use clj here, as it doesn't work when spawned from Emacs due to it
using rlwrap. If on Windows and no \"clojure\" executable is found we
default to \"powershell\"."
:type 'string
:safe #'stringp
:package-version '(cider . "0.17.0"))
(defcustom cider-clojure-cli-global-options
nil
"Command line options used to execute clojure with tools.deps."
:type 'string
:safe #'stringp
:package-version '(cider . "0.17.0"))
(defcustom cider-clojure-cli-aliases
nil
"A list of aliases to include when using the clojure cli.
Alias names should be of the form \":foo:bar\".
Leading \"-A\" \"-M\" \"-T\" or \"-X\" are stripped from aliases
then concatenated into the \"-M[your-aliases]:cider/nrepl\" form."
:type 'string
:safe #'stringp
:package-version '(cider . "1.1"))
(defcustom cider-shadow-cljs-command
"npx shadow-cljs"
"The command used to execute shadow-cljs.
By default we favor the project-specific shadow-cljs over the system-wide."
:type 'string
:safe #'stringp
:package-version '(cider . "0.17.0"))
(defcustom cider-shadow-cljs-global-options
""
"Command line options used to execute shadow-cljs (e.g.: -v for verbose mode)."
:type 'string
:safe #'stringp
:package-version '(cider . "0.17.0"))
(defcustom cider-shadow-cljs-parameters
"server"
"Params passed to shadow-cljs to start an nREPL server via `cider-jack-in'."
:type 'string
:safe #'stringp
:package-version '(cider . "0.17.0"))
(defcustom cider-gradle-command
"./gradlew"
"The command used to execute Gradle."
:type 'string
:safe #'stringp
:package-version '(cider . "0.10.0"))
(defcustom cider-gradle-global-options
""
"Command line options used to execute Gradle (e.g.: -m for dry run)."
:type 'string
:safe #'stringp
:package-version '(cider . "0.14.0"))
(defcustom cider-gradle-parameters
"clojureRepl"
"Params passed to gradle to start an nREPL server via `cider-jack-in'."
:type 'string
:safe #'stringp
:package-version '(cider . "0.10.0"))
(defcustom cider-babashka-command
"bb"
"The command used to execute Babashka."
:type 'string
:safe #'stringp
:package-version '(cider . "1.2.0"))
(defcustom cider-babashka-global-options
nil
"Command line options used to execute Babashka."
:type 'string
:safe #'stringp
:package-version '(cider . "1.2.0"))
(defcustom cider-babashka-parameters
"nrepl-server"
"Params passed to babashka to start an nREPL server via `cider-jack-in'."
:type 'string
:safe #'stringp
:package-version '(cider . "1.2.0"))
(defcustom cider-jack-in-default (if (executable-find "clojure") 'clojure-cli 'lein)
"The default tool to use when doing `cider-jack-in' outside a project.
This value will only be consulted when no identifying file types, i.e.
project.clj for leiningen or build.boot for boot, could be found.
As the Clojure CLI is bundled with Clojure itself, it's the default.
In the absence of the Clojure CLI (e.g. on Windows), we fallback
to Leiningen."
:type '(choice (const lein)
(const boot)
(const clojure-cli)
(const shadow-cljs)
(const gradle)
(const babashka))
:safe #'symbolp
:package-version '(cider . "0.9.0"))
(defcustom cider-preferred-build-tool
nil
"Allow choosing a build system when there are many.
When there are project markers from multiple build systems (e.g. lein and
boot) the user is prompted to select one of them. When non-nil, this
variable will suppress this behavior and will select whatever build system
is indicated by the variable if present. Note, this is only when CIDER
cannot decide which of many build systems to use and will never override a
command when there is no ambiguity."
:type '(choice (const lein)
(const boot)
(const clojure-cli)
(const shadow-cljs)
(const gradle)
(const babashka)
(const :tag "Always ask" nil))
:safe #'symbolp
:package-version '(cider . "0.13.0"))
(defcustom cider-allow-jack-in-without-project 'warn
"Controls what happens when doing `cider-jack-in' outside a project.
When set to 'warn you'd prompted to confirm the command.
When set to t `cider-jack-in' will quietly continue.
When set to nil `cider-jack-in' will fail."
:type '(choice (const :tag "always" t)
(const warn)
(const :tag "never" nil))
:safe #'symbolp
:package-version '(cider . "0.15.0"))
(defcustom cider-known-endpoints nil
"A list of connection endpoints where each endpoint is a list.
For example: \\='((\"label\" \"host\" \"port\")).
The label is optional so that \\='(\"host\" \"port\") will suffice.
This variable is used by `cider-connect'."
:type '(repeat (list (string :tag "label")
(string :tag "host")
(string :tag "port"))))
(defcustom cider-connected-hook nil
"List of functions to call when connected to Clojure nREPL server."
:type 'hook
:package-version '(cider . "0.9.0"))
(defcustom cider-disconnected-hook nil
"List of functions to call when disconnected from the Clojure nREPL server."
:type 'hook
:package-version '(cider . "0.9.0"))
(defcustom cider-inject-dependencies-at-jack-in t
"When nil, do not inject repl dependencies at `cider-jack-in' time.
The repl dependendcies are most likely to be nREPL middlewares."
:type 'boolean
:safe #'booleanp
:version '(cider . "0.11.0"))
(defcustom cider-offer-to-open-cljs-app-in-browser t
"When nil, do not offer to open ClojureScript apps in a browser on connect."
:type 'boolean
:safe #'booleanp
:version '(cider . "0.15.0"))
(defvar cider-ps-running-nrepls-command "ps u | grep leiningen"
"Process snapshot command used in `cider-locate-running-nrepl-ports'.")
(defvar cider-ps-running-nrepl-path-regexp-list
'("\\(?:leiningen.original.pwd=\\)\\(.+?\\) -D"
"\\(?:-classpath +:?\\(.+?\\)/self-installs\\)")
"Regexp list to get project paths.
Extract project paths from output of `cider-ps-running-nrepls-command'.
Sub-match 1 must be the project path.")
(defvar cider-host-history nil
"Completion history for connection hosts.")
;;;###autoload
(defun cider-version ()
"Display CIDER's version."
(interactive)
(message "CIDER %s" (cider--version)))
(defun cider-jack-in-command (project-type)
"Determine the command `cider-jack-in' needs to invoke for the PROJECT-TYPE."
(pcase project-type
('lein cider-lein-command)
('boot cider-boot-command)
('clojure-cli cider-clojure-cli-command)
('babashka cider-babashka-command)
('shadow-cljs cider-shadow-cljs-command)
('gradle cider-gradle-command)
(_ (user-error "Unsupported project type `%S'" project-type))))
(defun cider-jack-in-resolve-command (project-type)
"Determine the resolved file path to `cider-jack-in-command'.
Throws an error if PROJECT-TYPE is unknown."
(pcase project-type
('lein (cider--resolve-command cider-lein-command))
('boot (cider--resolve-command cider-boot-command))
('clojure-cli (cider--resolve-command cider-clojure-cli-command))
('babashka (cider--resolve-command cider-babashka-command))
;; here we have to account for the possibility that the command is either
;; "npx shadow-cljs" or just "shadow-cljs"
('shadow-cljs (let ((parts (split-string cider-shadow-cljs-command)))
(when-let* ((command (cider--resolve-command (car parts))))
(mapconcat #'identity (cons command (cdr parts)) " "))))
;; here we have to account for use of the Gradle wrapper which is
;; a shell script within their project, so if they have a clearly
;; relative path like "./gradlew" use locate file instead of checking
;; the exec-path
('gradle (cider--resolve-project-command cider-gradle-command))
(_ (user-error "Unsupported project type `%S'" project-type))))
(defun cider-jack-in-global-options (project-type)
"Determine the command line options for `cider-jack-in' for the PROJECT-TYPE."
(pcase project-type
('lein cider-lein-global-options)
('boot cider-boot-global-options)
('clojure-cli cider-clojure-cli-global-options)
('babashka cider-babashka-global-options)
('shadow-cljs cider-shadow-cljs-global-options)
('gradle cider-gradle-global-options)
(_ (user-error "Unsupported project type `%S'" project-type))))
(defun cider-jack-in-params (project-type)
"Determine the commands params for `cider-jack-in' for the PROJECT-TYPE."
;; The format of these command-line strings must consider different shells,
;; different values of IFS, and the possibility that they'll be run remotely
;; (e.g. with TRAMP). Using `", "` causes problems with TRAMP, for example.
;; Please be careful when changing them.
(pcase project-type
('lein cider-lein-parameters)
('boot cider-boot-parameters)
('clojure-cli nil)
('babashka cider-babashka-parameters)
('shadow-cljs cider-shadow-cljs-parameters)
('gradle cider-gradle-parameters)
(_ (user-error "Unsupported project type `%S'" project-type))))
;;; Jack-in dependencies injection
(defvar cider-jack-in-dependencies nil
"List of dependencies where elements are lists of artifact name and version.")
(put 'cider-jack-in-dependencies 'risky-local-variable t)
(defcustom cider-injected-nrepl-version "1.0.0"
"The version of nREPL injected on jack-in.
We inject the newest known version of nREPL just in case
your version of Boot or Leiningen is bundling an older one."
:type 'string
:package-version '(cider . "1.2.0")
:safe #'stringp)
(defvar cider-jack-in-cljs-dependencies nil
"List of dependencies where elements are lists of artifact name and version.
Added to `cider-jack-in-dependencies' when doing `cider-jack-in-cljs'.")
(put 'cider-jack-in-cljs-dependencies 'risky-local-variable t)
(cider-add-to-alist 'cider-jack-in-cljs-dependencies "cider/piggieback" "0.5.2")
(defvar cider-jack-in-dependencies-exclusions nil
"List of exclusions for jack in dependencies.
Elements of the list are artifact name and list of exclusions to apply for
the artifact.")
(put 'cider-jack-in-dependencies-exclusions 'risky-local-variable t)
(defconst cider-clojure-artifact-id "org.clojure/clojure"
"Artifact identifier for Clojure.")
(defconst cider-minimum-clojure-version "1.8.0"
"Minimum supported version of Clojure.")
(defconst cider-latest-clojure-version "1.10.1"
"Latest supported version of Clojure.")
(defconst cider-required-middleware-version "0.28.7"
"The CIDER nREPL version that's known to work properly with CIDER.")
(defcustom cider-injected-middleware-version cider-required-middleware-version
"The version of cider-nrepl injected on jack-in.
Should be newer than the required version for optimal results."
:type 'string
:package-version '(cider . "1.2.0")
:safe #'stringp)
(defcustom cider-enrich-classpath nil
"Whether to use git.io/JiJVX for adding sources and javadocs to the classpath.
This is done in a clean manner, without interfering with classloaders.
Only available for Leiningen projects at the moment."
:type 'boolean
:package-version '(cider . "1.2.0")
:safe #'booleanp)
(defcustom cider-jack-in-auto-inject-clojure nil
"Version of clojure to auto-inject into REPL.
If nil, do not inject Clojure into the REPL. If `latest', inject
`cider-latest-clojure-version', which should approximate to the most recent
version of Clojure. If `minimal', inject `cider-minimum-clojure-version',
which will be the lowest version CIDER supports. If a string, use this as
the version number. If it is a list, the first element should be a string,
specifying the artifact ID, and the second element the version number."
:type '(choice (const :tag "None" nil)
(const :tag "Latest" latest)
(const :tag "Minimal" minimal)
(string :tag "Specific Version")
(list :tag "Artifact ID and Version"
(string :tag "Artifact ID")
(string :tag "Version"))))
(defvar cider-jack-in-lein-plugins nil
"List of Leiningen plugins to be injected at jack-in.
Each element is a list of artifact name and version, followed optionally by
keyword arguments. The only keyword argument currently accepted is
`:predicate', which should be given a function that takes the list (name,
version, and keyword arguments) and returns non-nil to indicate that the
plugin should actually be injected. (This is useful primarily for packages
that extend CIDER, not for users. For example, a refactoring package might
want to inject some middleware only when within a project context.)")
(put 'cider-jack-in-lein-plugins 'risky-local-variable t)
(defvar cider-jack-in-lein-middlewares nil
"List of Leiningen :middleware values to be injected at jack-in.
Necessary for plugins which require an explicit middleware name to be specified.
Can also facilitate using middleware in a specific order.")
(put 'cider-jack-in-lein-middlewares 'risky-local-variable t)
(defvar cider-jack-in-cljs-lein-plugins nil
"List of Leiningen plugins to be injected at jack-in.
Added to `cider-jack-in-lein-plugins' (which see) when doing
`cider-jack-in-cljs'.")
(put 'cider-jack-in-cljs-lein-plugins 'risky-local-variable t)
(defun cider-jack-in-normalized-lein-plugins ()
"Return a normalized list of Leiningen plugins to be injected.
See `cider-jack-in-lein-plugins' for the format, except that the list
returned by this function does not include keyword arguments."
(let ((plugins (if cider-enrich-classpath
(append cider-jack-in-lein-plugins
`(("cider/cider-nrepl" ,cider-injected-middleware-version)
("mx.cider/enrich-classpath" "1.9.0")))
(append cider-jack-in-lein-plugins
`(("cider/cider-nrepl" ,cider-injected-middleware-version))))))
(thread-last
plugins
(seq-filter
(lambda (spec)
(if-let* ((pred (plist-get (seq-drop spec 2) :predicate)))
(funcall pred spec)
t)))
(mapcar
(lambda (spec)
(seq-take spec 2))))))
(defvar cider-jack-in-nrepl-middlewares nil
"List of Clojure variable names.
Each of these Clojure variables should hold a vector of nREPL middlewares.
Instead of a string, an element can be a list containing a string followed
by optional keyword arguments. The only keyword argument currently
accepted is `:predicate', which should be given a function that takes the
list (string and keyword arguments) and returns non-nil to indicate that
the middlewares should actually be injected.")
(put 'cider-jack-in-nrepl-middlewares 'risky-local-variable t)
(add-to-list 'cider-jack-in-nrepl-middlewares "cider.nrepl/cider-middleware")
(defvar cider-jack-in-cljs-nrepl-middlewares nil
"List of Clojure variable names.
Added to `cider-jack-in-nrepl-middlewares' (which see) when doing
`cider-jack-in-cljs'.")
(put 'cider-jack-in-cljs-nrepl-middlewares 'risky-local-variable t)
(add-to-list 'cider-jack-in-cljs-nrepl-middlewares "cider.piggieback/wrap-cljs-repl")
(defun cider-jack-in-normalized-nrepl-middlewares ()
"Return a normalized list of middleware variable names.
See `cider-jack-in-nrepl-middlewares' for the format, except that the list
returned by this function only contains strings."
(thread-last
cider-jack-in-nrepl-middlewares
(seq-filter
(lambda (spec)
(or (not (listp spec))
(if-let* ((pred (plist-get (cdr spec) :predicate)))
(funcall pred spec)
t))))
(mapcar
(lambda (spec)
(if (listp spec)
(car spec)
spec)))))
(defun cider--list-as-boot-artifact (list)
"Return a boot artifact string described by the elements of LIST.
LIST should have the form (ARTIFACT-NAME ARTIFACT-VERSION). The returned
string is quoted for passing as argument to an inferior shell."
(concat "-d " (shell-quote-argument (format "%s:%s" (car list) (cadr list)))))
(defun cider--jack-in-required-dependencies ()
"Returns the required CIDER deps.
They are normally added to `cider-jack-in-dependencies',
unless it's a Lein project."
`(("nrepl/nrepl" ,cider-injected-nrepl-version)
("cider/cider-nrepl" ,cider-injected-middleware-version)))
(defun cider-boot-dependencies (dependencies)
"Return a list of boot artifact strings created from DEPENDENCIES."
(concat (mapconcat #'cider--list-as-boot-artifact dependencies " ")
(unless (seq-empty-p dependencies) " ")))
(defun cider-boot-middleware-task (params middlewares)
"Create a command to add MIDDLEWARES with corresponding PARAMS."
(concat "cider.tasks/add-middleware "
(mapconcat (lambda (middleware)
(format "-m %s" (shell-quote-argument middleware)))
middlewares
" ")
" " params))
(defun cider-boot-jack-in-dependencies (global-opts params dependencies middlewares)
"Create boot jack-in dependencies.
Does so by concatenating GLOBAL-OPTS, DEPENDENCIES,
and MIDDLEWARES. PARAMS and MIDDLEWARES are passed on to
`cider-boot-middleware-task` before concatenating and DEPENDENCIES
are passed on to `cider-boot-dependencies`."
(concat global-opts
(unless (seq-empty-p global-opts) " ")
"-i \"(require 'cider.tasks)\" " ;; Note the space at the end here
(cider-boot-dependencies (append (cider--jack-in-required-dependencies) dependencies))
(cider-boot-middleware-task params middlewares)))
(defun cider--gradle-dependency-notation (dependency)
"Returns Gradle's GAV dependency syntax.
For a \"group/artifact\" \"version\") DEPENDENCY list
return as group:artifact:version notation."
(let ((group-artifact (replace-regexp-in-string "/" ":" (car dependency)))
(version (cadr dependency)))
(format "%s:%s" group-artifact version)))
(defun cider--gradle-jack-in-property (dependencies)
"Returns Clojurephant's dependency jack-in property.
For DEPENDENCIES, translates to Gradle's dependency notation
using `cider--gradle-dependency-notation`.''"
(if (seq-empty-p dependencies)
""
(shell-quote-argument
(concat "-Pdev.clojurephant.jack-in.nrepl="
(mapconcat #'cider--gradle-dependency-notation dependencies ",")))))
(defun cider--gradle-middleware-params (middlewares)
"Returns Gradle-formatted middleware params.
Given a list of MIDDLEWARES symbols, this returns
the Gradle parameters expected by Clojurephant's
ClojureNRepl task."
(mapconcat (lambda (middleware)
(shell-quote-argument (concat "--middleware=" middleware)))
middlewares
" "))
(defun cider-gradle-jack-in-dependencies (global-opts params dependencies middlewares)
"Create gradle jack in dependencies.
Does so by concatenating GLOBAL-OPTS, DEPENDENCIES,
and MIDDLEWARES. GLOBAL-OPTS and PARAMS are taken as-is.
DEPENDENCIES are translated into Gradle's typical
group:artifact:version notation and MIDDLEWARES are
prepared as arguments to Clojurephant's ClojureNRepl task."
(concat global-opts
(unless (seq-empty-p global-opts) " ")
(cider--gradle-jack-in-property (append (cider--jack-in-required-dependencies) dependencies))
" "
params
(unless (seq-empty-p params) " ")
(cider--gradle-middleware-params middlewares)))
(defun cider--lein-artifact-exclusions (exclusions)
"Return an exclusions vector described by the elements of EXCLUSIONS."
(if exclusions
(format " :exclusions [%s]" (mapconcat #'identity exclusions " "))
""))
(defun cider--list-as-lein-artifact (list &optional exclusions)
"Return an artifact string described by the elements of LIST.
LIST should have the form (ARTIFACT-NAME ARTIFACT-VERSION). Optionally a list
of EXCLUSIONS can be provided as well. The returned
string is quoted for passing as argument to an inferior shell."
(shell-quote-argument (format "[%s %S%s]" (car list) (cadr list) (cider--lein-artifact-exclusions exclusions))))
(defun cider-lein-jack-in-dependencies (global-opts params dependencies dependencies-exclusions lein-plugins &optional lein-middlewares)
"Create lein jack-in dependencies.
Does so by concatenating GLOBAL-OPTS, DEPENDENCIES, with DEPENDENCIES-EXCLUSIONS
removed, LEIN-PLUGINS, LEIN-MIDDLEWARES and finally PARAMS."
(concat
global-opts
(unless (seq-empty-p global-opts) " ")
(mapconcat #'identity
(append (seq-map (lambda (dep)
(let ((exclusions (cadr (assoc (car dep) dependencies-exclusions))))
(concat "update-in :dependencies conj "
(cider--list-as-lein-artifact dep exclusions))))
dependencies)
(seq-map (lambda (plugin)
(concat "update-in :plugins conj "
(cider--list-as-lein-artifact plugin)))
lein-plugins)
(seq-map (lambda (middleware)
(concat "update-in :middleware conj "
middleware))
lein-middlewares))
" -- ")
" -- "
params))
(defun cider--dedupe-deps (deps)
"Removes the duplicates in DEPS."
(cl-delete-duplicates deps :test 'equal))
(defun cider-clojure-cli-jack-in-dependencies (global-options _params dependencies)
"Create Clojure tools.deps jack-in dependencies.
Does so by concatenating DEPENDENCIES and GLOBAL-OPTIONS into a suitable
`clojure` invocation. The main is placed in an inline alias :cider/nrepl
so that if your aliases contain any mains, the cider/nrepl one will be the
one used."
(let* ((all-deps (thread-last
dependencies
(append (cider--jack-in-required-dependencies))
;; Duplicates are never OK since they would result in
;; `java.lang.IllegalArgumentException: Duplicate key [...]`:
(cider--dedupe-deps)
(seq-map (lambda (dep)
(format "%s {:mvn/version \"%s\"}" (car dep) (cadr dep))))))
(middleware (mapconcat
(apply-partially #'format "%s")
(cider-jack-in-normalized-nrepl-middlewares)
","))
(main-opts (format "\"-m\" \"nrepl.cmdline\" \"--middleware\" \"[%s]\"" middleware)))
(format "%s-Sdeps '{:deps {%s} :aliases {:cider/nrepl {:main-opts [%s]}}}' -M%s:cider/nrepl"
(if global-options (format "%s " global-options) "")
(string-join all-deps " ")
main-opts
(if cider-clojure-cli-aliases
;; remove exec-opts flags -A -M -T or -X from cider-clojure-cli-aliases
;; concatenated with :cider/nrepl to ensure :cider/nrepl comes last
(format "%s" (replace-regexp-in-string "^-\\(A\\|M\\|T\\|X\\)" "" cider-clojure-cli-aliases))
""))))
(defun cider-shadow-cljs-jack-in-dependencies (global-opts params dependencies)
"Create shadow-cljs jack-in deps.
Does so by concatenating GLOBAL-OPTS, DEPENDENCIES finally PARAMS."
(let ((dependencies (append (cider--jack-in-required-dependencies) dependencies)))
(concat
global-opts
(unless (seq-empty-p global-opts) " ")
(mapconcat #'identity
(seq-map (lambda (dep) (format "-d %s:%s" (car dep) (cadr dep))) dependencies)
" ")
" "
params)))
(defun cider-add-clojure-dependencies-maybe (dependencies)
"Return DEPENDENCIES with an added Clojure dependency if requested.
See also `cider-jack-in-auto-inject-clojure'."
(if cider-jack-in-auto-inject-clojure
(if (consp cider-jack-in-auto-inject-clojure)
(cons cider-jack-in-auto-inject-clojure dependencies)
(cons (list cider-clojure-artifact-id
(cond
((stringp cider-jack-in-auto-inject-clojure)
cider-jack-in-auto-inject-clojure)
((eq cider-jack-in-auto-inject-clojure 'minimal)
cider-minimum-clojure-version)
((eq cider-jack-in-auto-inject-clojure 'latest)
cider-latest-clojure-version)))
dependencies))
dependencies))
(defun cider-inject-jack-in-dependencies (global-opts params project-type)
"Return GLOBAL-OPTS and PARAMS with injected REPL dependencies.
These are set in `cider-jack-in-dependencies', `cider-jack-in-lein-plugins' and
`cider-jack-in-nrepl-middlewares' are injected from the CLI according to
the used PROJECT-TYPE. Eliminates the need for hacking profiles.clj or the
boot script for supporting CIDER with its nREPL middleware and
dependencies."
(pcase project-type
('lein (cider-lein-jack-in-dependencies
global-opts
params
(cider-add-clojure-dependencies-maybe
(append `(("nrepl/nrepl" ,cider-injected-nrepl-version)) cider-jack-in-dependencies))
cider-jack-in-dependencies-exclusions
(cider-jack-in-normalized-lein-plugins)
(if cider-enrich-classpath
(append cider-jack-in-lein-middlewares
'("cider.enrich-classpath/middleware"))
cider-jack-in-lein-middlewares)))
('boot (cider-boot-jack-in-dependencies
global-opts
params
(cider-add-clojure-dependencies-maybe
cider-jack-in-dependencies)
(cider-jack-in-normalized-nrepl-middlewares)))
('clojure-cli (cider-clojure-cli-jack-in-dependencies
global-opts
params
(cider-add-clojure-dependencies-maybe
cider-jack-in-dependencies)))
('babashka (concat
global-opts
(unless (seq-empty-p global-opts) " ")
params))
('shadow-cljs (cider-shadow-cljs-jack-in-dependencies
global-opts
params
(cider-add-clojure-dependencies-maybe
cider-jack-in-dependencies)))
('gradle (cider-gradle-jack-in-dependencies
global-opts
params
(cider-add-clojure-dependencies-maybe
cider-jack-in-dependencies)
(cider-jack-in-normalized-nrepl-middlewares)))
(_ (error "Unsupported project type `%S'" project-type))))
;;; ClojureScript REPL creation
(defcustom cider-check-cljs-repl-requirements t
"When non-nil will run the requirement checks for the different cljs repls.
Generally you should not disable this unless you run into some faulty check."
:type 'boolean
:safe #'booleanp
:package-version '(cider . "0.17.0"))
(defun cider-verify-clojurescript-is-present ()
"Check whether ClojureScript is present."
(unless (cider-library-present-p "cljs.core")
(user-error "ClojureScript is not available. See https://docs.cider.mx/cider/basics/clojurescript for details")))
(defun cider-verify-piggieback-is-present ()
"Check whether the piggieback middleware is present."
(unless (cider-library-present-p "cider.piggieback")
(user-error "Piggieback 0.4.x (aka cider/piggieback) is not available. See https://docs.cider.mx/cider/basics/clojurescript for details")))
(defun cider-check-node-requirements ()
"Check whether we can start a Node ClojureScript REPL."
(cider-verify-piggieback-is-present)
(unless (executable-find "node")
(user-error "Node.js is not present on the exec-path. Make sure you've installed it and your exec-path is properly set")))
(defun cider-check-figwheel-requirements ()
"Check whether we can start a Figwheel ClojureScript REPL."
(cider-verify-piggieback-is-present)
(unless (cider-library-present-p "figwheel-sidecar.repl")
(user-error "Figwheel-sidecar is not available. Please check https://docs.cider.mx/cider/basics/clojurescript for details")))
(defun cider-check-figwheel-main-requirements ()
"Check whether we can start a Figwheel ClojureScript REPL."
(cider-verify-piggieback-is-present)
(unless (cider-library-present-p "figwheel.main")
(user-error "Figwheel-main is not available. Please check https://docs.cider.mx/cider/basics/clojurescript for details")))
(defun cider-check-weasel-requirements ()
"Check whether we can start a Weasel ClojureScript REPL."
(cider-verify-piggieback-is-present)
(unless (cider-library-present-p "weasel.repl.server")
(user-error "Weasel in not available. Please check https://docs.cider.mx/cider/basics/clojurescript/#browser-connected-clojurescript-repl for details")))
(defun cider-check-boot-requirements ()
"Check whether we can start a Boot ClojureScript REPL."
(cider-verify-piggieback-is-present)
(unless (cider-library-present-p "adzerk.boot-cljs-repl")
(user-error "The Boot ClojureScript REPL is not available. Please check https://github.com/adzerk-oss/boot-cljs-repl/blob/master/README.md for details")))
(defun cider-check-krell-requirements ()
"Check whether we can start a Krell ClojureScript REPL."
(cider-verify-piggieback-is-present)
(unless (cider-library-present-p "krell.repl")
(user-error "The Krell ClojureScript REPL is not available. Please check https://github.com/vouch-opensource/krell for details")))
(defun cider-check-shadow-cljs-requirements ()
"Check whether we can start a shadow-cljs REPL."
(unless (cider-library-present-p "shadow.cljs.devtools.api")
(user-error "The shadow-cljs ClojureScript REPL is not available. Please check https://docs.cider.mx/cider/basics/clojurescript for details")))
(defun cider-normalize-cljs-init-options (options)
"Normalize the OPTIONS string used for initializing a ClojureScript REPL."
(if (or (string-prefix-p "{" options)
(string-prefix-p "(" options)
(string-prefix-p "[" options)
(string-prefix-p ":" options)
(string-prefix-p "\"" options))
options
(concat ":" options)))
(defcustom cider-shadow-watched-builds nil
"Defines the list of builds `shadow-cljs' should watch."
:type '(repeat string)
:safe #'listp
:package-version '(cider . "1.0"))
(defcustom cider-shadow-default-options nil
"Defines default `shadow-cljs' options."
:type 'string
:safe (lambda (s) (or (null s) (stringp s)))
:package-version '(cider . "0.18.0"))
(defun cider--shadow-parse-builds (hash)
"Parses the build names of a shadow-cljs.edn HASH map.
The default options of `browser-repl' and `node-repl' are also included."
(let* ((builds (when (hash-table-p hash)
(gethash :builds hash)))
(build-keys (when (hash-table-p builds)
(hash-table-keys builds))))
(append build-keys '(browser-repl node-repl))))
(defun cider--shadow-get-builds ()
"Extract build names from the shadow-cljs.edn config file in the project root."
(let ((shadow-edn (concat (clojure-project-dir) "shadow-cljs.edn")))
(when (file-exists-p shadow-edn)
(with-temp-buffer
(insert-file-contents shadow-edn)
(let ((hash (car (parseedn-read '((shadow/env . identity))))))
(cider--shadow-parse-builds hash))))))
(defun cider-shadow-select-cljs-init-form ()
"Generate the init form for a shadow-cljs select-only REPL.
We have to prompt the user to select a build, that's why this is a command,
not just a string."
(let ((form "(do (require '[shadow.cljs.devtools.api :as shadow]) (shadow/nrepl-select %s))")
(options (or cider-shadow-default-options
(completing-read "Select shadow-cljs build: "
(cider--shadow-get-builds)))))
(format form (cider-normalize-cljs-init-options options))))
(defun cider-shadow-cljs-init-form ()
"Generate the init form for a shadow-cljs REPL.
We have to prompt the user to select a build, that's why
this is a command, not just a string."
(let* ((shadow-require "(require '[shadow.cljs.devtools.api :as shadow])")
(default-build (cider-normalize-cljs-init-options
(or cider-shadow-default-options
(car cider-shadow-watched-builds)
(completing-read "Select shadow-cljs build: "
(cider--shadow-get-builds)))))
(watched-builds (or (mapcar #'cider-normalize-cljs-init-options cider-shadow-watched-builds)
(list default-build)))
(watched-builds-form (mapconcat (lambda (build) (format "(shadow/watch %s)" build))
watched-builds
" "))
;; form used for user-defined builds
(user-build-form "(do %s %s (shadow/nrepl-select %s))")
;; form used for built-in builds like :browser-repl and :node-repl
(default-build-form "(do %s (shadow/%s))"))
(if (member default-build '(":browser-repl" ":node-repl"))
(format default-build-form shadow-require (string-remove-prefix ":" default-build))
(format user-build-form shadow-require watched-builds-form default-build))))
(defcustom cider-figwheel-main-default-options nil
"Defines the `figwheel.main/start' options.
Note that figwheel-main/start can also accept a map of options, refer to
Figwheel for details."
:type 'string
:safe (lambda (s) (or (null s) (stringp s)))
:package-version '(cider . "0.18.0"))
(defun cider--figwheel-main-get-builds ()
"Extract build names from the <build-id>.cljs.edn config files.
Fetches them in the project root."
(when-let ((project-dir (clojure-project-dir)))
(let ((builds (directory-files project-dir nil ".*\\.cljs\\.edn")))
(mapcar (lambda (f) (string-match "^\\(.*\\)\\.cljs\\.edn" f)
(match-string 1 f))
builds))))
(defun cider-figwheel-main-init-form ()
"Produce the figwheel-main ClojureScript init form."
(let ((form "(do (require 'figwheel.main) (figwheel.main/start %s))")
(builds (cider--figwheel-main-get-builds)))
(cond
(cider-figwheel-main-default-options
(format form (cider-normalize-cljs-init-options (string-trim cider-figwheel-main-default-options))))
(builds
(format form (cider-normalize-cljs-init-options (completing-read "Select figwheel-main build: " builds))))
(t (user-error "No figwheel-main build files (<build-id>.cljs.edn) were found")))))
(defcustom cider-custom-cljs-repl-init-form nil
"The form used to start a custom ClojureScript REPL.
When set it becomes the return value of the `cider-custom-cljs-repl-init-form'
function, which normally prompts for the init form.
This defcustom is mostly intended for use with .dir-locals.el for
cases where it doesn't make sense to register a new ClojureScript REPL type."
:type 'string
:safe (lambda (s) (or (null s) (stringp s)))
:package-version '(cider . "0.23.0"))
(defun cider-custom-cljs-repl-init-form ()
"The form used to start a custom ClojureScript REPL.
Defaults to the value of `cider-custom-cljs-repl-init-form'.
If it's nil the function will prompt for a form.
The supplied string will be wrapped in a do form if needed."
(or
cider-custom-cljs-repl-init-form
(let ((form (read-from-minibuffer "Please, provide a form to start a ClojureScript REPL: ")))
;; TODO: We should probably make this more robust (e.g. by using a regexp or
;; parsing the form).
(if (string-prefix-p "(do" form)
form
(format "(do %s)" form)))))
(defvar cider-cljs-repl-types
'((figwheel "(do (require 'figwheel-sidecar.repl-api) (figwheel-sidecar.repl-api/start-figwheel!) (figwheel-sidecar.repl-api/cljs-repl))"
cider-check-figwheel-requirements)
(figwheel-main cider-figwheel-main-init-form cider-check-figwheel-main-requirements)
(figwheel-connected "(figwheel-sidecar.repl-api/cljs-repl)"
cider-check-figwheel-requirements)
(browser "(do (require 'cljs.repl.browser) (cider.piggieback/cljs-repl (cljs.repl.browser/repl-env)))")
(node "(do (require 'cljs.repl.node) (cider.piggieback/cljs-repl (cljs.repl.node/repl-env)))"
cider-check-node-requirements)
(weasel "(do (require 'weasel.repl.websocket) (cider.piggieback/cljs-repl (weasel.repl.websocket/repl-env :ip \"127.0.0.1\" :port 9001)))"
cider-check-weasel-requirements)
(boot "(do (require 'adzerk.boot-cljs-repl) (adzerk.boot-cljs-repl/start-repl))"
cider-check-boot-requirements)
(shadow cider-shadow-cljs-init-form cider-check-shadow-cljs-requirements)
(shadow-select cider-shadow-select-cljs-init-form cider-check-shadow-cljs-requirements)
(krell "(require '[clojure.edn :as edn]
'[clojure.java.io :as io]
'[cider.piggieback]
'[krell.api :as krell]
'[krell.repl])
(def config (edn/read-string (slurp (io/file \"build.edn\"))))
(apply cider.piggieback/cljs-repl (krell.repl/repl-env) (mapcat identity config))"
cider-check-krell-requirements)
(custom cider-custom-cljs-repl-init-form nil))
"A list of supported ClojureScript REPLs.
For each one we have its name, the form we need to evaluate in a Clojure
REPL to start the ClojureScript REPL and functions to verify their requirements.
The form should be either a string or a function producing a string.")
(defun cider-register-cljs-repl-type (type init-form &optional requirements-fn)
"Register a new ClojureScript REPL type.
Types are defined by the following:
- TYPE - symbol identifier that will be used to refer to the REPL type
- INIT-FORM - string or function (symbol) producing string
- REQUIREMENTS-FN - function to check whether the REPL can be started.
This param is optional.
All this function does is modifying `cider-cljs-repl-types'.
It's intended to be used in your Emacs config."
(unless (symbolp type)
(user-error "The REPL type must be a symbol"))
(unless (or (stringp init-form) (symbolp init-form))
(user-error "The init form must be a string or a symbol referring to a function"))
(unless (or (null requirements-fn) (symbolp requirements-fn))
(user-error "The requirements-fn must be a symbol referring to a function"))
(add-to-list 'cider-cljs-repl-types (list type init-form requirements-fn)))
(defcustom cider-default-cljs-repl nil
"The default ClojureScript REPL to start.
This affects commands like `cider-jack-in-cljs'. Generally it's
intended to be set via .dir-locals.el for individual projects, as its
relatively unlikely you'd like to use the same type of REPL in each project
you're working on."
:type '(choice (const :tag "Figwheel" figwheel)
(const :tag "Figwheel Main" figwheel-main)
(const :tag "Browser" browser)
(const :tag "Node" node)
(const :tag "Weasel" weasel)
(const :tag "Boot" boot)
(const :tag "Shadow" shadow)
(const :tag "Shadow w/o Server" shadow-select)
(const :tag "Krell" krell)
(const :tag "Custom" custom))
:safe #'symbolp
:package-version '(cider . "0.17.0"))
(make-obsolete-variable 'cider-cljs-lein-repl 'cider-default-cljs-repl "0.17")
(make-obsolete-variable 'cider-cljs-boot-repl 'cider-default-cljs-repl "0.17")
(make-obsolete-variable 'cider-cljs-gradle-repl 'cider-default-cljs-repl "0.17")
(defvar cider--select-cljs-repl-history nil)
(defun cider-select-cljs-repl (&optional default)
"Select the ClojureScript REPL to use with `cider-jack-in-cljs'.
DEFAULT is the default ClojureScript REPL to offer in completion."
(let ((repl-types (mapcar #'car cider-cljs-repl-types)))
(intern (completing-read "Select ClojureScript REPL type: " repl-types
nil nil nil 'cider--select-cljs-repl-history
(or default (car cider--select-cljs-repl-history))))))
(defun cider-cljs-repl-form (repl-type)
"Get the cljs REPL form for REPL-TYPE."
(if-let* ((repl-form (cadr (seq-find
(lambda (entry)
(eq (car entry) repl-type))
cider-cljs-repl-types))))
;; repl-form can be either a string or a function producing a string
(if (symbolp repl-form)
(funcall repl-form)
repl-form)
(user-error "No ClojureScript REPL type %s found. Please make sure that `cider-cljs-repl-types' has an entry for it" repl-type)))
(defun cider-verify-cljs-repl-requirements (&optional repl-type)
"Verify that the requirements for REPL-TYPE are met.
Return REPL-TYPE if requirements are met."
(let ((repl-type (or repl-type
cider-default-cljs-repl
(cider-select-cljs-repl))))
(when cider-check-cljs-repl-requirements
(when-let* ((fun (nth 2 (seq-find
(lambda (entry)
(eq (car entry) repl-type))
cider-cljs-repl-types))))
(funcall fun)))
repl-type))
(defun cider--check-cljs (&optional cljs-type no-error)
"Verify that all cljs requirements are met for CLJS-TYPE connection.
Return REPL-TYPE of requirement are met, and throw an ‘user-error’ otherwise.
When NO-ERROR is non-nil, don't throw an error, issue a message and return
nil."
(if no-error
(condition-case ex
(progn
(cider-verify-clojurescript-is-present)
(cider-verify-cljs-repl-requirements cljs-type))
(error
(message "Invalid ClojureScript dependency: %S" ex)
nil))
(cider-verify-clojurescript-is-present)
(cider-verify-cljs-repl-requirements cljs-type)))
(defun cider--offer-to-open-app-in-browser (server-buffer)
"Look for a server address in SERVER-BUFFER and offer to open it."
(when (buffer-live-p server-buffer)
(with-current-buffer server-buffer
(save-excursion
(goto-char (point-min))
(when-let* ((url (and (search-forward-regexp "http://localhost:[0-9]+" nil 'noerror)
(match-string 0))))
(when (y-or-n-p (format "Visit ‘%s’ in a browser? " url))
(browse-url url)))))))
;;; User Level Connectors
;;;###autoload (autoload 'cider-start-map "cider" "CIDER jack-in and connect keymap." t 'keymap)
(defvar cider-start-map
(let ((map (define-prefix-command 'cider-start-map)))
(define-key map (kbd "x") #'cider)
(define-key map (kbd "C-x") #'cider)
(define-key map (kbd "j j") #'cider-jack-in-clj)
(define-key map (kbd "j s") #'cider-jack-in-cljs)
(define-key map (kbd "j m") #'cider-jack-in-clj&cljs)
(define-key map (kbd "C-j j") #'cider-jack-in-clj)
(define-key map (kbd "C-j s") #'cider-jack-in-cljs)
(define-key map (kbd "C-j m") #'cider-jack-in-clj&cljs)
(define-key map (kbd "C-j C-j") #'cider-jack-in-clj)
(define-key map (kbd "C-j C-s") #'cider-jack-in-cljs)
(define-key map (kbd "C-j C-m") #'cider-jack-in-clj&cljs)
(define-key map (kbd "c j") #'cider-connect-clj)
(define-key map (kbd "c s") #'cider-connect-cljs)
(define-key map (kbd "c m") #'cider-connect-clj&cljs)
(define-key map (kbd "C-c j") #'cider-connect-clj)
(define-key map (kbd "C-c s") #'cider-connect-cljs)
(define-key map (kbd "C-c m") #'cider-connect-clj&cljs)
(define-key map (kbd "C-c C-j") #'cider-connect-clj)
(define-key map (kbd "C-c C-s") #'cider-connect-cljs)
(define-key map (kbd "C-c C-m") #'cider-connect-clj&cljs)
(define-key map (kbd "s j") #'cider-connect-sibling-clj)
(define-key map (kbd "s s") #'cider-connect-sibling-cljs)
(define-key map (kbd "C-s j") #'cider-connect-sibling-clj)
(define-key map (kbd "C-s s") #'cider-connect-sibling-cljs)
(define-key map (kbd "C-s C-j") #'cider-connect-sibling-clj)
(define-key map (kbd "C-s C-s") #'cider-connect-sibling-cljs)
map)
"CIDER jack-in and connect keymap.")
;;;###autoload
(defun cider-jack-in-clj (params)
"Start an nREPL server for the current project and connect to it.
PARAMS is a plist optionally containing :project-dir and :jack-in-cmd.
With the prefix argument, allow editing of the jack in command; with a
double prefix prompt for all these parameters."
(interactive "P")
(let ((params (thread-first
params
(cider--update-project-dir)
(cider--check-existing-session)
(cider--update-jack-in-cmd))))
(nrepl-start-server-process
(plist-get params :project-dir)
(plist-get params :jack-in-cmd)
(lambda (server-buffer)
(cider-connect-sibling-clj params server-buffer)))))
;;;###autoload
(defun cider-jack-in-cljs (params)
"Start an nREPL server for the current project and connect to it.
PARAMS is a plist optionally containing :project-dir, :jack-in-cmd and
:cljs-repl-type (e.g. Node, Figwheel, etc). With the prefix argument,
allow editing of the jack in command; with a double prefix prompt for all
these parameters."
(interactive "P")
(let ((cider-jack-in-dependencies (append cider-jack-in-dependencies cider-jack-in-cljs-dependencies))
(cider-jack-in-lein-plugins (append cider-jack-in-lein-plugins cider-jack-in-cljs-lein-plugins))
(cider-jack-in-nrepl-middlewares (append cider-jack-in-nrepl-middlewares cider-jack-in-cljs-nrepl-middlewares))
(orig-buffer (current-buffer)))
;; cider--update-jack-in-cmd relies indirectly on the above dynamic vars
(let ((params (thread-first
params
(cider--update-project-dir)
(cider--check-existing-session)
(cider--update-jack-in-cmd))))
(nrepl-start-server-process
(plist-get params :project-dir)
(plist-get params :jack-in-cmd)
(lambda (server-buffer)
(with-current-buffer orig-buffer
(cider-connect-sibling-cljs params server-buffer)))))))
;;;###autoload
(defun cider-jack-in-clj&cljs (&optional params soft-cljs-start)
"Start an nREPL server and connect with clj and cljs REPLs.
PARAMS is a plist optionally containing :project-dir, :jack-in-cmd and
:cljs-repl-type (e.g. Node, Figwheel, etc). With the prefix argument,
allow for editing of the jack in command; with a double prefix prompt for
all these parameters. When SOFT-CLJS-START is non-nil, start cljs REPL
only when the ClojureScript dependencies are met."
(interactive "P")
(let ((cider-jack-in-dependencies (append cider-jack-in-dependencies cider-jack-in-cljs-dependencies))
(cider-jack-in-lein-plugins (append cider-jack-in-lein-plugins cider-jack-in-cljs-lein-plugins))
(cider-jack-in-nrepl-middlewares (append cider-jack-in-nrepl-middlewares cider-jack-in-cljs-nrepl-middlewares))
(orig-buffer (current-buffer)))
;; cider--update-jack-in-cmd relies indirectly on the above dynamic vars
(let ((params (thread-first
params
(cider--update-project-dir)
(cider--check-existing-session)
(cider--update-jack-in-cmd)
(cider--update-cljs-type)
;; already asked, don't ask on sibling connect
(plist-put :do-prompt nil))))
(nrepl-start-server-process
(plist-get params :project-dir)
(plist-get params :jack-in-cmd)
(lambda (server-buffer)
(with-current-buffer orig-buffer
(let ((clj-repl (cider-connect-sibling-clj params server-buffer)))
(if soft-cljs-start
(when (cider--check-cljs (plist-get params :cljs-repl-type) 'no-error)
(cider-connect-sibling-cljs params clj-repl))
(cider-connect-sibling-cljs params clj-repl)))))))))
;;;###autoload
(defun cider-connect-sibling-clj (params &optional other-repl)
"Create a Clojure REPL with the same server as OTHER-REPL.
PARAMS is for consistency with other connection commands and is currently
ignored. OTHER-REPL defaults to `cider-current-repl' and in programs can
also be a server buffer, in which case a new session with a REPL for that
server is created."
(interactive "P")
(cider-nrepl-connect
(let* ((other-repl (or other-repl (cider-current-repl 'any 'ensure)))
(other-params (cider--gather-connect-params nil other-repl))
(ses-name (unless (nrepl-server-p other-repl)
(sesman-session-name-for-object 'CIDER other-repl))))
(thread-first
params
(cider--update-do-prompt)
(append other-params)
(plist-put :repl-init-function nil)
(plist-put :repl-type 'clj)
(plist-put :session-name ses-name)))))
;;;###autoload
(defun cider-connect-sibling-cljs (params &optional other-repl)
"Create a ClojureScript REPL with the same server as OTHER-REPL.
PARAMS is a plist optionally containing :cljs-repl-type (e.g. Node,
Figwheel, etc). All other parameters are inferred from the OTHER-REPL.
OTHER-REPL defaults to `cider-current-repl' but in programs can also be a
server buffer, in which case a new session for that server is created."
(interactive "P")
(let* ((other-repl (or other-repl (cider-current-repl 'any 'ensure)))
(other-params (cider--gather-connect-params nil other-repl))
(ses-name (unless (nrepl-server-p other-repl)
(sesman-session-name-for-object 'CIDER other-repl))))
(cider-nrepl-connect
(thread-first
params
(cider--update-do-prompt)
(append other-params)
(cider--update-cljs-type)
(cider--update-cljs-init-function)
(plist-put :session-name ses-name)
(plist-put :repl-type 'pending-cljs)))))
;;;###autoload
(defun cider-connect-clj (&optional params)
"Initialize a Clojure connection to an nREPL server.
PARAMS is a plist optionally containing :host, :port and :project-dir. On
prefix argument, prompt for all the parameters."
(interactive "P")
(cider-nrepl-connect
(thread-first
params
(cider--update-project-dir)
(cider--update-host-port)
(cider--check-existing-session)
(plist-put :repl-init-function nil)
(plist-put :session-name nil)
(plist-put :repl-type 'clj))))
;;;###autoload
(defun cider-connect-cljs (&optional params)
"Initialize a ClojureScript connection to an nREPL server.
PARAMS is a plist optionally containing :host, :port, :project-dir and
:cljs-repl-type (e.g. Node, Figwheel, etc). On prefix, prompt for all the
parameters regardless of their supplied or default values."
(interactive "P")
(cider-nrepl-connect
(thread-first
params
(cider--update-project-dir)
(cider--update-host-port)
(cider--check-existing-session)
(cider--update-cljs-type)
(cider--update-cljs-init-function)
(plist-put :session-name nil)
(plist-put :repl-type 'pending-cljs))))
;;;###autoload
(defun cider-connect-clj&cljs (params &optional soft-cljs-start)
"Initialize a Clojure and ClojureScript connection to an nREPL server.
PARAMS is a plist optionally containing :host, :port, :project-dir and
:cljs-repl-type (e.g. Node, Figwheel, etc). When SOFT-CLJS-START is
non-nil, don't start if ClojureScript requirements are not met."
(interactive "P")
(let* ((params (thread-first
params
(cider--update-project-dir)
(cider--update-host-port)
(cider--check-existing-session)
(cider--update-cljs-type)))
(clj-repl (cider-connect-clj params)))
(if soft-cljs-start
(when (cider--check-cljs (plist-get params :cljs-repl-type) 'no-error)
(cider-connect-sibling-cljs params clj-repl))
(cider-connect-sibling-cljs params clj-repl))))
(defvar cider-connection-init-commands
'(cider-jack-in-clj
cider-jack-in-cljs
cider-jack-in-clj&cljs
cider-connect-clj
cider-connect-cljs
cider-connect-clj&cljs
cider-connect-sibling-clj
cider-connect-sibling-cljs)
"A list of all user-level connection init commands in CIDER.")
;;;###autoload
(defun cider ()
"Start a connection of any type interactively."
(interactive)
(when-let* ((command (intern (completing-read "Select command: " cider-connection-init-commands))))
(call-interactively command)))
;;; PARAMS updating
(defun cider--update-do-prompt (params)
"Update :do-prompt in PARAMS."
(cond ((equal params '(4)) (list :edit-jack-in-command t))
((equal params '(16)) (list :do-prompt t))
(t params)))
(defun cider--update-project-dir (params)
"Update :project-dir in PARAMS."
(let* ((params (cider--update-do-prompt params))
(proj-dir (if (plist-get params :do-prompt)
(read-directory-name "Project: "
(clojure-project-dir (cider-current-dir)))
(plist-get params :project-dir)))
(orig-buffer (current-buffer)))
(if (or (null proj-dir)
(file-in-directory-p default-directory proj-dir))
(plist-put params :project-dir
(or proj-dir
(clojure-project-dir (cider-current-dir))))
;; If proj-dir is not a parent of default-directory, transfer all local
;; variables and hack dir-local variables into a temporary buffer and keep
;; that buffer within `params` for the later use by other --update-
;; functions. The context buffer should not be used outside of the param
;; initialization pipeline. Therefore, we don't bother with making it
;; unique or killing it anywhere.
(let ((context-buf-name " *cider-context-buffer*"))
(when (get-buffer context-buf-name)
(kill-buffer context-buf-name))
(with-current-buffer (get-buffer-create context-buf-name)
(dolist (pair (buffer-local-variables orig-buffer))
(pcase pair
(`(,name . ,value) ;ignore unbound variables
(ignore-errors (set (make-local-variable name) value))))
(setq-local buffer-file-name nil))
(let ((default-directory proj-dir))
(hack-dir-local-variables-non-file-buffer)
(thread-first
params
(plist-put :project-dir proj-dir)
(plist-put :--context-buffer (current-buffer)))))))))
(defun cider--update-cljs-type (params)
"Update :cljs-repl-type in PARAMS."
(with-current-buffer (or (plist-get params :--context-buffer)
(current-buffer))
(let ((params (cider--update-do-prompt params))
(inferred-type (or (plist-get params :cljs-repl-type)
cider-default-cljs-repl)))
(plist-put params :cljs-repl-type
(if (plist-get params :do-prompt)
(cider-select-cljs-repl inferred-type)
(or inferred-type
(cider-select-cljs-repl)))))))
(defcustom cider-edit-jack-in-command nil
"When truthy allow the user to edit the command."
:type 'boolean
:safe #'booleanp
:version '(cider . "0.22.0"))
(defvar cider--jack-in-nrepl-params-history nil
"History list for user-specified jack-in nrepl command params.")
(defvar cider--jack-in-cmd-history nil
"History list for user-specified jack-in commands.")
(defun cider--powershell-encode-command (cmd-params)
"Base64 encode the powershell command and jack-in CMD-PARAMS for clojure-cli."
(let* ((quoted-params (replace-regexp-in-string "\"" "\"\"" cmd-params))
(command (format "clojure %s" quoted-params))
(utf-16le-command (encode-coding-string command 'utf-16le)))
(format "-encodedCommand %s" (base64-encode-string utf-16le-command t))))
(defun cider--update-jack-in-cmd (params)
"Update :jack-in-cmd key in PARAMS."
(let* ((params (cider--update-do-prompt params))
(project-dir (plist-get params :project-dir))
(project-type (cider-project-type project-dir))
(command (cider-jack-in-command project-type))
(command-resolved (cider-jack-in-resolve-command project-type))
(command-global-opts (cider-jack-in-global-options project-type))
(command-params (cider-jack-in-params project-type)))
(if command-resolved
(with-current-buffer (or (plist-get params :--context-buffer)
(current-buffer))
(let* ((command-params (if (plist-get params :do-prompt)
(read-string "nREPL server command: "
command-params
'cider--jack-in-nrepl-params-history)
command-params))
(cmd-params (if cider-inject-dependencies-at-jack-in
(cider-inject-jack-in-dependencies command-global-opts command-params project-type)
command-params)))
(if (or project-dir cider-allow-jack-in-without-project)
(when (or project-dir
(eq cider-allow-jack-in-without-project t)
(and (null project-dir)
(eq cider-allow-jack-in-without-project 'warn)
(y-or-n-p "Are you sure you want to run `cider-jack-in' without a Clojure project? ")))
(let ((cmd (format "%s %s" command-resolved (if (or (string-equal command "powershell")
(string-equal command "pwsh"))
(cider--powershell-encode-command cmd-params)
cmd-params))))
(plist-put params :jack-in-cmd (if (or cider-edit-jack-in-command
(plist-get params :edit-jack-in-command))
(read-string "jack-in command: " cmd 'cider--jack-in-cmd-history)
cmd))))
(user-error "`cider-jack-in' is not allowed without a Clojure project"))))
(user-error "The %s executable isn't on your `exec-path'" command))))
(defun cider--update-host-port (params)
"Update :host and :port; or :socket-file in PARAMS."
(with-current-buffer (or (plist-get params :--context-buffer)
(current-buffer))
(let* ((params (cider--update-do-prompt params))
(host (plist-get params :host))
(port (plist-get params :port))
(endpoint (if (plist-get params :do-prompt)
(cider-select-endpoint)
(if (and host port)
(cons host port)
(cider-select-endpoint)))))
(if (equal "local-unix-domain-socket" (car endpoint))
(plist-put params :socket-file (cdr endpoint))
(thread-first
params
(plist-put :host (car endpoint))
(plist-put :port (cdr endpoint)))))))
(defun cider--update-cljs-init-function (params)
"Update PARAMS :repl-init-function for cljs connections."
(with-current-buffer (or (plist-get params :--context-buffer)
(current-buffer))
(let* ((cljs-type (plist-get params :cljs-repl-type))
(repl-init-form (cider-cljs-repl-form cljs-type)))
(thread-first
params
(plist-put :repl-init-function
(lambda ()
(cider--check-cljs cljs-type)
;; FIXME: ideally this should be done in the state handler
(setq-local cider-cljs-repl-type cljs-type)
(cider-nrepl-send-request
(list "op" "eval"
"ns" (cider-current-ns)
"code" repl-init-form)
(cider-repl-handler (current-buffer)))
(when (and (buffer-live-p nrepl-server-buffer)
cider-offer-to-open-cljs-app-in-browser)
(cider--offer-to-open-app-in-browser nrepl-server-buffer))))
(plist-put :repl-init-form repl-init-form)))))
(defun cider--check-existing-session (params)
"Ask for confirmation if a session with similar PARAMS already exists.
If no session exists or user chose to proceed, return PARAMS. If the user
canceled the action, signal quit."
(let* ((proj-dir (plist-get params :project-dir))
(host (plist-get params :host))
(port (plist-get params :port))
(session (seq-find (lambda (ses)
(let ((ses-params (cider--gather-session-params ses)))
(and (equal proj-dir (plist-get ses-params :project-dir))
(or (null port)
(equal port (plist-get ses-params :port)))
(or (null host)
(equal host (plist-get ses-params :host))))))
(sesman-current-sessions 'CIDER '(project)))))
(when session
(unless (y-or-n-p
(concat
"A CIDER session with the same connection parameters already exists (" (car session) "). "
"Are you sure you want to create a new session instead of using `cider-connect-sibling-clj(s)'? "))
(let ((debug-on-quit nil))
(signal 'quit nil)))))
params)
;;; Aliases
;;;###autoload
(defalias 'cider-jack-in #'cider-jack-in-clj)
;;;###autoload
(defalias 'cider-connect #'cider-connect-clj)
;;; Helpers
(defun cider-current-host ()
"Retrieve the current host."
(or (when (stringp buffer-file-name)
(file-remote-p buffer-file-name 'host))
"localhost"))
(defun cider-select-endpoint ()
"Interactively select the host and port to connect to."
(dolist (endpoint cider-known-endpoints)
(unless (stringp (or (nth 2 endpoint)
(nth 1 endpoint)))
(user-error "The port for %s in `cider-known-endpoints' should be a string"
(nth 0 endpoint))))
(let* ((ssh-hosts (cider--ssh-hosts))
(hosts (seq-uniq (append (when cider-host-history
;; history elements are strings of the form "host:port"
(list (split-string (car cider-host-history) ":")))
(list (list (cider-current-host)))
cider-known-endpoints
ssh-hosts
;; always add localhost
'(("localhost")
("local-unix-domain-socket")))))
(sel-host (cider--completing-read-host hosts))
(host (car sel-host))
(port (or (cadr sel-host)
(if (equal host "local-unix-domain-socket")
(cider--completing-read-socket-file)
(cider--completing-read-port host (cider--infer-ports host ssh-hosts))))))
(cons host port)))
(defun cider--ssh-hosts ()
"Retrieve all ssh host from local configuration files."
(seq-map (lambda (s) (list (replace-regexp-in-string ":$" "" s)))
;; `tramp-completion-mode' is obsoleted in 26
(cl-progv (if (version< emacs-version "26")
'(tramp-completion-mode)
'(non-essential)) '(t)
(tramp-completion-handle-file-name-all-completions "" "/ssh:"))))
(defun cider--completing-read-host (hosts)
"Interactively select host from HOSTS.
Each element in HOSTS is one of: (host), (host port) or (label host port).
Return a list of the form (HOST PORT), where PORT can be nil."
(let* ((hosts (cider-join-into-alist hosts))
(sel-host (completing-read "Host: " hosts nil nil nil
'cider-host-history (caar hosts)))
(host (or (cdr (assoc sel-host hosts)) (list sel-host))))
;; remove the label
(if (= 3 (length host)) (cdr host) host)))
(defun cider--tramp-file-name (vec)
"A simple compatibility wrapper around `make-tramp-file-name'.
Tramp version starting 26.1 is using a `cl-defstruct' rather than vanilla VEC."
(if (version< emacs-version "26.1")
vec
(with-no-warnings
(make-tramp-file-name :method (elt vec 0)
:host (elt vec 2)))))
(defcustom cider-infer-remote-nrepl-ports nil
"When true, cider will use ssh to try to infer nREPL ports on remote hosts."
:type 'boolean
:safe #'booleanp
:package-version '(cider . "0.19.0"))
(defun cider--infer-ports (host ssh-hosts)
"Infer nREPL ports on HOST.
Return a list of elements of the form (directory port). SSH-HOSTS is a list
of remote SSH hosts."
(let ((localp (or (nrepl-local-host-p host)
(not (assoc-string host ssh-hosts)))))
(if localp
;; change dir: current file might be remote
(let* ((change-dir-p (file-remote-p default-directory))
(default-directory (if change-dir-p "~/" default-directory)))
(cider-locate-running-nrepl-ports (unless change-dir-p default-directory)))
(when cider-infer-remote-nrepl-ports
(let ((vec (vector "sshx" nil host "" nil))
;; change dir: user might want to connect to a different remote
(dir (when (file-remote-p default-directory)
(with-parsed-tramp-file-name default-directory cur
(when (string= cur-host host) default-directory)))))
(tramp-maybe-open-connection (cider--tramp-file-name vec))
(with-current-buffer (tramp-get-connection-buffer (cider--tramp-file-name vec))
(cider-locate-running-nrepl-ports dir)))))))
(defun cider--completing-read-port (host ports)
"Interactively select port for HOST from PORTS."
(let* ((ports (cider-join-into-alist ports))
(sel-port (completing-read (format "Port for %s: " host) ports
nil nil nil nil (caar ports)))
(port (or (cdr (assoc sel-port ports)) sel-port))
(port (if (listp port) (cadr port) port)))
(if (stringp port) (string-to-number port) port)))
(defun cider--completing-read-socket-file ()
"Interactively select unix domain socket file name."
(read-file-name "Socket File: " nil nil t nil
(lambda (filename)
"Predicate: auto-complete only socket-files and directories"
(let ((filetype (string-to-char
(file-attribute-modes
(file-attributes
filename)))))
(or (eq ?s filetype)
(eq ?d filetype))))))
(defun cider-locate-running-nrepl-ports (&optional dir)
"Locate ports of running nREPL servers.
When DIR is non-nil also look for nREPL port files in DIR. Return a list
of list of the form (project-dir port)."
(let* ((paths (cider--running-nrepl-paths))
(proj-ports (apply #'append
(mapcar (lambda (d)
(mapcar (lambda (p) (list (file-name-nondirectory (directory-file-name d)) p))
(and d (nrepl-extract-ports (cider--file-path d)))))
(cons (clojure-project-dir dir) paths)))))
(seq-uniq (delq nil proj-ports))))
(defun cider--running-nrepl-paths ()
"Retrieve project paths of running nREPL servers.
Use `cider-ps-running-nrepls-command' and
`cider-ps-running-nrepl-path-regexp-list'."
(let (paths)
(with-temp-buffer
(insert (shell-command-to-string cider-ps-running-nrepls-command))
(dolist (regexp cider-ps-running-nrepl-path-regexp-list)
(goto-char 1)
(while (re-search-forward regexp nil t)
(setq paths (cons (match-string 1) paths)))))
(seq-uniq paths)))
(defun cider--identify-buildtools-present (&optional project-dir)
"Identify build systems present by their build files in PROJECT-DIR.
PROJECT-DIR defaults to current project."
(let* ((default-directory (or project-dir (clojure-project-dir (cider-current-dir))))
(build-files '((lein . "project.clj")
(boot . "build.boot")
(clojure-cli . "deps.edn")
(babashka . "bb.edn")
(shadow-cljs . "shadow-cljs.edn")
(gradle . "build.gradle")
(gradle . "build.gradle.kts"))))
(delq nil
(mapcar (lambda (candidate)
(when (file-exists-p (cdr candidate))
(car candidate)))
build-files))))
(defun cider-project-type (&optional project-dir)
"Determine the type of the project in PROJECT-DIR.
When multiple project file markers are present, check for a preferred build
tool in `cider-preferred-build-tool', otherwise prompt the user to choose.
PROJECT-DIR defaults to the current project."
(let* ((choices (cider--identify-buildtools-present project-dir))
(multiple-project-choices (> (length choices) 1))
;; this needs to be a string to be used in `completing-read'
(default (symbol-name (car choices)))
;; `cider-preferred-build-tool' used to be a string prior to CIDER
;; 0.18, therefore the need for `cider-maybe-intern'
(preferred-build-tool (cider-maybe-intern cider-preferred-build-tool)))
(cond ((and multiple-project-choices
(member preferred-build-tool choices))
preferred-build-tool)
(multiple-project-choices
(intern
(completing-read
(format "Which command should be used (default %s): " default)
choices nil t nil nil default)))
(choices
(car choices))
;; TODO: Move this fallback outside the project-type check
;; if we're outside a project we fallback to whatever tool
;; is specified in `cider-jack-in-default' (normally clojure-cli)
;; `cider-jack-in-default' used to be a string prior to CIDER
;; 0.18, therefore the need for `cider-maybe-intern'
(t (cider-maybe-intern cider-jack-in-default)))))
;; TODO: Implement a check for command presence over tramp
(defun cider--resolve-command (command)
"Find COMMAND in exec path (see variable `exec-path').
Return nil if not found. In case `default-directory' is non-local we
assume the command is available."
(when-let* ((command (or (and (file-remote-p default-directory) command)
(executable-find command)
(executable-find (concat command ".bat")))))
(shell-quote-argument command)))
(defun cider--resolve-project-command (command)
"Find COMMAND in project dir or exec path (see variable `exec-path').
If COMMAND starts with ./ or ../ resolve relative to `clojure-project-dir',
otherwise resolve via `cider--resolve-command'."
(if (string-match-p "\\`\\.\\{1,2\\}/" command)
(locate-file command (list (clojure-project-dir)) '("" ".bat") 'executable)
(cider--resolve-command command)))
(defcustom cider-connection-message-fn #'cider-random-words-of-inspiration
"The function to use to generate the message displayed on connect.
When set to nil no additional message will be displayed. A good
alternative to the default is `cider-random-tip'."
:type 'function
:group 'cider
:package-version '(cider . "0.11.0"))
(defun cider--maybe-inspire-on-connect ()
"Display an inspiration connection message."
(when cider-connection-message-fn
(message "Connected! %s" (funcall cider-connection-message-fn))))
(add-hook 'cider-connected-hook #'cider--maybe-inspire-on-connect)
;;;###autoload
(with-eval-after-load 'clojure-mode
(define-key clojure-mode-map (kbd "C-c M-x") #'cider)
(define-key clojure-mode-map (kbd "C-c M-j") #'cider-jack-in-clj)
(define-key clojure-mode-map (kbd "C-c M-J") #'cider-jack-in-cljs)
(define-key clojure-mode-map (kbd "C-c M-c") #'cider-connect-clj)
(define-key clojure-mode-map (kbd "C-c M-C") #'cider-connect-cljs)
(define-key clojure-mode-map (kbd "C-c C-x") 'cider-start-map)
(define-key clojure-mode-map (kbd "C-c C-s") 'sesman-map)
(require 'sesman)
(sesman-install-menu clojure-mode-map)
(add-hook 'clojure-mode-hook (lambda () (setq-local sesman-system 'CIDER))))
(provide 'cider)
;;; cider.el ends here
;;; nrepl-client.el --- Client for Clojure nREPL -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;; Reid McKenzie <me@arrdem.com>
;;
;; This program 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.
;;
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; Provides an Emacs Lisp client to connect to Clojure nREPL servers.
;;
;; A connection is an abstract idea of the communication between Emacs (client)
;; and nREPL server. On the Emacs side connections are represented by two
;; running processes. The two processes are the server process and client
;; process (the connection to the server). Each of these is represented by its
;; own process buffer, filter and sentinel.
;;
;; The nREPL communication process can be broadly represented as follows:
;;
;; 1) The server process is started as an Emacs subprocess (usually by
;; `cider-jack-in', which in turn fires up an nREPL server). Note that
;; if a connection was established using `cider-connect' there won't be
;; a server process.
;;
;; 2) The server's process filter (`nrepl-server-filter') detects the
;; connection port from the first plain text response from the server and
;; starts a communication process (socket connection) as another Emacs
;; subprocess. This is the nREPL client process (`nrepl-client-filter').
;; All requests and responses handling happens through this client
;; connection.
;;
;; 3) Requests are sent by `nrepl-send-request' and
;; `nrepl-send-sync-request'. A request is simply a list containing a
;; requested operation name and the parameters required by the
;; operation. Each request has an associated callback that is called once
;; the response for the request has arrived. Besides the above functions
;; there are specialized request senders for each type of common
;; operations. Examples are `nrepl-request:eval', `nrepl-request:clone',
;; `nrepl-sync-request:describe'.
;;
;; 4) Responses from the server are decoded in `nrepl-client-filter' and are
;; physically represented by alists whose structure depends on the type of
;; the response. After having been decoded, the data from the response is
;; passed over to the callback that was registered by the original
;; request.
;;
;; Please see the comments in dedicated sections of this file for more detailed
;; description.
;;; Code:
(require 'seq)
(require 'subr-x)
(require 'cl-lib)
(require 'nrepl-dict)
(require 'queue)
(require 'sesman)
(require 'tramp)
;;; Custom
(defgroup nrepl nil
"Interaction with the Clojure nREPL Server."
:prefix "nrepl-"
:group 'applications)
;; (defcustom nrepl-buffer-name-separator " "
;; "Used in constructing the REPL buffer name.
;; The `nrepl-buffer-name-separator' separates cider-repl from the project name."
;; :type '(string)
;; :group 'nrepl)
(make-obsolete-variable 'nrepl-buffer-name-separator 'cider-session-name-template "0.18")
;; (defcustom nrepl-buffer-name-show-port nil
;; "Show the connection port in the nrepl REPL buffer name, if set to t."
;; :type 'boolean
;; :group 'nrepl)
(make-obsolete-variable 'nrepl-buffer-name-show-port 'cider-session-name-template "0.18")
(defcustom nrepl-connected-hook nil
"List of functions to call when connecting to the nREPL server."
:type 'hook)
(defcustom nrepl-disconnected-hook nil
"List of functions to call when disconnected from the nREPL server."
:type 'hook)
(defcustom nrepl-force-ssh-for-remote-hosts nil
"If non-nil, do not attempt a direct connection for remote hosts."
:type 'boolean)
(defcustom nrepl-use-ssh-fallback-for-remote-hosts nil
"If non-nil, Use ssh as a fallback to connect to remote hosts.
It will attempt to connect via ssh to remote hosts when unable to connect
directly."
:type 'boolean)
(defcustom nrepl-sync-request-timeout 10
"The number of seconds to wait for a sync response.
Setting this to nil disables the timeout functionality."
:type 'integer)
(defcustom nrepl-hide-special-buffers nil
"Control the display of some special buffers in buffer switching commands.
When true some special buffers like the server buffer will be hidden."
:type 'boolean)
;;; Buffer Local Declarations
;; These variables are used to track the state of nREPL connections
(defvar-local nrepl-connection-buffer nil)
(defvar-local nrepl-server-buffer nil)
(defvar-local nrepl-messages-buffer nil)
(defvar-local nrepl-endpoint nil)
(defvar-local nrepl-project-dir nil)
(defvar-local nrepl-is-server nil)
(defvar-local nrepl-server-command nil)
(defvar-local nrepl-tunnel-buffer nil)
(defvar-local nrepl-session nil
"Current nREPL session id.")
(defvar-local nrepl-tooling-session nil
"Current nREPL tooling session id.
To be used for tooling calls (i.e. completion, eldoc, etc)")
(defvar-local nrepl-request-counter 0
"Continuation serial number counter.")
(defvar-local nrepl-pending-requests nil)
(defvar-local nrepl-completed-requests nil)
(defvar-local nrepl-last-sync-response nil
"Result of the last sync request.")
(defvar-local nrepl-last-sync-request-timestamp nil
"The time when the last sync request was initiated.")
(defvar-local nrepl-ops nil
"Available nREPL server ops (from describe).")
(defvar-local nrepl-versions nil
"Version information received from the describe op.")
(defvar-local nrepl-aux nil
"Auxiliary information received from the describe op.")
;;; nREPL Buffer Names
(defconst nrepl-message-buffer-name-template "*nrepl-messages %s(%r:%S)*")
(defconst nrepl-error-buffer-name "*nrepl-error*")
(defconst nrepl-repl-buffer-name-template "*cider-repl %s(%r:%S)*")
(defconst nrepl-server-buffer-name-template "*nrepl-server %s*")
(defconst nrepl-tunnel-buffer-name-template "*nrepl-tunnel %s*")
(declare-function cider-format-connection-params "cider-connection")
(defun nrepl-make-buffer-name (template params &optional dup-ok)
"Generate a buffer name using TEMPLATE and PARAMS.
TEMPLATE and PARAMS are as in `cider-format-connection-params'. If
optional DUP-OK is non-nil, the returned buffer is not \"uniquified\" by a
call to `generate-new-buffer-name'."
(let ((name (cider-format-connection-params template params)))
(if dup-ok
name
(generate-new-buffer-name name))))
(defun nrepl--make-hidden-name (buffer-name)
"Apply a prefix to BUFFER-NAME that will hide the buffer."
(concat (if nrepl-hide-special-buffers " " "") buffer-name))
(defun nrepl-repl-buffer-name (params &optional dup-ok)
"Return the name of the repl buffer.
PARAMS and DUP-OK are as in `nrepl-make-buffer-name'."
(nrepl-make-buffer-name nrepl-repl-buffer-name-template params dup-ok))
(defun nrepl-server-buffer-name (params)
"Return the name of the server buffer.
PARAMS is as in `nrepl-make-buffer-name'."
(nrepl-make-buffer-name (nrepl--make-hidden-name nrepl-server-buffer-name-template)
params))
(defun nrepl-tunnel-buffer-name (params)
"Return the name of the tunnel buffer.
PARAMS is as in `nrepl-make-buffer-name'."
(nrepl-make-buffer-name (nrepl--make-hidden-name nrepl-tunnel-buffer-name-template)
params))
(defun nrepl-messages-buffer-name (params)
"Return the name for the message buffer given connection PARAMS."
(nrepl-make-buffer-name nrepl-message-buffer-name-template params))
;;; Utilities
(defun nrepl-op-supported-p (op connection)
"Return t iff the given operation OP is supported by the nREPL CONNECTION."
(when (buffer-live-p connection)
(with-current-buffer connection
(and nrepl-ops (nrepl-dict-get nrepl-ops op)))))
(defun nrepl-aux-info (key connection)
"Return KEY's aux info, as returned via the :describe op for CONNECTION."
(with-current-buffer connection
(and nrepl-aux (nrepl-dict-get nrepl-aux key))))
(defun nrepl-local-host-p (host)
"Return t if HOST is local."
(string-match-p tramp-local-host-regexp host))
(defun nrepl-extract-port (dir)
"Read port from applicable repl-port file in directory DIR."
(or (nrepl--port-from-file (expand-file-name "repl-port" dir))
(nrepl--port-from-file (expand-file-name ".nrepl-port" dir))
(nrepl--port-from-file (expand-file-name "target/repl-port" dir))
(nrepl--port-from-file (expand-file-name ".shadow-cljs/nrepl.port" dir))))
(defun nrepl-extract-ports (dir)
"Read ports from applicable repl-port files in directory DIR."
(delq nil
(list (nrepl--port-from-file (expand-file-name "repl-port" dir))
(nrepl--port-from-file (expand-file-name ".nrepl-port" dir))
(nrepl--port-from-file (expand-file-name "target/repl-port" dir))
(nrepl--port-from-file (expand-file-name ".shadow-cljs/nrepl.port" dir)))))
(make-obsolete 'nrepl-extract-port 'nrepl-extract-ports "1.5.0")
(defun nrepl--port-from-file (file)
"Attempts to read port from a file named by FILE."
(when (file-exists-p file)
(with-temp-buffer
(insert-file-contents file)
(buffer-string))))
;;; Bencode
(cl-defstruct (nrepl-response-queue
(:include queue)
(:constructor nil)
(:constructor nrepl-response-queue (&optional stub)))
stub)
(put 'nrepl-response-queue 'function-documentation
"Create queue object used by nREPL to store decoded server responses.
The STUB slot stores a stack of nested, incompletely parsed objects.")
(defun nrepl--bdecode-list (&optional stack)
"Decode a bencode list or dict starting at point.
STACK is as in `nrepl--bdecode-1'."
;; skip leading l or d
(forward-char 1)
(let* ((istack (nrepl--bdecode-1 stack))
(pos0 (point))
(info (car istack)))
(while (null info)
(setq istack (nrepl--bdecode-1 (cdr istack))
pos0 (point)
info (car istack)))
(cond ((eq info :e)
(cons nil (cdr istack)))
((eq info :stub)
(goto-char pos0)
istack)
(t istack))))
(defun nrepl--bdecode-1 (&optional stack)
"Decode one elementary bencode object starting at point.
Bencoded object is either list, dict, integer or string. See
http://en.wikipedia.org/wiki/Bencode#Encoding_algorithm for the encoding
rules.
STACK is a list of so far decoded components of the current message. Car
of STACK is the innermost incompletely decoded object. The algorithm pops
this list when inner object was completely decoded or grows it by one when
new list or dict was encountered.
The returned value is of the form (INFO . STACK) where INFO is
:stub, nil, :end or :eob and STACK is either an incomplete parsing state as
above (INFO is :stub, nil or :eob) or a list of one component representing
the completely decoded message (INFO is :end). INFO is nil when an
elementary non-root object was successfully decoded. INFO is :end when this
object is a root list or dict."
(cond
;; list
((eq (char-after) ?l)
(nrepl--bdecode-list (cons () stack)))
;; dict
((eq (char-after) ?d)
(nrepl--bdecode-list (cons '(dict) stack)))
;; end of a list or a dict
((eq (char-after) ?e)
(forward-char 1)
(cons (if (cdr stack) :e :end)
(nrepl--push (nrepl--nreverse (car stack))
(cdr stack))))
;; string
((looking-at "\\([0-9]+\\):")
(let ((pos0 (point))
(beg (goto-char (match-end 0)))
(end (byte-to-position (+ (position-bytes (point))
(string-to-number (match-string 1))))))
(if (null end)
(progn (goto-char pos0)
(cons :stub stack))
(goto-char end)
;; normalise any platform-specific newlines
(let* ((original (buffer-substring-no-properties beg end))
(result (replace-regexp-in-string "\r\n\\|\n\r\\|\r" "\n" original)))
(cons nil (nrepl--push result stack))))))
;; integer
((looking-at "i\\(-?[0-9]+\\)e")
(goto-char (match-end 0))
(cons nil (nrepl--push (string-to-number (match-string 1))
stack)))
;; should happen in tests only as eobp is checked in nrepl-bdecode.
((eobp)
(cons :eob stack))
;; truncation in the middle of an integer or in 123: string prefix
((looking-at-p "[0-9i]")
(cons :stub stack))
;; else, throw a quiet error
(t
(message "Invalid bencode message detected. See the %s buffer for details."
nrepl-error-buffer-name)
(nrepl-log-error
(format "Decoder error at position %d (`%s'):"
(point) (buffer-substring (point) (min (+ (point) 10) (point-max)))))
(nrepl-log-error (buffer-string))
(ding)
;; Ensure loop break and clean queues' states in nrepl-bdecode:
(goto-char (point-max))
(cons :end nil))))
(defun nrepl--bdecode-message (&optional stack)
"Decode one full message starting at point.
STACK is as in `nrepl--bdecode-1'. Return a cons (INFO . STACK)."
(let* ((istack (nrepl--bdecode-1 stack))
(info (car istack))
(stack (cdr istack)))
(while (or (null info)
(eq info :e))
(setq istack (nrepl--bdecode-1 stack)
info (car istack)
stack (cdr istack)))
istack))
(defun nrepl--ensure-fundamental-mode ()
"Enable `fundamental-mode' if it is not enabled already."
(when (not (eq 'fundamental-mode major-mode))
(fundamental-mode)))
(defun nrepl-bdecode (string-q &optional response-q)
"Decode STRING-Q and place the results into RESPONSE-Q.
STRING-Q is either a queue of strings or a string. RESPONSE-Q is a queue of
server requests (nREPL dicts). STRING-Q and RESPONSE-Q are modified by side
effects.
Return a cons (STRING-Q . RESPONSE-Q) where STRING-Q is the original queue
containing the remainder of the input strings which could not be
decoded. RESPONSE-Q is the original queue with successfully decoded messages
enqueued and with slot STUB containing a nested stack of an incompletely
decoded message or nil if the strings were completely decoded."
(with-current-buffer (get-buffer-create " *nrepl-decoding*")
;; Don't needlessly call `fundamental-mode', to prevent needlessly firing
;; hooks. This fixes an issue with evil-mode where the cursor loses its
;; correct color.
(nrepl--ensure-fundamental-mode)
(erase-buffer)
(if (queue-p string-q)
(while (queue-head string-q)
(insert (queue-dequeue string-q)))
(insert string-q)
(setq string-q (queue-create)))
(goto-char 1)
(unless response-q
(setq response-q (nrepl-response-queue)))
(let ((istack (nrepl--bdecode-message
(nrepl-response-queue-stub response-q))))
(while (and (eq (car istack) :end)
(not (eobp)))
(queue-enqueue response-q (cadr istack))
(setq istack (nrepl--bdecode-message)))
(unless (eobp)
(queue-enqueue string-q (buffer-substring (point) (point-max))))
(if (not (eq (car istack) :end))
(setf (nrepl-response-queue-stub response-q) (cdr istack))
(queue-enqueue response-q (cadr istack))
(setf (nrepl-response-queue-stub response-q) nil))
(erase-buffer)
(cons string-q response-q))))
(defun nrepl-bencode (object)
"Encode OBJECT with bencode.
Integers, lists and nrepl-dicts are treated according to bencode
specification. Everything else is encoded as string."
(cond
((integerp object) (format "i%de" object))
((nrepl-dict-p object) (format "d%se" (mapconcat #'nrepl-bencode (cdr object) "")))
((listp object) (format "l%se" (mapconcat #'nrepl-bencode object "")))
(t (format "%s:%s" (string-bytes object) object))))
;;; Client: Process Filter
(defvar nrepl-response-handler-functions nil
"List of functions to call on each nREPL message.
Each of these functions should be a function with one argument, which will
be called by `nrepl-client-filter' on every response received. The current
buffer will be connection (REPL) buffer of the process. These functions
should take a single argument, a dict representing the message. See
`nrepl--dispatch-response' for an example.
These functions are called before the message's own callbacks, so that they
can affect the behavior of the callbacks. Errors signaled by these
functions are demoted to messages, so that they don't prevent the
callbacks from running.")
(defun nrepl-client-filter (proc string)
"Decode message(s) from PROC contained in STRING and dispatch them."
(let ((string-q (process-get proc :string-q)))
(queue-enqueue string-q string)
;; Start decoding only if the last letter is 'e'
(when (eq ?e (aref string (1- (length string))))
(let ((response-q (process-get proc :response-q)))
(nrepl-bdecode string-q response-q)
(while (queue-head response-q)
(with-current-buffer (process-buffer proc)
(let ((response (queue-dequeue response-q)))
(with-demoted-errors "Error in one of the `nrepl-response-handler-functions': %s"
(run-hook-with-args 'nrepl-response-handler-functions response))
(nrepl--dispatch-response response))))))))
(defun nrepl--dispatch-response (response)
"Dispatch the RESPONSE to associated callback.
First we check the callbacks of pending requests. If no callback was found,
we check the completed requests, since responses could be received even for
older requests with \"done\" status."
(nrepl-dbind-response response (id)
(nrepl-log-message response 'response)
(let ((callback (or (gethash id nrepl-pending-requests)
(gethash id nrepl-completed-requests))))
(if callback
(funcall callback response)
(error "[nREPL] No response handler with id %s found" id)))))
(defun nrepl-client-sentinel (process message)
"Handle sentinel events from PROCESS.
Notify MESSAGE and if the process is closed run `nrepl-disconnected-hook'
and kill the process buffer."
(if (string-match "deleted\\b" message)
(message "[nREPL] Connection closed")
(message "[nREPL] Connection closed unexpectedly (%s)"
(substring message 0 -1)))
(when (equal (process-status process) 'closed)
(when-let* ((client-buffer (process-buffer process)))
(sesman-remove-object 'CIDER nil client-buffer
(not (process-get process :keep-server))
'no-error)
(nrepl--clear-client-sessions client-buffer)
(with-current-buffer client-buffer
(goto-char (point-max))
(insert-before-markers
(propertize
(format "\n*** Closed on %s ***\n" (current-time-string))
'face 'cider-repl-stderr-face))
(run-hooks 'nrepl-disconnected-hook)
(let ((server-buffer nrepl-server-buffer))
(when (and (buffer-live-p server-buffer)
(not (process-get process :keep-server)))
(setq nrepl-server-buffer nil)
(nrepl--maybe-kill-server-buffer server-buffer)))))))
;;; Network
(defun nrepl--unix-connect (socket-file &optional no-error)
"If SOCKET-FILE is given, try to `make-network-process'.
If NO-ERROR is non-nil, show messages instead of throwing an error."
(if (not socket-file)
(unless no-error
(error "[nREPL] Socket file not provided"))
(message "[nREPL] Establishing unix socket connection to %s ..." socket-file)
(condition-case nil
(prog1 (list :proc (make-network-process :name "nrepl-connection" :buffer nil
:family 'local :service socket-file)
:host "local-unix-domain-socket"
:port socket-file
:socket-file socket-file)
(message "[nREPL] Unix socket connection to %s established" socket-file))
(error (let ((msg (format "[nREPL] Unix socket connection to %s failed" socket-file)))
(if no-error
(message msg)
(error msg))
nil)))))
(defun nrepl-connect (host port)
"Connect to the nREPL server identified by HOST and PORT.
For local hosts use a direct connection. For remote hosts, if
`nrepl-force-ssh-for-remote-hosts' is nil, attempt a direct connection
first. If `nrepl-force-ssh-for-remote-hosts' is non-nil or the direct
connection failed (and `nrepl-use-ssh-fallback-for-remote-hosts' is
non-nil), try to start a SSH tunneled connection. Return a plist of the
form (:proc PROC :host \"HOST\" :port PORT) that might contain additional
key-values depending on the connection type."
(let ((localp (if host
(nrepl-local-host-p host)
(not (file-remote-p default-directory)))))
(if localp
(nrepl--direct-connect (or host "localhost") port)
;; we're dealing with a remote host
(if (and host (not nrepl-force-ssh-for-remote-hosts))
(or (nrepl--direct-connect host port 'no-error)
;; direct connection failed
;; fallback to ssh tunneling if enabled
(and nrepl-use-ssh-fallback-for-remote-hosts
(message "[nREPL] Falling back to SSH tunneled connection ...")
(nrepl--ssh-tunnel-connect host port))
;; fallback is either not enabled or it failed as well
(if (and (null nrepl-use-ssh-fallback-for-remote-hosts)
(not localp))
(error "[nREPL] Direct connection to %s:%s failed; try setting `nrepl-use-ssh-fallback-for-remote-hosts' to t"
host port)
(error "[nREPL] Cannot connect to %s:%s" host port)))
;; `nrepl-force-ssh-for-remote-hosts' is non-nil
(nrepl--ssh-tunnel-connect host port)))))
(defun nrepl--direct-connect (host port &optional no-error)
"If HOST and PORT are given, try to `open-network-stream'.
If NO-ERROR is non-nil, show messages instead of throwing an error."
(if (not (and host port))
(unless no-error
(unless host
(error "[nREPL] Host not provided"))
(unless port
(error "[nREPL] Port not provided")))
(message "[nREPL] Establishing direct connection to %s:%s ..." host port)
(condition-case nil
(prog1 (list :proc (open-network-stream "nrepl-connection" nil host port)
:host host :port port)
(message "[nREPL] Direct connection to %s:%s established" host port))
(error (let ((msg (format "[nREPL] Direct connection to %s:%s failed" host port)))
(if no-error
(message msg)
(error msg))
nil)))))
(defun nrepl--ssh-tunnel-connect (host port)
"Connect to a remote machine identified by HOST and PORT through SSH tunnel."
(message "[nREPL] Establishing SSH tunneled connection to %s:%s ..." host port)
(let* ((remote-dir (if host (format "/ssh:%s:" host) default-directory))
(ssh (or (executable-find "ssh")
(error "[nREPL] Cannot locate 'ssh' executable")))
(cmd (nrepl--ssh-tunnel-command ssh remote-dir port))
(tunnel-buf (nrepl-tunnel-buffer-name
`((:host ,host) (:port ,port))))
(tunnel (start-process-shell-command "nrepl-tunnel" tunnel-buf cmd)))
(process-put tunnel :waiting-for-port t)
(set-process-filter tunnel (nrepl--ssh-tunnel-filter port))
(while (and (process-live-p tunnel)
(process-get tunnel :waiting-for-port))
(accept-process-output nil 0.005))
(if (not (process-live-p tunnel))
(error "[nREPL] SSH port forwarding failed. Check the '%s' buffer" tunnel-buf)
(message "[nREPL] SSH port forwarding established to localhost:%s" port)
(let ((endpoint (nrepl--direct-connect "localhost" port)))
(thread-first
endpoint
(plist-put :tunnel tunnel)
(plist-put :remote-host host))))))
(defun nrepl--ssh-tunnel-command (ssh dir port)
"Command string to open SSH tunnel to the host associated with DIR's PORT."
(with-parsed-tramp-file-name dir v
;; this abuses the -v option for ssh to get output when the port
;; forwarding is set up, which is used to synchronise on, so that
;; the port forwarding is up when we try to connect.
(format-spec
"%s -v -N -L %p:localhost:%p %u'%h'"
`((?s . ,ssh)
(?p . ,port)
(?h . ,v-host)
(?u . ,(if v-user (format "-l '%s' " v-user) ""))))))
(autoload 'comint-watch-for-password-prompt "comint" "(autoload).")
(defun nrepl--ssh-tunnel-filter (port)
"Return a process filter that waits for PORT to appear in process output."
(let ((port-string (format "LOCALHOST:%s" port)))
(lambda (proc string)
(when (string-match-p port-string string)
(process-put proc :waiting-for-port nil))
(when (and (process-live-p proc)
(buffer-live-p (process-buffer proc)))
(with-current-buffer (process-buffer proc)
(let ((moving (= (point) (process-mark proc))))
(save-excursion
(goto-char (process-mark proc))
(insert string)
(set-marker (process-mark proc) (point))
(comint-watch-for-password-prompt string))
(if moving (goto-char (process-mark proc)))))))))
;;; Client: Process Handling
(defun nrepl--kill-process (proc)
"Kill PROC using the appropriate, os specific way.
Implement a workaround to clean up an orphaned JVM process left around
after exiting the REPL on some windows machines."
(if (memq system-type '(cygwin windows-nt))
(interrupt-process proc)
(kill-process proc)))
(defun nrepl-kill-server-buffer (server-buf)
"Kill SERVER-BUF and its process."
(when (buffer-live-p server-buf)
(let ((proc (get-buffer-process server-buf)))
(when (process-live-p proc)
(set-process-query-on-exit-flag proc nil)
(nrepl--kill-process proc))
(kill-buffer server-buf))))
(defun nrepl--maybe-kill-server-buffer (server-buf)
"Kill SERVER-BUF and its process.
Do not kill the server if there is a REPL connected to that server."
(when (buffer-live-p server-buf)
(with-current-buffer server-buf
;; Don't kill if there is at least one REPL connected to it.
(when (not (seq-find (lambda (b)
(eq (buffer-local-value 'nrepl-server-buffer b)
server-buf))
(buffer-list)))
(nrepl-kill-server-buffer server-buf)))))
(defun nrepl-start-client-process (&optional host port server-proc buffer-builder socket-file)
"Create new client process identified by either HOST and PORT or SOCKET-FILE.
If SOCKET-FILE is non-nil, it takes precedence. In remote buffers, HOST
and PORT are taken from the current tramp connection. SERVER-PROC must be
a running nREPL server process within Emacs. BUFFER-BUILDER is a function
of one argument (endpoint returned by `nrepl-connect') which returns a
client buffer. Return the newly created client process."
(let* ((endpoint (if socket-file
(nrepl--unix-connect (expand-file-name socket-file))
(nrepl-connect host port)))
(client-proc (plist-get endpoint :proc))
(builder (or buffer-builder (error "`buffer-builder' must be provided")))
(client-buf (funcall builder endpoint)))
(set-process-buffer client-proc client-buf)
(set-process-filter client-proc #'nrepl-client-filter)
(set-process-sentinel client-proc #'nrepl-client-sentinel)
(set-process-coding-system client-proc 'utf-8-unix 'utf-8-unix)
(process-put client-proc :string-q (queue-create))
(process-put client-proc :response-q (nrepl-response-queue))
(with-current-buffer client-buf
(when-let* ((server-buf (and server-proc (process-buffer server-proc))))
(setq nrepl-project-dir (buffer-local-value 'nrepl-project-dir server-buf)
nrepl-server-buffer server-buf))
(setq nrepl-endpoint endpoint
nrepl-tunnel-buffer (when-let* ((tunnel (plist-get endpoint :tunnel)))
(process-buffer tunnel))
nrepl-pending-requests (make-hash-table :test 'equal)
nrepl-completed-requests (make-hash-table :test 'equal)))
(with-current-buffer client-buf
(nrepl--init-client-sessions client-proc)
(nrepl--init-capabilities client-buf)
(run-hooks 'nrepl-connected-hook))
client-proc))
(defun nrepl--init-client-sessions (client)
"Initialize CLIENT connection nREPL sessions.
We create two client nREPL sessions per connection - a main session and a
tooling session. The main session is general purpose and is used for pretty
much every request that needs a session. The tooling session is used only
for functionality that's implemented in terms of the \"eval\" op, so that
eval requests for functionality like pretty-printing won't clobber the
values of *1, *2, etc."
(let* ((client-conn (process-buffer client))
(response-main (nrepl-sync-request:clone client-conn))
(response-tooling (nrepl-sync-request:clone client-conn t))) ; t for tooling
(nrepl-dbind-response response-main (new-session err)
(if new-session
(with-current-buffer client-conn
(setq nrepl-session new-session))
(error "Could not create new session (%s)" err)))
(nrepl-dbind-response response-tooling (new-session err)
(if new-session
(with-current-buffer client-conn
(setq nrepl-tooling-session new-session))
(error "Could not create new tooling session (%s)" err)))))
(defun nrepl--init-capabilities (conn-buffer)
"Store locally in CONN-BUFFER the capabilities of nREPL server."
(let ((description (nrepl-sync-request:describe conn-buffer)))
(nrepl-dbind-response description (ops versions aux)
(with-current-buffer conn-buffer
(setq nrepl-ops ops)
(setq nrepl-versions versions)
(setq nrepl-aux aux)))))
(defun nrepl--clear-client-sessions (conn-buffer)
"Clear information about nREPL sessions in CONN-BUFFER.
CONN-BUFFER refers to a (presumably) dead connection,
which we can eventually reuse."
(with-current-buffer conn-buffer
(setq nrepl-session nil)
(setq nrepl-tooling-session nil)))
;;; Client: Response Handling
;; After being decoded, responses (aka, messages from the server) are dispatched
;; to handlers. Handlers are constructed with `nrepl-make-response-handler'.
(defvar nrepl-err-handler nil
"Evaluation error handler.")
(defun nrepl--mark-id-completed (id)
"Move ID from `nrepl-pending-requests' to `nrepl-completed-requests'.
It is safe to call this function multiple times on the same ID."
;; FIXME: This should go away eventually when we get rid of
;; pending-request hash table
(when-let* ((handler (gethash id nrepl-pending-requests)))
(puthash id handler nrepl-completed-requests)
(remhash id nrepl-pending-requests)))
(declare-function cider-repl--emit-interactive-output "cider-repl")
(defun nrepl-notify (msg type)
"Handle \"notification\" server request.
MSG is a string to be displayed. TYPE is the type of the message. All
notifications are currently displayed with `message' function and emitted
to the REPL."
(let* ((face (pcase type
((or "message" `nil) 'font-lock-builtin-face)
("warning" 'warning)
("error" 'error)))
(msg (if face
(propertize msg 'face face)
(format "%s: %s" (upcase type) msg))))
(cider-repl--emit-interactive-output msg (or face 'font-lock-builtin-face))
(message msg)))
(defvar cider-buffer-ns)
(defvar cider-print-quota)
(defvar cider-special-mode-truncate-lines)
(declare-function cider-need-input "cider-client")
(declare-function cider-set-buffer-ns "cider-mode")
(defun nrepl-make-response-handler (buffer value-handler stdout-handler
stderr-handler done-handler
&optional eval-error-handler
content-type-handler
truncated-handler)
"Make a response handler for connection BUFFER.
A handler is a function that takes one argument - response received from
the server process. The response is an alist that contains at least 'id'
and 'session' keys. Other standard response keys are 'value', 'out', 'err',
and 'status'.
The presence of a particular key determines the type of the response. For
example, if 'value' key is present, the response is of type 'value', if
'out' key is present the response is 'stdout' etc.
Depending on the type, the handler dispatches the appropriate value to one
of the supplied handlers: VALUE-HANDLER, STDOUT-HANDLER, STDERR-HANDLER,
DONE-HANDLER, EVAL-ERROR-HANDLER, CONTENT-TYPE-HANDLER, and
TRUNCATED-HANDLER.
Handlers are functions of the buffer and the value they handle, except for
the optional CONTENT-TYPE-HANDLER which should be a function of the buffer,
content, the content-type to be handled as a list `(type attrs)'.
If the optional EVAL-ERROR-HANDLER is nil, the default `nrepl-err-handler'
is used. If any of the other supplied handlers are nil nothing happens for
the corresponding type of response."
(lambda (response)
(nrepl-dbind-response response (content-type content-transfer-encoding body
value ns out err status id)
(when (buffer-live-p buffer)
(with-current-buffer buffer
(when (and ns (not (derived-mode-p 'clojure-mode)))
(cider-set-buffer-ns ns))))
(cond ((and content-type content-type-handler)
(funcall content-type-handler buffer
(if (string= content-transfer-encoding "base64")
(base64-decode-string body)
body)
content-type))
(value
(when value-handler
(funcall value-handler buffer value)))
(out
(when stdout-handler
(funcall stdout-handler buffer out)))
(err
(when stderr-handler
(funcall stderr-handler buffer err)))
(status
(when (and truncated-handler (member "nrepl.middleware.print/truncated" status))
(let ((warning (format "\n... output truncated to %sB ..."
(file-size-human-readable cider-print-quota))))
(funcall truncated-handler buffer warning)))
(when (member "notification" status)
(nrepl-dbind-response response (msg type)
(nrepl-notify msg type)))
(when (member "interrupted" status)
(message "Evaluation interrupted."))
(when (member "eval-error" status)
(funcall (or eval-error-handler nrepl-err-handler)))
(when (member "namespace-not-found" status)
(message "Namespace `%s' not found." ns))
(when (member "need-input" status)
(cider-need-input buffer))
(when (member "done" status)
(nrepl--mark-id-completed id)
(when done-handler
(funcall done-handler buffer))))))))
;;; Client: Request Core API
;; Requests are messages from an nREPL client (like CIDER) to an nREPL server.
;; Requests can be asynchronous (sent with `nrepl-send-request') or
;; synchronous (send with `nrepl-send-sync-request'). The request is a pair list
;; of operation name and operation parameters. The core operations are described
;; at https://github.com/nrepl/nrepl/blob/master/doc/ops.md. CIDER adds
;; many more operations through nREPL middleware. See
;; https://github.com/clojure-emacs/cider-nrepl#supplied-nrepl-middleware for
;; the up-to-date list.
(defun nrepl-next-request-id (connection)
"Return the next request id for CONNECTION."
(with-current-buffer connection
(number-to-string (cl-incf nrepl-request-counter))))
(defun nrepl-send-request (request callback connection &optional tooling)
"Send REQUEST and register response handler CALLBACK using CONNECTION.
REQUEST is a pair list of the form (\"op\" \"operation\" \"par1-name\"
\"par1\" ... ). See the code of `nrepl-request:clone',
`nrepl-request:stdin', etc. This expects that the REQUEST does not have a
session already in it. This code will add it as appropriate to prevent
connection/session drift.
Return the ID of the sent message.
Optional argument TOOLING Set to t if desiring the tooling session rather than
the standard session."
(with-current-buffer connection
(when-let* ((session (if tooling nrepl-tooling-session nrepl-session)))
(setq request (append request `("session" ,session))))
(let* ((id (nrepl-next-request-id connection))
(request (cons 'dict (lax-plist-put request "id" id)))
(message (nrepl-bencode request)))
(nrepl-log-message request 'request)
(puthash id callback nrepl-pending-requests)
(process-send-string nil message)
id)))
(defvar nrepl-ongoing-sync-request nil
"Dynamically bound to t while a sync request is ongoing.")
(declare-function cider-repl-emit-interactive-stderr "cider-repl")
(declare-function cider--render-stacktrace-causes "cider-eval")
(defun nrepl-send-sync-request (request connection &optional abort-on-input tooling)
"Send REQUEST to the nREPL server synchronously using CONNECTION.
Hold till final \"done\" message has arrived and join all response messages
of the same \"op\" that came along.
If ABORT-ON-INPUT is non-nil, the function will return nil at the first
sign of user input, so as not to hang the interface.
If TOOLING, use the tooling session rather than the standard session."
(let* ((time0 (current-time))
(response (cons 'dict nil))
(nrepl-ongoing-sync-request t)
status)
(nrepl-send-request request
(lambda (resp) (nrepl--merge response resp))
connection
tooling)
(while (and (not (member "done" status))
(not (and abort-on-input
(input-pending-p))))
(setq status (nrepl-dict-get response "status"))
;; If we get a need-input message then the repl probably isn't going
;; anywhere, and we'll just timeout. So we forward it to the user.
(if (member "need-input" status)
(progn (cider-need-input (current-buffer))
;; If the used took a few seconds to respond, we might
;; unnecessarily timeout, so let's reset the timer.
(setq time0 (current-time)))
;; break out in case we don't receive a response for a while
(when (and nrepl-sync-request-timeout
(time-less-p
nrepl-sync-request-timeout
(time-subtract nil time0)))
(error "Sync nREPL request timed out %s" request)))
;; Clean up the response, otherwise we might repeatedly ask for input.
(nrepl-dict-put response "status" (remove "need-input" status))
(accept-process-output nil 0.01))
;; If we couldn't finish, return nil.
(when (member "done" status)
(nrepl-dbind-response response (ex err eval-error pp-stacktrace id)
(when (and ex err)
(cond (eval-error (funcall nrepl-err-handler))
(pp-stacktrace (cider--render-stacktrace-causes
pp-stacktrace (remove "done" status))))) ;; send the error type
(when id
(with-current-buffer connection
(nrepl--mark-id-completed id)))
response))))
(defun nrepl-request:stdin (input callback connection)
"Send a :stdin request with INPUT using CONNECTION.
Register CALLBACK as the response handler."
(nrepl-send-request `("op" "stdin"
"stdin" ,input)
callback
connection))
(defun nrepl-request:interrupt (pending-request-id callback connection)
"Send an :interrupt request for PENDING-REQUEST-ID.
The request is dispatched using CONNECTION.
Register CALLBACK as the response handler."
(nrepl-send-request `("op" "interrupt"
"interrupt-id" ,pending-request-id)
callback
connection))
(define-minor-mode cider-enlighten-mode nil
:lighter (cider-mode " light")
:global t)
(defun nrepl--eval-request (input &optional ns line column)
"Prepare :eval request message for INPUT.
NS provides context for the request.
If LINE and COLUMN are non-nil and current buffer is a file buffer, \"line\",
\"column\" and \"file\" are added to the message."
(nconc (and ns `("ns" ,ns))
`("op" "eval"
"code" ,(substring-no-properties input))
(when cider-enlighten-mode
'("enlighten" "true"))
(let ((file (or (buffer-file-name) (buffer-name))))
(when (and line column file)
`("file" ,file
"line" ,line
"column" ,column)))))
(defun nrepl-request:eval (input callback connection &optional ns line column additional-params tooling)
"Send the request INPUT and register the CALLBACK as the response handler.
The request is dispatched via CONNECTION. If NS is non-nil,
include it in the request. LINE and COLUMN, if non-nil, define the position
of INPUT in its buffer. A CONNECTION uniquely determines two connections
available: the standard interaction one and the tooling session. If the
tooling is desired, set TOOLING to true.
ADDITIONAL-PARAMS is a plist to be appended to the request message."
(nrepl-send-request (append (nrepl--eval-request input ns line column) additional-params)
callback
connection
tooling))
(defun nrepl-sync-request:clone (connection &optional tooling)
"Sent a :clone request to create a new client session.
The request is dispatched via CONNECTION.
Optional argument TOOLING Tooling is set to t if wanting the tooling session
from CONNECTION."
(nrepl-send-sync-request '("op" "clone")
connection
nil tooling))
(defun nrepl-sync-request:close (connection)
"Sent a :close request to close CONNECTION's SESSION."
(nrepl-send-sync-request '("op" "close") connection)
(nrepl-send-sync-request '("op" "close") connection nil t)) ;; close tooling session
(defun nrepl-sync-request:describe (connection)
"Perform :describe request for CONNECTION and SESSION."
(nrepl-send-sync-request '("op" "describe")
connection))
(defun nrepl-sync-request:ls-sessions (connection)
"Perform :ls-sessions request for CONNECTION."
(nrepl-send-sync-request '("op" "ls-sessions") connection))
(defun nrepl-sync-request:ls-middleware (connection)
"Perform :ls-middleware request for CONNECTION."
(nrepl-send-sync-request '("op" "ls-middleware") connection))
(defun nrepl-sync-request:eval (input connection &optional ns tooling)
"Send the INPUT to the nREPL server synchronously.
The request is dispatched via CONNECTION.
If NS is non-nil, include it in the request
If TOOLING is non-nil the evaluation is done using the tooling nREPL
session."
(nrepl-send-sync-request
(nrepl--eval-request input ns)
connection
nil
tooling))
(defun nrepl-sessions (connection)
"Get a list of active sessions on the nREPL server using CONNECTION."
(nrepl-dict-get (nrepl-sync-request:ls-sessions connection) "sessions"))
(defun nrepl-middleware (connection)
"Get a list of middleware on the nREPL server using CONNECTION."
(nrepl-dict-get (nrepl-sync-request:ls-middleware connection) "middleware"))
;;; Server
;; The server side process is started by `nrepl-start-server-process' and has a
;; very simple filter that pipes its output directly into its process buffer
;; (*nrepl-server*). The main purpose of this process is to start the actual
;; nrepl communication client (`nrepl-client-filter') when the message "nREPL
;; server started on port ..." is detected.
;; internal variables used for state transfer between nrepl-start-server-process
;; and nrepl-server-filter.
(defvar-local nrepl-on-port-callback nil)
(defun nrepl-server-p (buffer-or-process)
"Return t if BUFFER-OR-PROCESS is an nREPL server."
(let ((buffer (if (processp buffer-or-process)
(process-buffer buffer-or-process)
buffer-or-process)))
(buffer-local-value 'nrepl-is-server buffer)))
(defun nrepl-start-server-process (directory cmd on-port-callback)
"Start nREPL server process in DIRECTORY using shell command CMD.
Return a newly created process. Set `nrepl-server-filter' as the process
filter, which starts REPL process with its own buffer once the server has
started. ON-PORT-CALLBACK is a function of one argument (server buffer)
which is called by the process filter once the port of the connection has
been determined."
(let* ((default-directory (or directory default-directory))
(serv-buf (get-buffer-create
(nrepl-server-buffer-name
`(:project-dir ,default-directory)))))
(with-current-buffer serv-buf
(setq nrepl-is-server t
nrepl-project-dir default-directory
nrepl-server-command cmd
nrepl-on-port-callback on-port-callback))
(let ((serv-proc (start-file-process-shell-command
"nrepl-server" serv-buf cmd)))
(set-process-filter serv-proc #'nrepl-server-filter)
(set-process-sentinel serv-proc #'nrepl-server-sentinel)
(set-process-coding-system serv-proc 'utf-8-unix 'utf-8-unix)
(message "[nREPL] Starting server via %s"
(propertize cmd 'face 'font-lock-keyword-face))
serv-proc)))
(defconst nrepl-listening-address-regexp
(rx (or
;; standard
(and "nREPL server started on port " (group-n 1 (+ (any "0-9"))))
;; babashka
(and "Started nREPL server at "
(group-n 2 (+? any)) ":" (group-n 1 (+ (any "0-9"))))))
"A regexp to search an nREPL's stdout for the address it is listening on.
If it matches, the address components can be extracted using the following
match groups:
1 for the port, and
2 for the host (babashka only).")
(defun nrepl-server-filter (process output)
"Process nREPL server output from PROCESS contained in OUTPUT."
;; In Windows this can be false:
(let ((server-buffer (process-buffer process)))
(when (buffer-live-p server-buffer)
(with-current-buffer server-buffer
;; auto-scroll on new output
(let ((moving (= (point) (process-mark process))))
(save-excursion
(goto-char (process-mark process))
(insert output)
(ansi-color-apply-on-region (process-mark process) (point))
(set-marker (process-mark process) (point)))
(when moving
(goto-char (process-mark process))
(when-let* ((win (get-buffer-window)))
(set-window-point win (point)))))
;; detect the port the server is listening on from its output
(when (and (null nrepl-endpoint)
(string-match nrepl-listening-address-regexp output))
(let ((host (or (match-string 2 output)
(file-remote-p default-directory 'host)
"localhost"))
(port (string-to-number (match-string 1 output))))
(setq nrepl-endpoint (list :host host
:port port))
(message "[nREPL] server started on %s" port)
(when nrepl-on-port-callback
(funcall nrepl-on-port-callback (process-buffer process)))))))))
(defmacro emacs-bug-46284/when-27.1-windows-nt (&rest body)
"Only evaluate BODY when Emacs bug #46284 has been detected."
(when (and (eq system-type 'windows-nt)
(string= emacs-version "27.1"))
(cons 'progn body)))
(declare-function cider--close-connection "cider-connection")
(defun nrepl-server-sentinel (process event)
"Handle nREPL server PROCESS EVENT."
(let* ((server-buffer (process-buffer process))
(clients (seq-filter (lambda (b)
(eq (buffer-local-value 'nrepl-server-buffer b)
server-buffer))
(buffer-list)))
(problem (if (and server-buffer (buffer-live-p server-buffer))
(with-current-buffer server-buffer
(buffer-substring (point-min) (point-max)))
"")))
(emacs-bug-46284/when-27.1-windows-nt
;; There is a bug in emacs 27.1 (since fixed) that sets all EVENT
;; descriptions for signals to "unknown signal". We correct this by
;; resetting it back to its canonical value.
(when (eq (process-status process) 'signal)
(cl-case (process-exit-status process)
;; SIGHUP==1 emacs nt/inc/ms-w32.h
(1 (setq event "Hangup"))
;; SIGINT==2 x86_64-w64-mingw32/include/signal.h
(2 (setq event "Interrupt"))
;; SIGKILL==9 emacs nt/inc/ms-w32.h
(9 (setq event "Killed")))))
(when server-buffer
(kill-buffer server-buffer))
(cond
((string-match-p "^killed\\|^interrupt" event)
nil)
((string-match-p "^hangup" event)
(mapc #'cider--close-connection clients))
;; On Windows, a failed start sends the "finished" event. On Linux it sends
;; "exited abnormally with code 1".
(t (error "Could not start nREPL server: %s" problem)))))
;;; Messages
(defcustom nrepl-log-messages nil
"If non-nil, log protocol messages to an nREPL messages buffer.
This is extremely useful for debug purposes, as it allows you to inspect
the communication between Emacs and an nREPL server. Enabling the logging
might have a negative impact on performance, so it's not recommended to
keep it enabled unless you need to debug something."
:type 'boolean
:safe #'booleanp)
(defconst nrepl-message-buffer-max-size 1000000
"Maximum size for the nREPL message buffer.
Defaults to 1000000 characters, which should be an insignificant
memory burden, while providing reasonable history.")
(defconst nrepl-message-buffer-reduce-denominator 4
"Divisor by which to reduce message buffer size.
When the maximum size for the nREPL message buffer is exceeded, the size of
the buffer is reduced by one over this value. Defaults to 4, so that 1/4
of the buffer is removed, which should ensure the buffer's maximum is
reasonably utilized, while limiting the number of buffer shrinking
operations.")
(defvar nrepl-messages-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "n") #'next-line)
(define-key map (kbd "p") #'previous-line)
(define-key map (kbd "TAB") #'forward-button)
(define-key map (kbd "RET") #'nrepl-log-expand-button)
(define-key map (kbd "e") #'nrepl-log-expand-button)
(define-key map (kbd "E") #'nrepl-log-expand-all-buttons)
(define-key map (kbd "<backtab>") #'backward-button)
map))
(define-derived-mode nrepl-messages-mode special-mode "nREPL Messages"
"Major mode for displaying nREPL messages.
\\{nrepl-messages-mode-map}"
(when cider-special-mode-truncate-lines
(setq-local truncate-lines t))
(setq-local sesman-system 'CIDER)
(setq-local electric-indent-chars nil)
(setq-local comment-start ";")
(setq-local comment-end "")
(setq-local paragraph-start "(-->\\|(<--")
(setq-local paragraph-separate "(<--"))
(defun nrepl-decorate-msg (msg type)
"Decorate nREPL MSG according to its TYPE."
(pcase type
(`request (cons '--> (cdr msg)))
(`response (cons '<-- (cdr msg)))))
(defun nrepl-log-message (msg type)
"Log the nREPL MSG.
TYPE is either request or response. The message is logged to a buffer
described by `nrepl-message-buffer-name-template'."
(when nrepl-log-messages
;; append a time-stamp to the message before logging it
;; the time-stamps are quite useful for debugging
(setq msg (cons (car msg)
(lax-plist-put (cdr msg) "time-stamp"
(format-time-string "%Y-%m-%0d %H:%M:%S.%N"))))
(with-current-buffer (nrepl-messages-buffer (current-buffer))
(setq buffer-read-only nil)
(when (> (buffer-size) nrepl-message-buffer-max-size)
(goto-char (/ (buffer-size) nrepl-message-buffer-reduce-denominator))
(re-search-forward "^(" nil t)
(delete-region (point-min) (- (point) 1)))
(goto-char (point-max))
(nrepl-log-pp-object (nrepl-decorate-msg msg type)
(nrepl-log--message-color (lax-plist-get (cdr msg) "id"))
t)
(when-let* ((win (get-buffer-window)))
(set-window-point win (point-max)))
(setq buffer-read-only t))))
(defun nrepl-toggle-message-logging ()
"Toggle the value of `nrepl-log-messages' between nil and t.
This in effect enables or disables the logging of nREPL messages."
(interactive)
(setq nrepl-log-messages (not nrepl-log-messages))
(if nrepl-log-messages
(message "nREPL message logging enabled")
(message "nREPL message logging disabled")))
(defcustom nrepl-message-colors
'("red" "brown" "coral" "orange" "green" "deep sky blue" "blue" "dark violet")
"Colors used in the messages buffer."
:type '(repeat color))
(defun nrepl-log-expand-button (&optional button)
"Expand the objects hidden in BUTTON's :nrepl-object property.
BUTTON defaults the button at point."
(interactive)
(if-let* ((button (or button (button-at (point)))))
(let* ((start (overlay-start button))
(end (overlay-end button))
(obj (overlay-get button :nrepl-object))
(inhibit-read-only t))
(save-excursion
(goto-char start)
(delete-overlay button)
(delete-region start end)
(nrepl-log-pp-object obj)
(delete-char -1)))
(error "No button at point")))
(defun nrepl-log-expand-all-buttons ()
"Expand all buttons in nREPL log buffer."
(interactive)
(if (not (eq major-mode 'nrepl-messages-mode))
(user-error "Not in a `nrepl-messages-mode'")
(save-excursion
(let* ((pos (point-min))
(button (next-button pos)))
(while button
(setq pos (overlay-start button))
(nrepl-log-expand-button button)
(setq button (next-button pos)))))))
(defun nrepl-log--expand-button-mouse (event)
"Expand the text hidden under overlay button.
EVENT gives the button position on window."
(interactive "e")
(pcase (elt event 1)
(`(,window ,_ ,_ ,_ ,_ ,point . ,_)
(with-selected-window window
(nrepl-log-expand-button (button-at point))))))
(defun nrepl-log-insert-button (label object)
"Insert button with LABEL and :nrepl-object property as OBJECT."
(insert-button label
:nrepl-object object
'action #'nrepl-log-expand-button
'face 'link
'help-echo "RET: Expand object."
;; Workaround for bug#1568 (don't use local-map here; it
;; overwrites major mode map.)
'keymap `(keymap (mouse-1 . nrepl-log--expand-button-mouse)))
(insert "\n"))
(defun nrepl-log--message-color (id)
"Return the color to use when pretty-printing the nREPL message with ID.
If ID is nil, return nil."
(when id
(thread-first
(string-to-number id)
(mod (length nrepl-message-colors))
(nth nrepl-message-colors))))
(defun nrepl-log--pp-listlike (object &optional foreground button)
"Pretty print nREPL list like OBJECT.
FOREGROUND and BUTTON are as in `nrepl-log-pp-object'."
(cl-flet ((color (str)
(propertize str 'face
(append '(:weight ultra-bold)
(when foreground `(:foreground ,foreground))))))
(let ((head (format "(%s" (car object))))
(insert (color head))
(if (null (cdr object))
(insert ")\n")
(let* ((indent (+ 2 (- (current-column) (length head))))
(sorted-pairs (sort (seq-partition (cl-copy-list (cdr object)) 2)
(lambda (a b)
(string< (car a) (car b)))))
(name-lengths (seq-map (lambda (pair) (length (car pair))) sorted-pairs))
(longest-name (seq-max name-lengths))
;; Special entries are displayed first
(specialq (lambda (pair) (member (car pair) '("id" "op" "session" "time-stamp"))))
(special-pairs (seq-filter specialq sorted-pairs))
(not-special-pairs (seq-remove specialq sorted-pairs))
(all-pairs (seq-concatenate 'list special-pairs not-special-pairs))
(sorted-object (apply #'seq-concatenate 'list all-pairs)))
(insert "\n")
(cl-loop for l on sorted-object by #'cddr
do (let ((indent-str (make-string indent ?\s))
(name-str (propertize (car l) 'face
;; Only highlight top-level keys.
(unless (eq (car object) 'dict)
'font-lock-keyword-face)))
(spaces-str (make-string (- longest-name (length (car l))) ?\s)))
(insert (format "%s%s%s " indent-str name-str spaces-str))
(nrepl-log-pp-object (cadr l) nil button)))
(when (eq (car object) 'dict)
(delete-char -1))
(insert (color ")\n")))))))
(defun nrepl-log-pp-object (object &optional foreground button)
"Pretty print nREPL OBJECT, delimited using FOREGROUND.
If BUTTON is non-nil, try making a button from OBJECT instead of inserting
it into the buffer."
(let ((min-dict-fold-size 1)
(min-list-fold-size 10)
(min-string-fold-size 60))
(if-let* ((head (car-safe object)))
;; list-like objects
(cond
;; top level dicts (always expanded)
((memq head '(<-- -->))
(nrepl-log--pp-listlike object foreground button))
;; inner dicts
((eq head 'dict)
(if (and button (> (length object) min-dict-fold-size))
(nrepl-log-insert-button "(dict ...)" object)
(nrepl-log--pp-listlike object foreground button)))
;; lists
(t
(if (and button (> (length object) min-list-fold-size))
(nrepl-log-insert-button (format "(%s ...)" (prin1-to-string head)) object)
(pp object (current-buffer)))))
;; non-list objects
(if (stringp object)
(if (and button (> (length object) min-string-fold-size))
(nrepl-log-insert-button (format "\"%s...\"" (substring object 0 min-string-fold-size)) object)
(insert (prin1-to-string object) "\n"))
(pp object (current-buffer))
(insert "\n")))))
(declare-function cider--gather-connect-params "cider-connection")
(defun nrepl-messages-buffer (conn)
"Return or create the buffer for CONN.
The default buffer name is *nrepl-messages connection*."
(with-current-buffer conn
(or (and (buffer-live-p nrepl-messages-buffer)
nrepl-messages-buffer)
(setq nrepl-messages-buffer
(let ((buffer (get-buffer-create
(nrepl-messages-buffer-name
(cider--gather-connect-params)))))
(with-current-buffer buffer
(buffer-disable-undo)
(nrepl-messages-mode)
buffer))))))
(defun nrepl-error-buffer ()
"Return or create the buffer.
The default buffer name is *nrepl-error*."
(or (get-buffer nrepl-error-buffer-name)
(let ((buffer (get-buffer-create nrepl-error-buffer-name)))
(with-current-buffer buffer
(buffer-disable-undo)
(nrepl--ensure-fundamental-mode)
buffer))))
(defun nrepl-log-error (msg)
"Log the given MSG to the buffer given by `nrepl-error-buffer'."
(with-current-buffer (nrepl-error-buffer)
(setq buffer-read-only nil)
(goto-char (point-max))
(insert msg)
(when-let* ((win (get-buffer-window)))
(set-window-point win (point-max)))
(setq buffer-read-only t)))
(make-obsolete 'nrepl-default-client-buffer-builder nil "0.18")
(provide 'nrepl-client)
;;; nrepl-client.el ends here
;;; nrepl-dict.el --- Dictionary functions for Clojure nREPL -*- lexical-binding: t -*-
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2022 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.dev>
;; Artur Malabarba <bruce.connor.am@gmail.com>
;; Hugo Duncan <hugo@hugoduncan.org>
;; Steve Purcell <steve@sanityinc.com>
;;
;; This program 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.
;;
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; Provides functions to interact with and create `nrepl-dict's. These are
;; simply plists with an extra element at the head.
;;; Code:
(require 'cl-lib)
(defun nrepl-dict (&rest key-vals)
"Create nREPL dict from KEY-VALS."
(cons 'dict key-vals))
(defun nrepl-dict-from-hash (hash)
"Create nREPL dict from HASH."
(let ((dict (nrepl-dict)))
(maphash (lambda (k v) (nrepl-dict-put dict k v)) hash)
dict))
(defun nrepl-dict-p (object)
"Return t if OBJECT is an nREPL dict."
(and (listp object)
(eq (car object) 'dict)))
(defun nrepl-dict-empty-p (dict)
"Return t if nREPL dict DICT is empty."
(null (cdr dict)))
(defun nrepl-dict-contains (dict key)
"Return nil if nREPL dict DICT doesn't contain KEY.
If DICT does contain KEY, then a non-nil value is returned. Due to the
current implementation, this return value is the tail of DICT's key-list
whose car is KEY. Comparison is done with `equal'."
(member key (nrepl-dict-keys dict)))
(defun nrepl-dict-get (dict key &optional default)
"Get from DICT value associated with KEY, optional DEFAULT if KEY not in DICT.
If dict is nil, return nil. If DEFAULT not provided, and KEY not in DICT,
return nil. If DICT is not an nREPL dict object, an error is thrown."
(when dict
(if (nrepl-dict-p dict)
(if (nrepl-dict-contains dict key)
(lax-plist-get (cdr dict) key)
default)
(error "Not an nREPL dict object: %s" dict))))
(defun nrepl-dict-put (dict key value)
"Associate in DICT, KEY to VALUE.
Return new dict. Dict is modified by side effects."
(if (null dict)
`(dict ,key ,value)
(if (not (nrepl-dict-p dict))
(error "Not an nREPL dict object: %s" dict)
(setcdr dict (lax-plist-put (cdr dict) key value))
dict)))
(defun nrepl-dict-keys (dict)
"Return all the keys in the nREPL DICT."
(if (nrepl-dict-p dict)
(cl-loop for l on (cdr dict) by #'cddr
collect (car l))
(error "Not an nREPL dict")))
(defun nrepl-dict-vals (dict)
"Return all the values in the nREPL DICT."
(if (nrepl-dict-p dict)
(cl-loop for l on (cdr dict) by #'cddr
collect (cadr l))
(error "Not an nREPL dict")))
(defun nrepl-dict-map (fn dict)
"Map FN on nREPL DICT.
FN must accept two arguments key and value."
(if (nrepl-dict-p dict)
(cl-loop for l on (cdr dict) by #'cddr
collect (funcall fn (car l) (cadr l)))
(error "Not an nREPL dict")))
(defun nrepl-dict-merge (dict1 dict2)
"Destructively merge DICT2 into DICT1.
Keys in DICT2 override those in DICT1."
(let ((base (or dict1 '(dict))))
(nrepl-dict-map (lambda (k v)
(nrepl-dict-put base k v))
(or dict2 '(dict)))
base))
(defun nrepl-dict-get-in (dict keys)
"Return the value in a nested DICT.
KEYS is a list of keys. Return nil if any of the keys is not present or if
any of the values is nil."
(let ((out dict))
(while (and keys out)
(setq out (nrepl-dict-get out (pop keys))))
out))
(defun nrepl-dict-flat-map (function dict)
"Map FUNCTION over DICT and flatten the result.
FUNCTION follows the same restrictions as in `nrepl-dict-map', and it must
also always return a sequence (since the result will be flattened)."
(when dict
(apply #'append (nrepl-dict-map function dict))))
(defun nrepl-dict-filter (function dict)
"For all key-values of DICT, return new dict where FUNCTION returns non-nil.
FUNCTION should be a function taking two arguments, key and value."
(let ((new-map (nrepl-dict))
(keys (nrepl-dict-keys dict)))
(dolist (key keys)
(let ((val (nrepl-dict-get dict key)))
(when (funcall function key val)
(nrepl-dict-put new-map key val))))
new-map))
;;; More specific functions
(defun nrepl--cons (car list-or-dict)
"Generic cons of CAR to LIST-OR-DICT."
(if (eq (car list-or-dict) 'dict)
(cons 'dict (cons car (cdr list-or-dict)))
(cons car list-or-dict)))
(defun nrepl--nreverse (list-or-dict)
"Generic `nreverse' which works on LIST-OR-DICT."
(if (eq (car list-or-dict) 'dict)
(cons 'dict (nreverse (cdr list-or-dict)))
(nreverse list-or-dict)))
(defun nrepl--push (obj stack)
"Cons OBJ to the top element of the STACK."
;; stack is assumed to be a list
(if (eq (caar stack) 'dict)
(cons (cons 'dict (cons obj (cdar stack)))
(cdr stack))
(cons (if (null stack)
obj
(cons obj (car stack)))
(cdr stack))))
(defun nrepl--merge (dict1 dict2 &optional no-join)
"Join nREPL dicts DICT1 and DICT2 in a meaningful way.
String values for non \"id\" and \"session\" keys are concatenated. Lists
are appended. nREPL dicts merged recursively. All other objects are
accumulated into a list. DICT1 is modified destructively and
then returned.
If NO-JOIN is given, return the first non nil dict."
(if no-join
(or dict1 dict2)
(cond ((null dict1) dict2)
((null dict2) dict1)
((stringp dict1) (concat dict1 dict2))
((nrepl-dict-p dict1)
(nrepl-dict-map
(lambda (k2 v2)
(nrepl-dict-put dict1 k2
(nrepl--merge (nrepl-dict-get dict1 k2) v2
(member k2 '("id" "session")))))
dict2)
dict1)
((and (listp dict2) (listp dict1)) (append dict1 dict2))
((listp dict1) (append dict1 (list dict2)))
(t `(,dict1 ,dict2)))))
;;; Dbind
(defmacro nrepl-dbind-response (response keys &rest body)
"Destructure an nREPL RESPONSE dict.
Bind the value of the provided KEYS and execute BODY."
(declare (debug (form (&rest symbolp) body)))
`(let ,(cl-loop for key in keys
collect `(,key (nrepl-dict-get ,response ,(format "%s" key))))
,@body))
(put 'nrepl-dbind-response 'lisp-indent-function 2)
(provide 'nrepl-dict)
;;; nrepl-dict.el ends here