diff --git a/inf-fsharp-mode.el b/inf-fsharp-mode.el index 13210d8..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. @@ -90,7 +91,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 @@ -101,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.