;;; sage.el --- SageMath support for Emacs -*- lexical-binding: t; -*- ;; Copyright (C) 2024 Free Software Foundation, Inc. ;; Author: Rahguzar ;; Maintainer: Rahguzar ;; Keywords: languages ;; 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 . ;;; Commentary: ;; Major modes for editing Sage files and interacting with sage process. These ;; are thin wrappers around `python-mode' and `inferior-python-mode'. To ;; change the behavior of these modes, the facilities provided by `python.el' ;; can be used in conjunction with `sage-mode-hook' and `sage-shell-mode-hook'. ;;; Code: (require 'python) ;;;; Variables (defgroup sage nil "Emacs interface to sagemath." :group 'languages) (defcustom sage-shell-interpreter (executable-find "sage") "Interpreter for sage code. Sage version should be at least 9.3." :type 'string) (defcustom sage-shell-interpreter-args '("--simple-prompt") "Arguments for the sage interpreter. Note: \"--simple-promot\" is required for ipython to work." :type '(repeat string)) (defvar sage-shell-eval-setup-code "\ import warnings def __PYTHON_EL_eval(source, filename): import ast, sys from sage.repl.preparse import preparse_file if isinstance(source, str): source = preparse_file(source) else: source = preparse_file(source.decode()) if sys.version_info[0] == 2: from __builtin__ import compile, eval, globals else: from builtins import compile, eval, globals try: p, e = ast.parse(source, filename), None except SyntaxError: t, v, tb = sys.exc_info() sys.excepthook(t, v, tb.tb_next) return if p.body and isinstance(p.body[-1], ast.Expr): e = p.body.pop() try: g = globals() exec(compile(p, filename, 'exec'), g, g) if e: return eval(compile(ast.Expression(e.value), filename, 'eval'), g, g) except Exception: t, v, tb = sys.exc_info() sys.excepthook(t, v, tb.tb_next) " "Code for sending code to a sage process. This is essentially same as `python-shell-eval-setup-code' except the addition of a preparse step that converts sage code to python code.") (defvar sage-shell-doc-setup-code "\ from IPython.terminal.prompts import Prompts, Token class __SAGEPrompt(type(get_ipython().prompts)): redirect_happening = False initial_prompts = get_ipython().prompts def in_prompt_tokens(self, cli=None): if self.redirect_happening: self.redirect_happening = False return [(Token.Prompt,'***** COMINT REDIRECT ENDED *****')] else: return self.initial_prompts.in_prompt_tokens() get_ipython().prompts = __SAGEPrompt(get_ipython()) def __SAGE_get_doc(str): In.pop() get_ipython().prompts.redirect_happening = True get_ipython().run_line_magic('pinfo', str) " "Code to setup fetching documentation from a Sage process. It is needed because the documentation somtimes contains bare prompts which trip up the comint redirection machinery. This works around that.") ;;;; Sage shell mode (defun sage-shell-setup-doc () "Send `sage-shell-doc-setup-code' to Sage process." (python-shell-send-string-no-output sage-shell-doc-setup-code)) (define-derived-mode sage-shell-mode inferior-python-mode "Sage Repl" "Execute Sage commands interactively." (setq-local python-shell-eval-setup-code sage-shell-eval-setup-code comint-redirect-completed t) (add-hook 'python-shell-first-prompt-hook #'sage-shell-setup-doc) (add-hook 'comint-redirect-filter-functions #'ansi-color-apply nil t)) ;;;###autoload (defun run-sage (&optional dedicated show) "Run an inferior sage process. The command to run the process is calculated using `sage-shell-interpreter' and `sage-shell-interpreter-args'. When called interactively with `prefix-arg', it allows the user to choose whether the interpreter should be DEDICATED to the current buffer or project. When numeric prefix arg is other than 0 or 4 do not SHOW. For a given buffer and same values of DEDICATED, if a process is already running for it, it will do nothing. This means that if the current buffer is using a global process, the user is still able to switch it to use a dedicated one. Type \\[describe-mode] in the process buffer for a list of commands." (interactive (if current-prefix-arg (list (alist-get (car (read-multiple-choice "Make dedicated process?" '((?b "to buffer") (?p "to project") (?n "no")))) '((?b . buffer) (?p . project))) (= (prefix-numeric-value current-prefix-arg) 4)) (list python-shell-dedicated t))) (let* ((proc (python-shell-get-process-name dedicated)) (buf (get-buffer-create (format "*%s*" proc)))) (unless (get-buffer-process buf) (with-current-buffer buf (let ((inhibit-read-only t)) (erase-buffer)) (apply #'make-comint-in-buffer proc buf sage-shell-interpreter nil sage-shell-interpreter-args) ;; Ideally we should able to bind `python-shell-interpreter' and ;; `python-shell-interpreter-args' and call `python-run'. However, that ;; runs into problems since `inferior-python-mode' uses ;; `python-shell--interpreter' and `python-shell--interpreter-args' ;; which can only be let bound from within `python.el'. This makes it ;; impossible to call any derived mode of `inferior-python-mode' ;; without dealing with these internal variables. (dlet ((python-shell--interpreter "sage") (python-shell--interpreter-args "--simple-prompt")) (let ((python-shell-prompt-detect-enabled nil) (python-shell-interpreter-interactive-arg "") (python-shell-prompt-regexp "sage: ")) (sage-shell-mode)) (setq-local python-shell-prompt-detect-enabled nil python-shell-interpreter-interactive-arg "" python-shell-prompt-regexp "sage: ")))) (if show (display-buffer buf)) proc)) ;;;; Sage Mode (defvar-keymap sage-mode-map " " #'run-sage " " #'sage-lookup-doc) ;;;###autoload (define-derived-mode sage-mode python-mode "Sage" :group 'sage (setq-local python-shell-buffer-name "Sage" python-shell-interpreter sage-shell-interpreter python-shell-interpreter-args (combine-and-quote-strings sage-shell-interpreter-args) python-shell-eval-setup-code sage-shell-eval-setup-code)) ;;;###autoload (cl-pushnew `(,(rx ".sage" eos) . sage-mode) auto-mode-alist :test #'equal) ;;;; Documentation (defun sage--redirect-send (command output-buffer process echo no-display) "Version of `comint-redirect-send-command-to-process'. Which see for COMMAND, OUTPUT-BUFFER, PROCESS, ECHO and NO-DISPLAY. The COMMAND must output ***** COMINT REDIRECT ENDED ***** as the last line." (with-current-buffer (process-buffer process) (if (and (python-util-comint-end-of-output-p) comint-redirect-completed) (let ((comint-redirect-perform-sanity-check nil) (comint-prompt-regexp (rx bol "***** COMINT REDIRECT ENDED *****"))) (comint-redirect-send-command-to-process command output-buffer process echo no-display)) (error "Sage process is busy")))) (defun sage-lookup-doc (symbol &optional proc display) "Look up documentation for the SYMBOL. It is the symbol at point when called interactively. PROC is the sage process. If DISPLAY is non-nil, buffer is displayed." (interactive (list (python-info-current-symbol) (python-shell-get-process) t)) (let ((buf (get-buffer-create "*Sage Documentation*")) (proc (or proc (python-shell-get-process)))) (prog1 buf (with-current-buffer buf (erase-buffer) (setq mode-line-format nil) (font-lock-mode) (visual-line-mode)) (sage--redirect-send (format "__SAGE_get_doc('%s')" symbol) buf proc nil (not display))))) ;;;; Provide (provide 'sage) ;;; sage.el ends here