Skip to content
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

Support querying subprocess for completion. #119

Closed
wants to merge 9 commits into from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ hash-bang line (`#!/usr/bin/lua`). Putting this snippet to `.emacs` should be en
- documentation lookup (using online/offline reference manual, e.g. [string.find](http://www.lua.org/manual/5.1/manual.html#pdf-string.find))
- [imenu](http://www.gnu.org/software/emacs/manual/html_node/emacs/Imenu.html) integration
- [HideShow](http://www.gnu.org/software/emacs/manual/html_node/emacs/Hideshow.html) integration
- using `completion-at-point` queries active Lua subprocess for completion targets

## CUSTOMIZATION

Expand All @@ -56,6 +57,7 @@ The following variables are available for customization (see more via `M-x custo
- Var `lua-mode-hook`: list of functions to execute when lua-mode is initialized
- Var `lua-documentation-url` (default `"http://www.lua.org/manual/5.1/manual.html#pdf-"`): base URL for documentation lookup
- Var `lua-documentation-function` (default `browse-url`): function used to show documentation (`eww` is a viable alternative for Emacs 25)
- Var `lua-local-require-completions` (default `nil`): set to `t` to have completion attempt to load local libraries for current file for completion targets; can cause unexpected side-effects

## LUA SUBPROCESS CREATION

Expand Down
143 changes: 143 additions & 0 deletions lua-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,9 @@ Groups 6-9 can be used in any of argument regexps."
(setq mode-popup-menu
(cons (concat mode-name " Mode Commands") lua-emacs-menu)))

(make-local-variable 'completion-at-point-functions)
(add-to-list 'completion-at-point-functions 'lua-complete-function)

;; hideshow setup
(unless (assq 'lua-mode hs-special-modes-alist)
(add-to-list 'hs-special-modes-alist
Expand Down Expand Up @@ -1825,6 +1828,146 @@ If `lua-process' is nil or dead, start a new process first."
(lua-send-region start end)
(error "Not on a function definition")))))

(defun lua-completion-trim-input (i)
(format "'%s'," (replace-regexp-in-string "[[:space:]]" "" i)))


(defun lua-completion-string-for (expr libs locals out-file)
"Construct a string of Lua code to write completions to `out-file'.

The `expr' arg should be the input string (which may contain dots
for table lookup), and `libs' should be a list of the format returned
by `lua-local-libs', or nil."
(mapconcat 'identity
`("do"
"local clone = function(t)"
" local n = {} for k,v in pairs(t) do n[k] = v end return n"
"end"

;; Completion context starts as just a clone of _G.
"local top_ctx = clone(_G)"
;; But then we take all matches of local x = require y from
;; our code buffer and stuff them into the completion table too.
,@(mapcar (lambda (l)
(format "top_ctx['%s'] = require(%s)"
(car l) (cadr l))) libs)
;; Top-level locals should also provide completion candidates.
;; We don't know anything about what they contain, so just
;; initialize them to an empty table for 1-level completion.
,@(mapcar (apply-partially 'format "top_ctx['%s'] = {}") locals)

;; recursively delve into context based on input_parts
"local function cpl_for(input_parts, ctx)"
" if type(ctx) ~= \"table\" then return {} end"
" if #input_parts == 0 and ctx ~= top_ctx then"
" return ctx"
" elseif #input_parts == 1 then" ; the last segment of input
" local matches = {}"
" for k in pairs(ctx) do"
" if k:find('^' .. input_parts[1]) then"
" table.insert(matches, k)"
" end"
" end"
" return matches"
" else" ; more segments of input remain; descend into ctx table
" local token1 = table.remove(input_parts, 1)"
" return cpl_for(input_parts, ctx[token1])"
" end"
"end"

;; this is a table of the segments of the input
"local input = {"
,@(mapcar 'lua-completion-trim-input (split-string expr "\\.")) "}"
;; spit it out to a file; lua-mode can't send data back to emacs
,(format "local f = io.open('%s', 'w')" out-file)
"for _,l in ipairs(cpl_for(input, top_ctx)) do"
" f:write(l .. string.char(10))"
"end"
"f:close()"
"end") " "))

(defvar lua-local-require-completions nil
"During completion, scan file for local require calls for context.

Defaults to nil because this will cause code to be loaded during completion.
Loading arbitrary code can have unexpected side-effects, so use with caution.")

(defvar lua-local-require-regexp
"^local\\s-+\\([^ \n]+\\)\\s-*=\\s-*require[( ]+\\([^ )\n]+\\)"
"A regexp to match lines where a library is required and put in a local.")

(defvar lua-top-level-local-regexp
"^local\\s-+\\([^ \n]+\\)\\s-*="
"A regexp to match top-level local definitions")

(defun lua-local-libs ()
"Find all modules loaded with require which are stored in locals.

Returns a list of lists where the car is the local name and the cadr
is the string that is passed to require."
(save-excursion
(when lua-local-require-completions
(let ((libs nil))
(goto-char (point-min))
;; find each match of "local x = require y" and save for later
(while (lua-find-regexp 'forward lua-local-require-regexp)
(add-to-list 'libs (list (match-string-no-properties 1)
(match-string-no-properties 2))))
libs))))

(defun lua-top-level-locals (lib-names)
"Return a list of all top-level locals in the file for completion targets."
(save-excursion
(let ((locals nil))
(goto-char (point-min))
(while (lua-find-regexp 'forward lua-top-level-local-regexp)
(let ((local (match-string-no-properties 1)))
(when (not (member local lib-names))
(add-to-list 'locals local))))
locals)))

(defun lua-start-of-expr ()
"Search backwards to find the beginning of the current expression.

This is distinct from `backward-sexp' which treats . and : as a separator."
(save-excursion
(backward-sexp)
(let ((bos (point)))
(when (> (point) 1) (backward-char))
(when (string-match "[[:space:]]" (thing-at-point 'char))
(search-backward-regexp "[^\n\s-]" nil t)
(when (string= (thing-at-point 'char) "\n")
(backward-char)))
(if (member (thing-at-point 'char) '(":" "."))
(lua-start-of-expr)
bos))))

(defun lua-complete-function ()
"Completion function for `completion-at-point-functions'.

Queries current lua subprocess for possible completions."
(let* ((start-of-expr (lua-start-of-expr))
(expr (buffer-substring-no-properties start-of-expr (point)))
(expr (car (last (split-string expr "\n")))) ; avoid multi-line input
(libs (lua-local-libs))
(locals (lua-top-level-locals (mapcar 'car libs)))
(file (make-temp-file "lua-completions-")))
(lua-send-string (lua-completion-string-for expr libs locals file))
(sit-for 0.1)
(unwind-protect
(list (save-excursion
(when (symbol-at-point) (backward-sexp))
(point))
(point)
(when (file-exists-p file)
(with-temp-buffer
(insert-file-contents file)
(butlast (split-string
(buffer-substring-no-properties
(point-min) (point-max))
"\n")))))
(delete-file file))))

(defun lua-maybe-skip-shebang-line (start)
"Skip shebang (#!/path/to/interpreter/) line at beginning of buffer.

Expand Down
1 change: 1 addition & 0 deletions test/file.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return {abc = 123, def = 456}
56 changes: 56 additions & 0 deletions test/test-completion.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
;; -*- flycheck-disabled-checkers: (emacs-lisp-checkdoc) -*-
(load (concat (file-name-directory (or load-file-name (buffer-file-name)
default-directory))
"utils.el") nil 'nomessage 'nosuffix)
(require 'cl-lib)

(describe "Test lua-complete-function"
(it "completes top-level globals"
(with-lua-buffer
(insert "tabl")
(run-lua)
(completion-at-point)
(expect (buffer-string) :to-equal "table")))
(it "completes nested globals"
(with-lua-buffer
(insert "table.ins")
(run-lua)
(completion-at-point)
(expect (buffer-string) :to-equal "table.insert")))
(it "completes nested globals with spaces"
(with-lua-buffer
(insert "table. ins")
(run-lua)
(completion-at-point)
(expect (buffer-string) :to-equal "table. insert")))
(it "completes nested globals inside function call"
(with-lua-buffer
(insert "print(table.con)")
(backward-char)
(run-lua)
(completion-at-point)
(expect (buffer-string) :to-equal "print(table.concat)")))
(it "completes nested globals with newline"
(with-lua-buffer
(insert "table.\n ins")
(run-lua)
(completion-at-point)
(expect (buffer-string) :to-equal "table.\n insert"))))

(describe "Test lua-complete-function with lua-local-require-regexp"
(it "completes locally-required libraries"
(with-lua-buffer
(insert "local xyz = require('test.file')\n")
(insert "xy")
(let ((lua-local-require-completions t))
(run-lua)
(completion-at-point))
(expect (thing-at-point 'line) :to-equal "xyz")))
(it "completes values nested in locally-required libraries"
(with-lua-buffer
(insert "local xyz = require('test.file')\n")
(insert "xyz.ab")
(let ((lua-local-require-completions t))
(run-lua)
(completion-at-point))
(expect (thing-at-point 'line) :to-equal "xyz.abc"))))