Skip to content

Commit

Permalink
First attempt at tab completion for inferior fsharp
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jdodds committed Mar 9, 2019
1 parent abd533a commit f589e6c
Showing 1 changed file with 128 additions and 1 deletion.
129 changes: 128 additions & 1 deletion 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 @@ -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 <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

0 comments on commit f589e6c

Please sign in to comment.