Skip to content

Commit a56895c

Browse files
committed
WIP: Add omnicompletion support
I debated two different approaches: 1. Inject a namespace which loads needed dependencies and uses `clojure.repl/apropos` as a fallback 2. Use a custom classloader with the classpath of the repl project and any dependencies Ultimately I decided against option 2 because it has a lot more upkeep with regards to dynamic evaluation in the repl; e.g. user evals a new defn, that won't be picked up (I think?). The other point is that I know too little about how classloaders work to feel comfortable using a custom one. Omnicomplete function requires the plugin to be able to return data to the client. This was not possible with the async implementation of `run-command`, so I renamed that to `run-command-async` and added `run-command` as a sync alternative. NOTE: This requires clojure-vim/neovim-client#4
1 parent 0912bb9 commit a56895c

File tree

3 files changed

+137
-33
lines changed

3 files changed

+137
-33
lines changed

plugin/socketrepl.vim

+16
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,22 @@ function! ReadySwitchBufferNS()
191191
endfunction
192192
command! SwitchBufferNS call ReadySwitchBufferNS()
193193

194+
function! socketrepl#omnicomplete(findstart, base)
195+
if a:findstart
196+
let res = rpcrequest(g:nvim_tcp_plugin_channel, 'complete-initial', [])
197+
return l:res
198+
else
199+
echo a:base
200+
let res = rpcrequest(g:nvim_tcp_plugin_channel, 'complete-matches', a:base)
201+
return l:res
202+
endif
203+
endfunction
204+
205+
augroup socketrepl_completion
206+
autocmd!
207+
autocmd FileType clojure setlocal omnifunc=socketrepl#omnicomplete
208+
augroup END
209+
194210
if !exists('g:disable_socket_repl_mappings')
195211
nnoremap K :DocCursor<cr>
196212
nnoremap [d :SourceCursor<cr>

src/socket_repl/socket_repl.clj

+13-9
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
(defn subscribe-output
2121
"Pipes the socket repl output to `chan`"
22-
[{:keys [output-channel]} chan]
23-
(async/pipe output-channel chan))
22+
[{:keys [output-mult]} chan]
23+
(async/tap output-mult chan))
2424

2525
(defn connect
2626
"Create a connection to a socket repl."
@@ -93,10 +93,14 @@
9393

9494
(defn new
9595
[]
96-
{:input-channel (async/chan 1024)
97-
:output-channel (async/chan 1024 (map edn/read-string))
98-
:connection (atom {:host nil
99-
:port nil
100-
:socket nil
101-
:reader nil
102-
:print-stream nil})})
96+
(let [output-channel (async/chan
97+
(async/sliding-buffer 1)
98+
(map edn/read-string))]
99+
{:input-channel (async/chan 1024)
100+
:output-channel output-channel
101+
:output-mult (async/mult output-channel)
102+
:connection (atom {:host nil
103+
:port nil
104+
:socket nil
105+
:reader nil
106+
:print-stream nil})}))

src/socket_repl/socket_repl_plugin.clj

+108-24
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Neovim."
44
(:require
55
[clojure.core.async :as async]
6+
[clojure.edn :as edn]
67
[clojure.java.io :as io]
78
[clojure.string :as string]
89
[clojure.tools.namespace.find :as namespace.find]
@@ -28,7 +29,7 @@
2829
(string/join "\n" (map str (.getStackTrace throwable)))
2930
"\n######################\n")))
3031

31-
(defn run-command
32+
(defn run-command-async
3233
[{:keys [nvim nrepl socket-repl]} f]
3334
(fn [msg]
3435
(if-not (or (socket-repl/connected? socket-repl)
@@ -38,7 +39,17 @@
3839
nvim ":echo 'Use :Connect host:port to connect to a socket repl'"))
3940
(async/thread (f msg)))
4041
;; Don't return an async channel, return something msg-pack can serialize.
41-
:done))
42+
"done"))
43+
44+
(defn run-command
45+
[{:keys [nvim nrepl socket-repl]} f]
46+
(fn [msg]
47+
(if-not (or (socket-repl/connected? socket-repl)
48+
(nrepl/connected? nrepl))
49+
(async/thread
50+
(api/command
51+
nvim ":echo 'Use :Connect host:port to connect to a socket repl'"))
52+
(f msg))))
4253

4354
(defn get-rlog-buffer
4455
"Returns the buffer w/ b:rlog set, if one exists."
@@ -59,6 +70,12 @@
5970
(let [buffer (get-rlog-buffer nvim)]
6071
(when buffer (api.buffer/get-number nvim buffer))))
6172

73+
(defn get-completion-context
74+
[nvim]
75+
(let [skip-exp "synIDattr(synID(line(\".\"),col(\".\"),1),\"name\") =~? \"comment\\|string\\|char\\|regexp\""
76+
ctx-start (api/call-function "searchpairpos" ["(" "" ")" "Wrnb" skip-exp])
77+
ctx-start-here (api/call-function "searchpairpos" ["(" "" ")" "Wrnc" skip-exp])]))
78+
6279
(defn code-channel
6380
[plugin]
6481
(:code-channel plugin))
@@ -88,15 +105,23 @@
88105
(async/>!! (socket-repl/input-channel internal-socket-repl)
89106
'(do
90107
(ns srepl.injection
91-
(:refer-clojure :rename {eval core-eval}))
92-
(defn eval
93-
([form] (eval form nil))
94-
([form options]
95-
(let [result (core-eval form)]
96-
(merge
97-
#:socket-repl{:result (pr-str result)
98-
:form (pr-str form)}
99-
options))))))
108+
(:require
109+
[clojure.repl :as repl]))
110+
111+
(defonce available-plugins (atom {}))
112+
113+
(try
114+
(require '[compliment.core :as compliment])
115+
(swap! available-plugins assoc :compliment true)
116+
(catch Exception e
117+
nil))
118+
119+
(defn completions
120+
[prefix options]
121+
(if
122+
(:compliment @available-plugins)
123+
(compliment/completions prefix options)
124+
(repl/apropos prefix)))))
100125
(catch Throwable t
101126
(log/error t "Error connecting to socket repl")
102127
(async/thread (api/command
@@ -125,7 +150,7 @@
125150
(nvim/register-method!
126151
nvim
127152
"eval"
128-
(run-command
153+
(run-command-async
129154
plugin
130155
(fn [msg]
131156
(try
@@ -142,7 +167,7 @@
142167
(nvim/register-method!
143168
nvim
144169
"eval-form"
145-
(run-command
170+
(run-command-async
146171
plugin
147172
(fn [msg]
148173
(let [[row col] (api-ext/get-cursor-location nvim)
@@ -157,7 +182,7 @@
157182
(nvim/register-method!
158183
nvim
159184
"eval-buffer"
160-
(run-command
185+
(run-command-async
161186
plugin
162187
(fn [msg]
163188
(let [buffer (api/get-current-buf nvim)]
@@ -169,7 +194,7 @@
169194
(nvim/register-method!
170195
nvim
171196
"doc"
172-
(run-command
197+
(run-command-async
173198
plugin
174199
(fn [msg]
175200
(let [code (format "(clojure.repl/doc %s)" (-> msg
@@ -180,7 +205,7 @@
180205
(nvim/register-method!
181206
nvim
182207
"doc-cursor"
183-
(run-command
208+
(run-command-async
184209
plugin
185210
(fn [msg]
186211
(api-ext/get-current-word-async
@@ -192,7 +217,7 @@
192217
(nvim/register-method!
193218
nvim
194219
"source"
195-
(run-command
220+
(run-command-async
196221
plugin
197222
(fn [msg]
198223
(let [code (format "(clojure.repl/source %s)" (-> msg
@@ -203,7 +228,7 @@
203228
(nvim/register-method!
204229
nvim
205230
"source-cursor"
206-
(run-command
231+
(run-command-async
207232
plugin
208233
(fn [msg]
209234
(api-ext/get-current-word-async
@@ -214,8 +239,66 @@
214239

215240
(nvim/register-method!
216241
nvim
217-
"cp"
242+
"complete-initial"
243+
(run-command
244+
plugin
245+
(fn [msg]
246+
(let [line (api/get-current-line nvim)
247+
[cursor-row cursor-col] (api-ext/get-cursor-location nvim)]
248+
(let [start-col (- cursor-col
249+
(->> line
250+
(take cursor-col)
251+
(reverse)
252+
(take-while #(not (#{\ \(} %)))
253+
(count)))]
254+
start-col)))))
255+
256+
(nvim/register-method!
257+
nvim
258+
"complete-matches"
218259
(run-command
260+
plugin
261+
(fn [msg]
262+
(let [word (first (message/params msg))
263+
context (get-completion-context nvim)
264+
code-form (pr-str
265+
`(srepl.injection/completions
266+
~word
267+
{:ns *ns*
268+
:context ~context}))
269+
#_code-form #_(str "(srepl.injection/completions "
270+
"\"" word "\" "
271+
"{:ns *ns*})")
272+
res-chan (async/chan 1 (filter #(= (:form %)
273+
code-form)))]
274+
(try
275+
(socket-repl/subscribe-output internal-socket-repl res-chan)
276+
(async/>!! (socket-repl/input-channel internal-socket-repl) code-form)
277+
(let [matches (async/<!! res-chan)
278+
r (map (fn [{:keys [candidate type ns] :as match}]
279+
(log/info (str "Match: " match))
280+
{"word" candidate
281+
"menu" ns
282+
"kind" (case type
283+
:function "f"
284+
:special-form "d"
285+
:class "t"
286+
:local "v"
287+
:keyword "v"
288+
:resource "t"
289+
:namespace "t"
290+
:method "f"
291+
:static-field "m"
292+
"")})
293+
(edn/read-string (:val matches)))]
294+
r)
295+
(finally
296+
(async/close! res-chan)))))))
297+
298+
(nvim/register-method!
299+
nvim
300+
"cp"
301+
(run-command-async
219302
plugin
220303
(fn [msg]
221304
(let [code-form "(map #(.getAbsolutePath %) (clojure.java.classpath/classpath))"]
@@ -230,12 +313,12 @@
230313
(log/info (:ms res))
231314
(log/info (:val res)))
232315
(finally
233-
(.close res-chan)))))))))
316+
(async/close! res-chan)))))))))
234317

235318
(nvim/register-method!
236319
nvim
237320
"switch-buffer-ns"
238-
(run-command
321+
(run-command-async
239322
plugin
240323
(fn [msg]
241324
(let [buffer-name (api/get-current-buf nvim)
@@ -246,12 +329,13 @@
246329
code-form (if eval-entire-declaration?
247330
namespace-declaration
248331
`(clojure.core/in-ns '~(second namespace-declaration)))]
249-
(async/>!! (socket-repl/input-channel socket-repl) code-form)))))
332+
(async/>!! (socket-repl/input-channel socket-repl) code-form)
333+
(async/>!! (socket-repl/input-channel internal-socket-repl) code-form)))))
250334

251335
(nvim/register-method!
252336
nvim
253337
"show-log"
254-
(run-command
338+
(run-command-async
255339
plugin
256340
(fn [msg]
257341
(let [file (-> repl-log repl-log/file .getAbsolutePath)]
@@ -272,7 +356,7 @@
272356
(nvim/register-method!
273357
nvim
274358
"dismiss-log"
275-
(run-command
359+
(run-command-async
276360
plugin
277361
(fn [msg]
278362
(api/command

0 commit comments

Comments
 (0)