Skip to content

Inferior fsharp tab completion #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 136 additions & 2 deletions inf-fsharp-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -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.")

Expand All @@ -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 "<tab>") 'inferior-fsharp-get-completion)
map))

;; Augment fsharp mode, so you can process fsharp code in the source files.
Expand Down Expand Up @@ -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
Expand All @@ -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 <tab> 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
;; <tab> 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.
Expand Down