Skip to content

Commit 0d6472d

Browse files
author
Greg Sexton
committed
Implement kernel-backed completion
1 parent 8d3ece8 commit 0d6472d

File tree

3 files changed

+110
-33
lines changed

3 files changed

+110
-33
lines changed

README.org

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,19 +157,24 @@
157157
SRC block, place the point on the thing you're interested in and
158158
run =M-x ob-ipython-inspect=. I recommend you bind this to a key.
159159

160+
* ~ob-ipython-completions~ queries the kernel for completions at a
161+
position. You may use this to hook up any completion mechanism.
162+
We already provide a company backend. With company installed, add
163+
~(add-to-list 'company-backends 'company-ob-ipython)~ somewhere
164+
in your config. This should then work while editing a src block.
165+
160166
* It's often easier to play with code using a REPL. With the point
161167
in an ipython SRC block, you can open a REPL connected to the
162-
current kernel by running =C-c C-v C-z=. I recommend you do this
163-
anyway, as python-mode can now use this REPL to provide
164-
completion in code buffers.
168+
current kernel by running =C-c C-v C-z=.
165169

166170
* If evaluated code produces an error, this will be displayed
167171
nicely in a buffer using IPython's traceback support.
168172

169-
* Stdout from code evaluation is displayed in a popup buffer. This
170-
is great for debugging or getting verbose output that is best
171-
left out of documents. If you wish to capture stdout in your
172-
document use the =:results output= SRC block header.
173+
* Stdout/err from code evaluation is displayed in a popup buffer.
174+
This is great for debugging or getting verbose output that is
175+
best left out of documents (e.g progress updates). If you wish to
176+
capture output in your document use the =:results output= SRC
177+
block header.
173178

174179
* You can interrupt or kill a running kernel. This is helpful if
175180
things get stuck or really broken. See =M-x
@@ -214,8 +219,6 @@
214219
(add-hook 'org-babel-after-execute-hook 'org-display-inline-images 'append)
215220
#+END_SRC
216221

217-
* Open a REPL using =C-c C-v C-z= so that you get completion in Python buffers.
218-
219222
* Export with the =LaTeX= backend using the =minted= package for
220223
source block highlighting fails for =ipython= blocks by default
221224
with the error

client.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,31 @@
99
semaphore = threading.Semaphore(value=0)
1010
interested_lock = threading.Lock()
1111
interested = []
12-
hasreply = False
13-
hasidle = False
1412

15-
def msg_router(name, ch):
16-
global hasreply, hasidle
13+
def msg_router(io, shell):
1714
while True:
18-
msg = ch()
19-
msg['channel'] = name
15+
msg = io()
16+
msg['channel'] = 'io'
2017
msgid = msg['parent_header'].get('msg_id', None)
2118
with interested_lock:
2219
if msgid not in interested:
2320
continue
24-
if msg.get('msg_type', '') in ['execute_reply', 'inspect_reply']:
25-
hasreply = True
26-
elif (msg.get('msg_type', '') == 'status' and
21+
print(json.dumps(msg, default=str))
22+
if (msg.get('msg_type', '') == 'status' and
2723
msg['content']['execution_state'] == 'idle'):
28-
hasidle = True
29-
if not msg['msg_type'] in ['status', 'execute_input']:
30-
print(json.dumps(msg, default=str))
31-
if hasreply and hasidle:
24+
break
25+
26+
while True:
27+
msg = shell()
28+
msg['channel'] = 'shell'
29+
msgid = msg['parent_header'].get('msg_id', None)
30+
with interested_lock:
31+
if msgid not in interested:
32+
continue
33+
print(json.dumps(msg, default=str))
34+
if msg.get('msg_type', '') in ['execute_reply',
35+
'inspect_reply',
36+
'complete_reply']:
3237
semaphore.release()
3338

3439
def create_client(name):
@@ -39,17 +44,17 @@ def create_client(name):
3944
c = client.BlockingKernelClient(connection_file=cf)
4045
c.load_connection_file()
4146
c.start_channels()
42-
chans = [('io', c.get_iopub_msg), ('shell', c.get_shell_msg), ('stdin', c.get_stdin_msg)]
43-
for name, ch in chans:
44-
t = threading.Thread(target=msg_router, args=(name, ch))
45-
t.setDaemon(True)
46-
t.start()
47+
io, shell = c.get_iopub_msg, c.get_shell_msg
48+
t = threading.Thread(target=msg_router, args=(io, shell))
49+
t.setDaemon(True)
50+
t.start()
4751
return c
4852

4953
parser = argparse.ArgumentParser()
5054
parser.add_argument('--conn-file')
5155
parser.add_argument('--execute', action='store_true')
5256
parser.add_argument('--inspect', action='store_true')
57+
parser.add_argument('--complete', action='store_true')
5358
args = parser.parse_args()
5459

5560
c = create_client(args.conn_file)
@@ -67,4 +72,16 @@ def create_client(name):
6772
detail_level=req.get('detail', 0))
6873
interested.append(msgid)
6974

75+
elif args.complete:
76+
req = json.loads(sys.stdin.read())
77+
code = req['code']
78+
pos = req.get('pos', len(code))
79+
# causes things to hang as kernel doesn't come back with a
80+
# complete_reply
81+
if code[pos-1] in ['\n', '\r']:
82+
sys.exit(0)
83+
msgid = c.complete(code,
84+
cursor_pos=pos)
85+
interested.append(msgid)
86+
7087
semaphore.acquire()

ob-ipython.el

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ a new kernel will be started."
317317

318318
(defun ob-ipython--extract-status (msgs)
319319
(->> msgs
320-
(-filter (lambda (msg) (-contains? '("execute_reply" "inspect_reply")
320+
(-filter (lambda (msg) (-contains? '("execute_reply" "inspect_reply" "complete_reply")
321321
(cdr (assoc 'msg_type msg)))))
322322
car
323323
(assoc 'content)
@@ -340,7 +340,7 @@ a new kernel will be started."
340340
(with-temp-buffer
341341
(let ((ret (apply 'call-process-region input nil
342342
(ob-ipython--get-python) nil t nil
343-
;; TODO: hardcoded default
343+
;; TODO: hardcoded default -- can use local org-src--babel-info
344344
(list "--" ob-ipython-client-path "--conn-file" "default" "--inspect"))))
345345
(if (> ret 0)
346346
(ob-ipython--dump-error (buffer-string))
@@ -368,19 +368,76 @@ a new kernel will be started."
368368
"Ask a kernel for documentation on the thing at POS in BUFFER."
369369
(interactive (list (current-buffer) (point)))
370370
(-if-let (result (->> (ob-ipython--inspect buffer pos) (assoc 'text/plain) cdr))
371-
(ob-ipython--create-inspect-buffer result)
372-
(message "No documentation was found.")))
371+
(ob-ipython--create-inspect-buffer result)
372+
(message "No documentation was found.")))
373+
374+
;; completion
375+
376+
(defun ob-ipython--complete-request (code &optional pos)
377+
(let ((input (json-encode `((code . ,code)
378+
(pos . ,(or pos (length code)))))))
379+
(with-temp-buffer
380+
(let ((ret (apply 'call-process-region input nil
381+
(ob-ipython--get-python) nil t nil
382+
;; TODO: hardcoded default
383+
(list "--" ob-ipython-client-path "--conn-file" "default" "--complete"))))
384+
(if (> ret 0)
385+
(ob-ipython--dump-error (buffer-string))
386+
(goto-char (point-min))
387+
(ob-ipython--collect-json))))))
388+
389+
(defun ob-ipython-completions (buffer pos)
390+
"Ask a kernel for completions on the thing at POS in BUFFER."
391+
(let* ((code (with-current-buffer buffer
392+
(buffer-substring-no-properties (point-min) (point-max))))
393+
(resp (ob-ipython--complete-request code pos))
394+
(status (ob-ipython--extract-status resp)))
395+
(if (not (string= "ok" status))
396+
'()
397+
(->> resp
398+
(-filter (lambda (msg)
399+
(-contains? '("complete_reply")
400+
(cdr (assoc 'msg_type msg)))))
401+
(-mapcat (lambda (msg)
402+
(->> msg
403+
(assoc 'content)
404+
cdr)))))))
405+
406+
(defun company-ob-ipython (command &optional arg &rest ignored)
407+
(interactive (list 'interactive))
408+
(cl-case command
409+
(interactive (company-begin-backend 'company-ob-ipython))
410+
(prefix (and
411+
ob-ipython-mode
412+
(let ((res (ob-ipython-completions (current-buffer) (1- (point)))))
413+
(substring (buffer-string) (cdr (assoc 'cursor_start res))
414+
(cdr (assoc 'cursor_end res))))))
415+
(candidates (let ((res (ob-ipython-completions (current-buffer) (1- (point)))))
416+
(cdr (assoc 'matches res))))
417+
(sorted t)))
418+
419+
;; mode
420+
421+
(define-minor-mode ob-ipython-mode
422+
""
423+
nil
424+
" ipy"
425+
'())
373426

374427
;; babel framework
375428

376429
(add-to-list 'org-src-lang-modes '("ipython" . python))
377430

378431
(defvar org-babel-default-header-args:ipython '())
379432

433+
(defun org-babel-edit-prep:ipython (info)
434+
;; TODO: based on kernel, should change the mode
435+
(ob-ipython-mode +1))
436+
380437
(defun ob-ipython--normalize-session (session)
381438
(if (string= "default" session)
382-
(error "default is reserved for when no name is provided. Please use a different session name.")
383-
(or session "default")))
439+
(error "default is reserved for when no name is provided. Please use a different session name.")
440+
(or session "default")))
384441

385442
(defun org-babel-execute:ipython (body params)
386443
"Execute a block of IPython code with Babel.

0 commit comments

Comments
 (0)