From abd533ac842f0aa1adc6c92f69bb072709fcca20 Mon Sep 17 00:00:00 2001 From: Jeremiah Dodds Date: Fri, 1 Mar 2019 12:25:09 -0500 Subject: [PATCH 1/2] Tell comint to set TERM=emacs for fsi, and don't force using a pipe We want to be able to use fsi's tab-completion, but we can't if we have to pass --readline- as it turns off readline support. comint reports itself as a dumb-terminal by default, which causes fsi to treat it like one. Setting TERM=emacs (or likely and value that's not 'dumb') appears to let us interact with fsi in the way we want to. The previous combination of (process-connection-nil) and setting --readline- was probably done to get around fsi's behavior when using a pty and TERM=dumb, but Im not 100% sure of that. This setup for init-ing the inferior fsharp process might need revisted in the future. --- inf-fsharp-mode.el | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/inf-fsharp-mode.el b/inf-fsharp-mode.el index 13210d8..185d7e1 100644 --- a/inf-fsharp-mode.el +++ b/inf-fsharp-mode.el @@ -90,7 +90,14 @@ be sent from another buffer in fsharp mode. (or cmd (read-from-minibuffer "fsharp toplevel to run: " inferior-fsharp-program))) (let ((cmdlist (inferior-fsharp-args-to-list inferior-fsharp-program)) - (process-connection-type nil)) + ;; fsi (correctly) disables any sort of console interaction if it + ;; thinks we're a dumb terminal, and `comint-term-environment' + ;; (correctly) defaults to setting TERM=dumb on systems using + ;; terminfo, which is basically every modern system. + ;; + ;; we want to make use of fsi's tab completion, so tell comint + ;; to set TERM=emacs for our inferior fsharp process. + (comint-terminfo-terminal "emacs")) (with-current-buffer (apply (function make-comint) inferior-fsharp-buffer-subname (car cmdlist) nil From f589e6c752d4aafd04e5833773de93f76c8afb8b Mon Sep 17 00:00:00 2001 From: Jeremiah Dodds Date: Sun, 3 Mar 2019 08:40:28 -0500 Subject: [PATCH 2/2] First attempt at tab completion for inferior fsharp This is a start at implementing tab completion for inferior fsharp buffers through comint-redirect style process filters. The completion process is as follows: When inferior-fsharp-get-completion is triggered, we copy the input at the inferior fsharp prompt and clear the input. We then set up redirection of the inferior processes output via comint-redirect-setup, and set up a proess filter to check the output for our completion. Once we get output that looks like a completion, we grab it, send (length completion) backspaces to the inferior process to clear it's input buffer, remove our process filter, undo our redirection setup, return our completion back to the function that requested it, and insert it in the inferior fsharp prompt line. This is pretty awkward feeling due to needing to manage our buffer's prompt and fsi's input buffer seperately, hopefully that's not entirely unavoidable. Right now, this lacks a few things needed as a bare minimum to be acceptable, imo: + it currently assumes a completion will always be returned (easy to fix) + it needs to call (sleep-for) to ensure our output filters aren't deactivated before all the output from fsi has been received + it will only ever give the first completion -- the way fsi does it is to return subsequent members of completions on subsequent tab presses, we don't have a mechanism for handling this yet. The first point can probably be fixed just by looking for a bell in output or using a timeout, but the second two will require a more fleshed out solution for process/buffer communication. Need to read python-shell-completion-native and probably some other inferior-repl modes completion implementations for guidance. We also need to figure out how to write tests for this and write them before going much further here. --- inf-fsharp-mode.el | 129 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/inf-fsharp-mode.el b/inf-fsharp-mode.el index 185d7e1..0de3e5f 100644 --- a/inf-fsharp-mode.el +++ b/inf-fsharp-mode.el @@ -38,7 +38,7 @@ (defvar inferior-fsharp-program (if fsharp-ac-using-mono - "fsharpi --readline-" + "fsharpi" (concat "\"" (fsharp-mode--executable-find "fsi.exe") "\" --fsi-server-input-codepage:65001")) "*Program name for invoking an inferior fsharp from Emacs.") @@ -48,6 +48,7 @@ (defvar inferior-fsharp-mode-map (let ((map (copy-keymap comint-mode-map))) (define-key map [M-return] 'fsharp-comint-send) + (define-key map (kbd "") 'inferior-fsharp-get-completion) map)) ;; Augment fsharp mode, so you can process fsharp code in the source files. @@ -108,6 +109,132 @@ be sent from another buffer in fsharp mode. (inferior-fsharp-mode)) (display-buffer inferior-fsharp-buffer-name)))) + +;; the first value returned from our inferior f# process that appears to be a +;; completion. our filters can end up receiving multiple results that would match +;; any reasonable regexps, doing this prevents clobbering our match with +;; confusing-looking values. +;; +;; this will hopefully go away as we figure out how to get full completion results +;; from fsi without the sort of awkward automation we're doing here, but on the +;; chance that it doesn't we might consider making this buffer-local in the case +;; that people want to use multiple inferior fsharp buffers in the future. +(defvar inf-fsharp-completion-match nil) + +;; the completion functions below are almost directly ripped from `comint', in +;; particular the `comint-redirect' functions. since `comint' pretty much +;; assumes we're line- based, and we cant easily (as far as i know) extend fsi +;; at runtime to let us retrieve full completion info the way that the +;; `python-shell-completion-native' functions do, we need to do some extra stuff +;; to send and handle deleting input that comint doesn't know has already +;; been sent to fsi + +(defun inf-fsharp-redirect-filter (process input-string) + (with-current-buffer (process-buffer process) + (unless inf-fsharp-completion-match + ;; this if-cascasde doesn't work if we convert it to a cond clause ?? + (if (and input-string + (string-match comint-redirect-finished-regexp input-string)) + (setq inf-fsharp-completion-match input-string) + + ;; for some reason, we appear to get the results from fsi fontified + ;; already in `comint-redirect-previous-input-string' without having + ;; them pass through this function as `input-string' even though this + ;; function (or comint-redirect-filter when we were using that directly) + ;; is the only place we've been able to find that modifies the variable. + ;; + ;; looks like a race-condition or multithreading issue but not sure. + ;; either way, we need to check here to make sure we don't miss our match + (if (and comint-redirect-previous-input-string + (string-match comint-redirect-finished-regexp + (concat comint-redirect-previous-input-string input-string))) + (setq inf-fsharp-completion-match + (concat comint-redirect-previous-input-string input-string))))) + + (setq comint-redirect-previous-input-string input-string) + + (if inf-fsharp-completion-match + (let ((del-string (make-string (length inf-fsharp-completion-match) ?\b))) + ;; fsi thinks we should have completed string that hasn't been sent in + ;; the input buffer, but we will actually send later after inserting + ;; the fsi-completed string into our repl buffer, so we need to delete + ;; the match from fsi's input buffer to avoid sending nonsense strings. + (process-send-string process del-string) + + ;; we need to make sure our deletion command goes through before we + ;; exit this func and remove our current output-filter otherwise we'll + ;; end up with the output from fsi confirming our backspaces in our + ;; repl buffer, i.e, if we had "string" we'd see "strin stri str st s + ;; ". we'd like to find a better way than sleeping to do this, but + ;; there's not really a way for emacs to know that a process is done + ;; sending it input as opposed to just not sending it yet.... + (sleep-for 1) + + (save-excursion + (set-buffer comint-redirect-output-buffer) + (erase-buffer) + (goto-char (point-min)) + (insert (ansi-color-filter-apply inf-fsharp-completion-match))) + + (comint-redirect-cleanup) + (run-hooks 'comint-redirect-hook))))) + +(defun inf-fsharp-redirect-get-completion-from-process (input output-buffer process) + (let* ((process-buffer (if (processp process) + (process-buffer process) + process)) + (proc (get-buffer-process process-buffer))) + + (with-current-buffer process-buffer + (comint-redirect-setup + output-buffer + (current-buffer) + (concat input "\\(.+\\)") + nil) + + (set-process-filter proc #'inf-fsharp-redirect-filter) + (process-send-string (current-buffer) (concat input "\t"))))) + +(defun inf-fsharp-get-completion-from-process (process to-complete) + (let ((output-buffer " *inf-fsharp-completion*")) + (with-current-buffer (get-buffer-create output-buffer) + (erase-buffer) + (inf-fsharp-redirect-get-completion-from-process to-complete output-buffer process) + + (set-buffer (process-buffer process)) + (while (and (null comint-redirect-completed) + (accept-process-output process))) + + (set-buffer output-buffer) + (buffer-substring-no-properties (point-min) (point-max))))) + +(defun inferior-fsharp-get-completion () + (interactive) + (let* ((inf-proc (get-process inferior-fsharp-buffer-subname)) + (orig-filter (process-filter inf-proc))) + + ;; reset our global completion match marker every time we start a completion + ;; search so we don't accidentally use old complete data. + (setq inf-fsharp-completion-match nil) + + (with-current-buffer (process-buffer inf-proc) + (let* ((pos (marker-position (cdr comint-last-prompt))) + (input (buffer-substring-no-properties pos (point)))) + + ;; we get the whole of our input back from fsi in the response to our + ;; completion request, so remove the initial repl input here and + ;; replace it with that response. + (delete-backward-char (length input)) + + (insert (inf-fsharp-get-completion-from-process inf-proc input)))) + + ;; we'd prefer to reset this filter closer in the file to where we replace + ;; it, but we ran into some issues with setting it too early. try to fix + ;; this up when we figure out a nicer way of doing this completion stuff + ;; overall. + (set-process-filter inf-proc orig-filter))) + + ;;;###autoload (defun run-fsharp (&optional cmd) "Run an inferior fsharp process.