From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Stefan Monnier Newsgroups: gmane.emacs.devel Subject: Standardized access to a REPL (was: Suggesting that feature/tree-sitter be merged) Date: Sat, 19 Nov 2022 09:07:58 -0500 Message-ID: References: <0249C656-21C8-49F2-B979-A1894BF80637@gmail.com> <874juvhoyi.fsf@posteo.net> <72C5E060-76D5-45C7-80E8-C794468DAAB8@gmail.com> <87o7t35sjm.fsf@posteo.net> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="21173"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: Gnus/5.13 (Gnus v5.13) Cc: Yuan Fu , Jostein =?windows-1252?Q?Kj=F8nigsen?= , emacs-devel , Theodor Thornhill , Eli Zaretskii , jostein@kjonigsen.net To: Philip Kaludercic Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Sat Nov 19 15:08:24 2022 Return-path: Envelope-to: ged-emacs-devel@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1owOW8-0005ND-Ck for ged-emacs-devel@m.gmane-mx.org; Sat, 19 Nov 2022 15:08:24 +0100 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1owOVx-0004a6-Eg; Sat, 19 Nov 2022 09:08:13 -0500 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1owOVs-0004Xa-CS for emacs-devel@gnu.org; Sat, 19 Nov 2022 09:08:11 -0500 Original-Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1owOVo-0003Ao-5C; Sat, 19 Nov 2022 09:08:07 -0500 Original-Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1]) by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 7C75B440B8C; Sat, 19 Nov 2022 09:08:01 -0500 (EST) Original-Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1]) by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 1A652440B7A; Sat, 19 Nov 2022 09:08:00 -0500 (EST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca; s=mail; t=1668866880; bh=JTFfBgvP0W1FOX05asEckslJ769hKW5l/hFW173lZZM=; h=From:To:Cc:Subject:In-Reply-To:References:Date:From; b=anXMO50dh+q0C5oQcRTzgSsXOELhelrzMXlD1v9x/xDSQRs6zfoPO23bP4GjQltXF p0Wu07V8Z1+gdbIGZ9e8/V8WgXR6+496Msug2JBIdJsZcdJ3ii2qNJ43aruwZwv03p uZQmhdyvpFl7FDIACi+NUy8eza6cNFwOUWC2hx3CxbSR2r/0gHk5HkezGyTii4V9vb 7oZz64aYJ9hYZnF6bWsNP9RUQGM4/QyEQlPzpPelYBwajbM67GnfMAK2BPt4EfAUlv kaTwr6BiyyJd6/m11LZUWyVXFXCs4xHuU3mABBydDQ3NJDab4vWG8HQwHm8RMqMGW1 DGr/zPEpZ7vbA== Original-Received: from pastel (unknown [104.247.241.157]) by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id C0FF4120E56; Sat, 19 Nov 2022 09:07:59 -0500 (EST) In-Reply-To: <87o7t35sjm.fsf@posteo.net> (Philip Kaludercic's message of "Sat, 19 Nov 2022 07:09:49 +0000") Received-SPF: pass client-ip=132.204.25.50; envelope-from=monnier@iro.umontreal.ca; helo=mailscanner.iro.umontreal.ca X-Spam_score_int: -42 X-Spam_score: -4.3 X-Spam_bar: ---- X-Spam_report: (-4.3 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, RCVD_IN_DNSWL_MED=-2.3, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Original-Sender: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.devel:300179 Archived-At: --=-=-= Content-Type: text/plain >>> `compiled-prog-mode' that has generic commands for building program, >>> `interpreted-prog-mode' that has generic commands for REPL [...] > (define-prog-mode foo > :type 'compiled Let me point out that the idea that some languages are compiled and others are interpreted is bogus. This is a property of a language's *implementation* and not of the language per se. And of course, here we don't even really care about this facet of the implementation: you're using those terms as a proxy for whether we use a REPL or a batch-compiler. Many languages have both REPLs and batch compilers (like, say ELisp), so a major mode needs to be able to offer access to both functionalities at the same time. Regarding the original suggestion to provide a uniform access to a REPL, I started on this a long time ago, but never got it "finished" :-( I attached what I still have of that effort. Stefan --=-=-= Content-Type: application/emacs-lisp Content-Disposition: attachment; filename=prog-proc.el Content-Transfer-Encoding: quoted-printable ;;; prog-proc.el --- Interacting from a source buffer with an inferior proc= ess -*- lexical-binding:t -*- ;; Copyright (C) 2012 Free Software Foundation, Inc. ;; Author: (Stefan Monnier) ;; 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: ;; Prog-Proc is a package designed to complement Comint: while Comint was ;; designed originally to handle the needs of inferior process buffers, such ;; as a buffer running a Scheme repl, Comint does not actually provide any ;; functionality that links this process buffer with some source code. ;; ;; That's where Prog-Proc comes into play: it provides the usual commands a= nd ;; key-bindings that lets the user send his code to the underlying repl. ;;; Code: (eval-when-compile (require 'cl-lib)) (require 'comint) (require 'compile) (defvar prog-proc-mode-map (let ((map (make-sparse-keymap))) (define-key map [?\C-c ?\C-l] 'prog-proc-load-file) (define-key map [?\C-c ?\C-c] 'prog-proc-compile) (define-key map [?\C-c ?\C-z] 'prog-proc-switch-to) (define-key map [?\C-c ?\C-r] 'prog-proc-send-region) (define-key map [?\C-c ?\C-b] 'prog-proc-send-buffer) ;; FIXME: Add ;; (define-key map [?\M-C-x] 'prog-proc-send-defun) ;; (define-key map [?\C-x ?\C-e] 'prog-proc-send-last-sexp) ;; FIXME: Add menu. Now, that's trickier because keymap inheritance ;; doesn't play nicely with menus! map) "Keymap for `prog-proc-mode'.") (defvar-local prog-proc--buffer nil "The inferior-process buffer to which to send code.") (defmacro prog-proc-define-token (name &optional parent) ;; FIXME: This use of cl-defstruct ends up defining needlessly the ;; predicate, the constructor and their respective inliners. ;; `(progn ;; (cl-defstruct (,name ;; (:predicate nil) ;; (:copier nil) ;; (:constructor nil) ;; (:constructor ,name) ;; ,@(when parent `((:include ,parent))))) ;; (defconst ,name (,name))) (let ((tag-sym (intern (format "cl-struct-%S" name))) (children-sym (intern (format "cl-struct-%S-tags" name)))) `(progn (defvar ,children-sym) (eval-and-compile (cl-struct-define ',name nil ',(or parent ;; FIXME: Make a "token" parent. 'cl-structure-object) nil nil '((cl-tag-slot)) ',children-sym ',tag-sy= m t)) (defconst ,name (vector ',tag-sym))))) (cl-defstruct (prog-proc-descriptor (:constructor prog-proc-make) (:predicate nil) (:copier nil)) (name nil :read-only t) (run nil :read-only t) (load-cmd nil :read-only t) (chdir-cmd nil :read-only t) (command-eol "\n" :read-only t) (compile-commands-alist nil :read-only t)) (defvar prog-proc-descriptor nil "Struct containing the various functions to create a new process, ...") (defmacro prog-proc--prop (prop) `(,(intern (format "prog-proc-descriptor-%s" prop)) (or prog-proc-descriptor ;; FIXME: Look for available ones and pick one. (error "Not a `prog-proc' buffer")))) (defmacro prog-proc--call (method &rest args) `(funcall (prog-proc--prop ,method) ,@args)) ;; The inferior process and his buffer are basically interchangeable. ;; Currently the code takes prog-proc--buffer as the main reference, ;; but all users should either use prog-proc-proc or prog-proc-buffer ;; to find the info. (defun prog-proc-proc () "Return the inferior process for the code in current buffer." (or (and (buffer-live-p prog-proc--buffer) (get-buffer-process prog-proc--buffer)) (when (derived-mode-p 'prog-proc-mode 'prog-proc-comint-mode) (setq prog-proc--buffer (current-buffer)) (get-buffer-process prog-proc--buffer)) (let ((ppd prog-proc-descriptor) (buf (prog-proc--call run))) (with-current-buffer buf (if (and ppd (null prog-proc-descriptor)) (set (make-local-variable 'prog-proc-descriptor) ppd))) (setq prog-proc--buffer buf) (get-buffer-process prog-proc--buffer)))) (defun prog-proc-buffer () "Return the buffer of the inferior process." (process-buffer (prog-proc-proc))) (defun prog-proc-run-repl () "Start the read-eval-print process, if it's not running yet." (interactive) (ignore (prog-proc-proc))) (defun prog-proc-switch-to () "Switch to the buffer running the read-eval-print process." (pop-to-buffer (prog-proc-buffer))) (defun prog-proc-send-string (proc str) "Send command STR to PROC, with an EOL terminator appended." (with-current-buffer (process-buffer proc) ;; FIXME: comint-send-string does not pass the string through ;; comint-input-filter-function, so we have to do it by hand. ;; Maybe we should insert the command into the buffer and then call ;; comint-send-input? (prog-proc-comint-input-filter-function nil) (comint-send-string proc (concat str (prog-proc--prop command-eol))))) (defun prog-proc-load-file (file &optional and-go) "Load FILE into the read-eval-print process. FILE is the file visited by the current buffer. If prefix argument AND-GO is used, then we additionally switch to the buffer where the process is running." (interactive (list (or buffer-file-name (read-file-name "File to load: " nil nil t)) current-prefix-arg)) (comint-check-source file) (let ((proc (prog-proc-proc))) (prog-proc-send-string proc (prog-proc--call load-cmd file)) (when and-go (pop-to-buffer (process-buffer proc))))) (defvar prog-proc--tmp-file nil) (defun prog-proc-send-region (start end &optional and-go) "Send the content of the region to the read-eval-print process. START..END delimit the region; AND-GO if non-nil indicate to additionally switch to the process's buffer." (interactive "r\nP") (if (> start end) (let ((tmp end)) (setq end start) (setq start tmp)) (if (=3D start end) (error "Nothing to send: the region is empty"))) (let ((proc (prog-proc-proc)) (tmp (make-temp-file "emacs-region"))) (write-region start end tmp nil 'silently) (when prog-proc--tmp-file (ignore-errors (delete-file (car prog-proc--tmp-file))) (set-marker (cdr prog-proc--tmp-file) nil)) (setq prog-proc--tmp-file (cons tmp (copy-marker start))) (prog-proc-send-string proc (prog-proc--call load-cmd tmp)) (when and-go (pop-to-buffer (process-buffer proc))))) (defun prog-proc-send-buffer (&optional and-go) "Send the content of the current buffer to the read-eval-print process. AND-GO if non-nil indicate to additionally switch to the process's buffer." (interactive "P") (prog-proc-send-region (point-min) (point-max) and-go)) (define-derived-mode prog-proc-mode prog-mode "Prog-Proc" "Major mode for editing source code and interact with an interactive loop= ." ) ;;; Extended comint-mode for Prog-Proc. (defun prog-proc-chdir (dir) "Change the working directory of the inferior process to DIR." (interactive "DChange to directory: ") (let ((dir (expand-file-name dir)) (proc (prog-proc-proc))) (with-current-buffer (process-buffer proc) (prog-proc-send-string proc (prog-proc--call chdir-cmd dir)) (setq default-directory (file-name-as-directory dir))))) (defun prog-proc-comint-input-filter-function (str) ;; `compile.el' doesn't know that file location info from errors should be ;; recomputed afresh (without using stale info from earlier compilations). (compilation-forget-errors) ;Has to run before compilation-fake-loc. (if (and prog-proc--tmp-file (marker-buffer (cdr prog-proc--tmp-file))) (compilation-fake-loc (cdr prog-proc--tmp-file) (car prog-proc--tmp-file))) str) (defvar prog-proc-comint-mode-map (let ((map (make-sparse-keymap))) (define-key map [?\C-c ?\C-r] 'prog-proc-run-repl) (define-key map [?\C-c ?\C-l] 'prog-proc-load-file) map)) (define-derived-mode prog-proc-comint-mode comint-mode "Prog-Proc-Comint" "Major mode for an inferior process used to run&compile source code." ;; Enable compilation-minor-mode, but only after the child mode is setup ;; since the child-mode might want to add rules to ;; compilation-error-regexp-alist. (add-hook 'after-change-major-mode-hook #'compilation-minor-mode nil t) ;; The keymap of compilation-minor-mode is too unbearable, so we ;; need to hide most of the bindings. (let ((map (make-sparse-keymap))) (dolist (keys '([menu-bar] [follow-link])) ;; Preserve some of the bindings. (define-key map keys (lookup-key compilation-minor-mode-map keys))) (add-to-list 'minor-mode-overriding-map-alist (cons 'compilation-minor-mode map))) (add-hook 'comint-input-filter-functions #'prog-proc-comint-input-filter-function nil t)) (defvar prog-proc--compile-command nil "The command used by default by `prog-proc-compile'.") (defun prog-proc-compile (command &optional and-go) "Pass COMMAND to the read-eval-loop process to compile the current file. You can then use the command \\[next-error] to find the next error message and move to the source code that caused it. Interactively, prompts for the command if `compilation-read-command' is non-nil. With prefix arg, always prompts. Prefix arg AND-GO also means to switch to the read-eval-loop buffer afterwa= rds." (interactive (let* ((dir default-directory) (cmd "cd \".")) ;; Look for files to determine the default command. (while (and (stringp dir) (progn (cl-dolist (cf (prog-proc--prop compile-commands-alist)) (when (file-exists-p (expand-file-name (cdr cf) dir)) (setq cmd (concat cmd "\"; " (car cf))) (cl-return nil))) (not cmd))) (let ((newdir (file-name-directory (directory-file-name dir)))) (setq dir (unless (equal newdir dir) newdir)) (setq cmd (concat cmd "/..")))) (setq cmd (cond ((local-variable-p 'prog-proc--compile-command) prog-proc--compile-command) ((string-match "^\\s-*cd\\s-+\"\\.\"\\s-*;\\s-*" cmd) (substring cmd (match-end 0))) ((string-match "^\\s-*cd\\s-+\"\\(\\./\\)" cmd) (replace-match "" t t cmd 1)) ((string-match ";" cmd) cmd) (t prog-proc--compile-command))) ;; code taken from compile.el (list (if (or compilation-read-command current-prefix-arg) (read-from-minibuffer "Compile command: " cmd nil nil '(compile-history . 1)) cmd)))) ;; ;; now look for command's file to determine the directory ;; (setq dir default-directory) ;; (while (and (stringp dir) ;; (dolist (cf (prog-proc--prop compile-commands-alist) t) ;; (when (and (equal cmd (car cf)) ;; (file-exists-p (expand-file-name (cdr cf) dir))) ;; (return nil)))) ;; (let ((newdir (file-name-directory (directory-file-name dir)))) ;; (setq dir (unless (equal newdir dir) newdir)))) ;; (setq dir (or dir default-directory)) ;; (list cmd dir))) (set (make-local-variable 'prog-proc--compile-command) command) (save-some-buffers (not compilation-ask-about-save) nil) (let ((dir default-directory)) (when (string-match "^\\s-*cd\\s-+\"\\([^\"]+\\)\"\\s-*;" command) (setq dir (match-string 1 command)) (setq command (replace-match "" t t command))) (setq dir (expand-file-name dir)) (let ((proc (prog-proc-proc)) (eol (prog-proc--prop command-eol))) (with-current-buffer (process-buffer proc) (setq default-directory dir) (prog-proc-send-string proc (concat (prog-proc--call chdir-cmd dir) ;; Strip the newline, to avoid adding a prompt. (if (string-match "\n\\'" eol) (replace-match " " t t eol) eol) command)) (when and-go (pop-to-buffer (process-buffer proc))))))) (provide 'prog-proc) ;;; prog-proc.el ends here --=-=-=--