;;; racket-logger.el -*- lexical-binding: t; -*- ;; Copyright (c) 2013-2016 by Greg Hendershott. ;; Portions Copyright (C) 1985-1986, 1999-2013 Free Software Foundation, Inc. ;; Author: Greg Hendershott ;; URL: https://github.com/greghendershott/racket-mode ;; License: ;; This 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 2, or (at your option) ;; any later version. This 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. See ;; http://www.gnu.org/licenses/ for details. (require 'easymenu) (require 'rx) (require 'racket-custom) (require 'racket-repl) ;; Need to define this before racket-logger-mode (defvar racket-logger-mode-map (racket--easy-keymap-define '(("l" racket-logger-topic-level) ("w" toggle-truncate-lines) ("n" racket-logger-next-item) ("p" racket-logger-previous-item) ("g" racket-logger-clear) ("x" racket-logger-exit) ("C-c C-z" racket-repl)))) (easy-menu-define racket-logger-mode-menu racket-logger-mode-map "Menu for Racket logger mode." '("Racket" ["Configure Topic and Level" racket-logger-topic-level] ["Toggle Truncate Lines" toggle-truncate-lines] "---" ["Switch to REPL" racket-repl] "---" ["Clear and Reconnect" racket-logger-clear] ["Exit Logger" racket-logger-exit])) (defconst racket-logger-font-lock-keywords (eval-when-compile `((,#'racket--font-lock-config . racket-logger-config-face) (,(rx bol "[ fatal]") . racket-logger-fatal-face) (,(rx bol "[ error]") . racket-logger-error-face) (,(rx bol "[warning]") . racket-logger-warning-face) (,(rx bol "[ info]") . racket-logger-info-face) (,(rx bol "[ debug]") . racket-logger-debug-face) (,(rx bol ?\[ (+? anything) ?\] space (group (+? anything) ?:) space) 1 racket-logger-topic-face)))) (defconst racket-logger--print-config-prefix "racket-logger-config:\n") (defun racket--font-lock-config (limit) "Handle multi-line font-lock of the configuration info." (ignore-errors (when (re-search-forward (concat "^" racket-logger--print-config-prefix) limit t) (let ((md (match-data))) (goto-char (match-end 0)) (forward-sexp 1) (setf (elt md 1) (point)) ;; set (match-end 0) (set-match-data md) t)))) (define-derived-mode racket-logger-mode special-mode "Racket-Logger" "Major mode for Racket logger output. \\ The customization variable `racket-logger-config' determines the levels for topics. During a session you may change topic levels using `racket-logger-topic-level'. For more information see: \\{racket-logger-mode-map} " (setq-local font-lock-defaults (list racket-logger-font-lock-keywords)) (setq-local truncate-lines t)) (defvar racket-logger--buffer-name "*Racket Logger*") (defvar racket-logger--process nil) (defvar racket-logger--connect-timeout 3) (defun racket-logger--connect () (unless racket-logger--process (with-temp-message "Connecting to logger process..." (with-timeout (racket-logger--connect-timeout (error "Could not connect; try `racket-run' first")) (while (not racket-logger--process) (condition-case () (setq racket-logger--process (let ((process-connection-type nil)) ;use pipe not pty (open-network-stream "racket-logger" (get-buffer-create racket-logger--buffer-name) "127.0.0.1" (1+ racket-command-port)))) (error (sit-for 0.1))))) (process-send-string racket-logger--process (format "%S\n" racket--cmd-auth)) (racket-logger--activate-config) (set-process-sentinel racket-logger--process #'racket-logger--process-sentinel)))) (defun racket-logger--process-sentinel (proc change) (funcall (process-filter proc) proc change) ;display in buffer (unless (memq (process-status proc) '(run open connect)) (setq racket-logger--process nil))) (defun racket-logger--disconnect () (when racket-logger--process (with-temp-message "Disconnecting from logger process..." (set-process-sentinel racket-logger--process (lambda (_p _c))) (delete-process racket-logger--process) (setq racket-logger--process nil)))) (defun racket-logger--activate-config () "Send config to Racket process, and, display it in the buffer." (process-send-string racket-logger--process (format "%S" racket-logger-config)) (funcall (process-filter racket-logger--process) racket-logger--process (propertize (concat racket-logger--print-config-prefix (pp-to-string racket-logger-config)) 'font-lock-multiline t))) (defun racket-logger--set (topic level) (unless (symbolp topic) (error "TOPIC must be symbolp")) (unless (symbolp level) (error "LEVEL must be symbolp")) (pcase (assq topic racket-logger-config) (`() (add-to-list 'racket-logger-config (cons topic level))) (v (setcdr v level))) (racket-logger--activate-config)) (defun racket-logger--unset (topic) (unless (symbolp topic) (error "TOPIC must be symbolp")) (when (eq topic '*) (user-error "Cannot unset the level for the '* topic")) (setq racket-logger-config (assq-delete-all topic racket-logger-config)) (racket-logger--activate-config)) (defun racket-logger--topics () "Effectively (sort (dict-keys racket-logger-config))." (sort (mapcar (lambda (x) (format "%s" (car x))) racket-logger-config) #'string<)) (defun racket-logger--topic-level (topic not-found) "Effectively (dict-ref racket-logger-config topic not-found)." (or (cdr (assq topic racket-logger-config)) not-found)) ;;; commands (defun racket-logger () "Create the `racket-logger-mode' buffer and connect to logger output. If the `racket-repl-mode' buffer is displayed in a window, split that window and put the logger in the bottom window. Otherwise, use `pop-to-buffer'." (interactive) ;; Create buffer if necessary (unless (get-buffer racket-logger--buffer-name) (with-current-buffer (get-buffer-create racket-logger--buffer-name) (racket-logger-mode)) (racket-logger--connect)) ;; Give it a window if necessary (unless (get-buffer-window racket-logger--buffer-name) (pcase (get-buffer-window racket-repl-buffer-name) (`() (pop-to-buffer (get-buffer racket-logger--buffer-name))) (win (set-window-buffer (split-window win) (get-buffer racket-logger--buffer-name))))) ;; Select the window (select-window (get-buffer-window racket-logger--buffer-name))) (defun racket-logger-exit () "Disconnect, kill the buffer, and delete the window." (interactive) (when (y-or-n-p "Disconnect and kill buffer? ") (racket-logger--disconnect) (kill-buffer) (delete-window))) (defun racket-logger-clear () "Clear the buffer and reconnect." (interactive) (when (y-or-n-p "Clear buffer and reconnect? ") (let ((inhibit-read-only t)) (delete-region (point-min) (point-max))) (racket-logger--disconnect) (racket-logger--connect))) (defconst racket-logger--item-rx (rx bol ?\[ (0+ space) (or "fatal" "error" "warning" "info" "debug") ?\] space)) (defun racket-logger-next-item (&optional count) "Move point N items forward. An \"item\" is a line starting with a log level in brackets. Interactively, N is the numeric prefix argument. If N is omitted or nil, move point 1 item forward." (interactive "P") (forward-char 1) (if (re-search-forward racket-logger--item-rx nil t count) (beginning-of-line) (backward-char 1))) (defun racket-logger-previous-item (&optional count) "Move point N items backward. An \"item\" is a line starting with a log level in brackets. Interactively, N is the numeric prefix argument. If N is omitted or nil, move point 1 item backward." (interactive "P") (re-search-backward racket-logger--item-rx nil t count)) (defun racket-logger-topic-level () "Set or unset the level for a topic. For convenience, input choices using `ido-completing-read'. The topic labeled \"*\" is the level to use for all topics not specifically assigned a level. The level choice \"*\" means the topic will no longer have its own level, therefore will follow the level specified for the \"*\" topic." (interactive) (let* ((topic (ido-completing-read "Topic: " (racket-logger--topics))) (topic (pcase topic ("" "*") (v v))) (topic (intern topic)) (levels (list "fatal" "error" "warning" "info" "debug")) (levels (if (eq topic '*) levels (cons "*" levels))) (level (ido-completing-read (format "Level for topic `%s': " topic) levels nil t nil nil (format "%s" (racket-logger--topic-level topic "*")))) (level (pcase level ("" nil) ("*" nil) (v (intern v))))) (if level (racket-logger--set topic level) (racket-logger--unset topic)))) (provide 'racket-logger) ;;; racket-logger.el ends here