Skip to content

Commit 3b5f929

Browse files
committed
patch 7.4.1191
Problem: The channel feature isn't working yet. Solution: Add the connect(), disconnect(), sendexpr() and sendraw() functions. Add initial documentation. Add a demo server.
1 parent ba59ddb commit 3b5f929

9 files changed

Lines changed: 846 additions & 39 deletions

File tree

runtime/doc/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ DOCS = \
1717
arabic.txt \
1818
autocmd.txt \
1919
change.txt \
20+
channel.txt \
2021
cmdline.txt \
2122
debug.txt \
2223
debugger.txt \
@@ -151,6 +152,7 @@ HTMLS = \
151152
arabic.html \
152153
autocmd.html \
153154
change.html \
155+
channel.html \
154156
cmdline.html \
155157
debug.html \
156158
debugger.html \

runtime/doc/channel.txt

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
*channel.txt* For Vim version 7.4. Last change: 2016 Jan 28
2+
3+
4+
VIM REFERENCE MANUAL by Bram Moolenaar
5+
6+
7+
Inter-process communication *channel*
8+
9+
DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT
10+
11+
Vim uses channels to communicate with other processes.
12+
A channel uses a socket. *socket-interface*
13+
14+
Vim current supports up to 10 simultanious channels.
15+
The Netbeans interface also uses a channel. |netbeans|
16+
17+
1. Demo |channel-demo|
18+
2. Opening a channel |channel-open|
19+
3. Using a JSON channel |channel-use|
20+
4. Vim commands |channel-commands|
21+
5. Using a raw channel |channel-use|
22+
6. Job control |job-control|
23+
24+
{Vi does not have any of these features}
25+
{only available when compiled with the |+channel| feature}
26+
27+
==============================================================================
28+
1. Demo *channel-demo*
29+
30+
This requires Python. The demo program can be found in
31+
$VIMRUNTIME/tools/demoserver.py
32+
Run it in one terminal. We will call this T1.
33+
34+
Run Vim in another terminal. Connect to the demo server with: >
35+
let handle = connect('localhost:8765', 'json')
36+
37+
In T1 you should see:
38+
=== socket opened === ~
39+
40+
You can now send a message to the server: >
41+
echo sendexpr(handle, 'hello!')
42+
43+
The message is received in T1 and a response is sent back to Vim.
44+
You can see the raw messages in T1. What Vim sends is:
45+
[1,"hello!"] ~
46+
And the response is:
47+
[1,"got it"] ~
48+
The number will increase every time you send a message.
49+
50+
The server can send a command to Vim. Type this on T1 (literally, including
51+
the quotes): >
52+
NOT IMPLEMENTED YET
53+
["ex","echo 'hi there'"]
54+
And you should see the message in Vim.
55+
56+
To handle asynchronous communication a callback needs to be used: >
57+
func MyHandler(handle, msg)
58+
echo "from the handler: " . a:msg
59+
endfunc
60+
call sendexpr(handle, 'hello!', "MyHandler")
61+
62+
Instead of giving a callback with every send call, it can also be specified
63+
when opening the channel: >
64+
call disconnect(handle)
65+
let handle = connect('localhost:8765', 'json', "MyHandler")
66+
call sendexpr(handle, 'hello!', 0)
67+
68+
==============================================================================
69+
2. Opening a channel *channel-open*
70+
71+
To open a channel:
72+
let handle = connect({address}, {mode}, {callback})
73+
74+
{address} has the form "hostname:port". E.g., "localhost:8765".
75+
76+
{mode} can be: *channel-mode*
77+
"json" - Use JSON, see below; most convenient way
78+
"raw" - Use raw messages
79+
80+
*channel-callback*
81+
{callback} is a function that is called when a message is received that is not
82+
handled otherwise. It gets two arguments: the channel handle and the received
83+
message. Example: >
84+
func Handle(handle, msg)
85+
echo 'Received: ' . a:msg
86+
endfunc
87+
let handle = connect("localhost:8765", 'json', "Handle")
88+
89+
When {mode} is "json" the "msg" argument is the body of the received message,
90+
converted to Vim types.
91+
When {mode} is "raw" the "msg" argument is the whole message as a string.
92+
93+
When {mode} is "json" the {callback} is optional. When omitted it is only
94+
possible to receive a message after sending one.
95+
96+
The handler can be added or changed later: >
97+
call sethandler(handle, {callback})
98+
When {callback} is empty (zero or an empty string) the handler is removed.
99+
100+
Once done with the channel, disconnect it like this: >
101+
call disconnect(handle)
102+
103+
==============================================================================
104+
3. Using a JSON channel *channel-use*
105+
106+
If {mode} is "json" then a message can be sent synchronously like this: >
107+
let response = sendexpr(handle, {expr})
108+
This awaits a response from the other side.
109+
110+
To send a message, without handling a response: >
111+
call sendexpr(handle, {expr}, 0)
112+
113+
To send a message and letting the response handled by a specific function,
114+
asynchronously: >
115+
call sendexpr(handle, {expr}, {callback})
116+
117+
The {expr} is converted to JSON and wrapped in an array. An example of the
118+
message that the receiver will get when {expr} is the string "hello":
119+
[12,"hello"] ~
120+
121+
The format of the JSON sent is:
122+
[{number},{expr}]
123+
124+
In which {number} is different every time. It must be used in the response
125+
(if any):
126+
127+
[{number},{response}]
128+
129+
This way Vim knows which sent message matches with which received message and
130+
can call the right handler. Also when the messages arrive out of order.
131+
132+
The sender must always send valid JSON to Vim. Vim can check for the end of
133+
the message by parsing the JSON. It will only accept the message if the end
134+
was received.
135+
136+
When the process wants to send a message to Vim without first receiving a
137+
message, it must use the number zero:
138+
[0,{response}]
139+
140+
Then channel handler will then get {response} converted to Vim types. If the
141+
channel does not have a handler the message is dropped.
142+
143+
On read error or disconnect() the string "DETACH" is sent, if still possible.
144+
The channel will then be inactive.
145+
146+
==============================================================================
147+
4. Vim commands *channel-commands*
148+
149+
NOT IMPLEMENTED YET
150+
151+
With a "json" channel the process can send commands to Vim that will be
152+
handled by Vim internally, it does not require a handler for the channel.
153+
154+
Possible commands are:
155+
["ex", {Ex command}]
156+
["normal", {Normal mode command}]
157+
["eval", {number}, {expression}]
158+
["expr", {expression}]
159+
160+
With all of these: Be careful what these commands do! You can easily
161+
interfere with what the user is doing. To avoid trouble use |mode()| to check
162+
that the editor is in the expected state. E.g., to send keys that must be
163+
inserted as text, not executed as a command: >
164+
["ex","if mode() == 'i' | call feedkeys('ClassName') | endif"]
165+
166+
The "ex" command is executed as any Ex command. There is no response for
167+
completion or error. You could use functions in an |autoload| script.
168+
You can also invoke |feedkeys()| to insert anything.
169+
170+
The "normal" command is executed like with |:normal|.
171+
172+
The "eval" command will result in sending back the result of the expression:
173+
[{number}, {result}]
174+
Here {number} is the same as what was in the request.
175+
176+
The "expr" command is similar, but does not send back any response.
177+
Example:
178+
["expr","setline('$', ['one', 'two', 'three'])"]
179+
180+
==============================================================================
181+
5. Using a raw channel *channel-raw*
182+
183+
If {mode} is "raw" then a message can be send like this: >
184+
let response = sendraw(handle, {string})
185+
The {string} is sent as-is. The response will be what can be read from the
186+
channel right away. Since Vim doesn't know how to recognize the end of the
187+
message you need to take care of it yourself.
188+
189+
To send a message, without expecting a response: >
190+
call sendraw(handle, {string}, 0)
191+
The process can send back a response, the channel handler will be called with
192+
it.
193+
194+
To send a message and letting the response handled by a specific function,
195+
asynchronously: >
196+
call sendraw(handle, {string}, {callback})
197+
198+
This {string} can also be JSON, use |jsonencode()| to create it and
199+
|jsondecode()| to handle a received JSON message.
200+
201+
==============================================================================
202+
6. Job control *job-control*
203+
204+
NOT IMPLEMENTED YET
205+
206+
To start another process: >
207+
call startjob({command})
208+
209+
This does not wait for {command} to exit.
210+
211+
TODO:
212+
213+
let handle = startjob({command}, 's') # uses stdin/stdout
214+
let handle = startjob({command}, '', {address}) # uses socket
215+
let handle = startjob({command}, 'd', {address}) # start if connect fails
216+
217+
218+
vim:tw=78:ts=8:ft=help:norl:

runtime/doc/eval.txt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,8 @@ complete_add( {expr}) Number add completion match
18201820
complete_check() Number check for key typed during completion
18211821
confirm( {msg} [, {choices} [, {default} [, {type}]]])
18221822
Number number of choice picked by user
1823+
connect( {address}, {mode} [, {callback}])
1824+
Number open a channel
18231825
copy( {expr}) any make a shallow copy of {expr}
18241826
cos( {expr}) Float cosine of {expr}
18251827
cosh( {expr}) Float hyperbolic cosine of {expr}
@@ -2027,6 +2029,10 @@ searchpairpos( {start}, {middle}, {end} [, {flags} [, {skip} [...]]])
20272029
List search for other end of start/end pair
20282030
searchpos( {pattern} [, {flags} [, {stopline} [, {timeout}]]])
20292031
List search for {pattern}
2032+
sendexpr( {handle}, {expr} [, {callback}])
2033+
any send {expr} over JSON channel {handle}
2034+
sendraw( {handle}, {string} [, {callback}])
2035+
any send {string} over raw channel {handle}
20302036
server2client( {clientid}, {string})
20312037
Number send reply string
20322038
serverlist() String get a list of available servers
@@ -2660,6 +2666,18 @@ confirm({msg} [, {choices} [, {default} [, {type}]]])
26602666
don't fit, a vertical layout is used anyway. For some systems
26612667
the horizontal layout is always used.
26622668

2669+
connect({address}, {mode} [, {callback}]) *connect()*
2670+
Open a channel to {address}. See |channel|.
2671+
2672+
{address} has the form "hostname:port", e.g.,
2673+
"localhost:8765".
2674+
2675+
{mode} is either "json" or "raw". See |channel-mode| for the
2676+
meaning.
2677+
2678+
{callback} is a function that handles received messages on the
2679+
channel. See |channel-callback|.
2680+
26632681
*copy()*
26642682
copy({expr}) Make a copy of {expr}. For Numbers and Strings this isn't
26652683
different from using {expr} directly.
@@ -3861,7 +3879,9 @@ glob2regpat({expr}) *glob2regpat()*
38613879
if filename =~ glob2regpat('Make*.mak')
38623880
< This is equivalent to: >
38633881
if filename =~ '^Make.*\.mak$'
3864-
<
3882+
< When {expr} is an empty string the result is "^$", match an
3883+
empty string.
3884+
38653885
*globpath()*
38663886
globpath({path}, {expr} [, {nosuf} [, {list} [, {allinks}]]])
38673887
Perform glob() on all directories in {path} and concatenate
@@ -5593,6 +5613,23 @@ searchpos({pattern} [, {flags} [, {stopline} [, {timeout}]]]) *searchpos()*
55935613
< In this example "submatch" is 2 when a lowercase letter is
55945614
found |/\l|, 3 when an uppercase letter is found |/\u|.
55955615

5616+
sendexpr({handle}, {expr} [, {callback}]) *sendexpr()*
5617+
Send {expr} over JSON channel {handle}. See |channel-use|.
5618+
5619+
When {callback} is given returns immediately. Without
5620+
{callback} waits for a JSON response and returns the decoded
5621+
expression. When there is an error or timeout returns an
5622+
empty string.
5623+
5624+
When {callback} is zero no response is expected.
5625+
Otherwise {callback} must be a Funcref or the name of a
5626+
function. It is called when the response is received. See
5627+
|channel-callback|.
5628+
5629+
sendraw({handle}, {string} [, {callback}]) *sendraw()*
5630+
Send {string} over raw channel {handle}. See |channel-raw|.
5631+
Works like |sendexpr()|, but does not decode the response.
5632+
55965633
server2client( {clientid}, {string}) *server2client()*
55975634
Send a reply string to {clientid}. The most recent {clientid}
55985635
that sent a string can be retrieved with expand("<client>").

runtime/tools/demoserver.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/python
2+
# Server that will accept connections from a Vim channel.
3+
# Run this server and then in Vim you can open the channel:
4+
# :let handle = connect('localhost:8765', 'json')
5+
#
6+
# Then Vim can send requests to the server:
7+
# :let response = sendexpr(handle, 'hello!')
8+
#
9+
# And you can control Vim by typing a JSON message here, e.g.:
10+
# ["ex","echo 'hi there'"]
11+
#
12+
# See ":help channel-demo" in Vim.
13+
14+
import SocketServer
15+
import json
16+
import socket
17+
import sys
18+
import threading
19+
20+
thesocket = None
21+
22+
class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler):
23+
24+
def handle(self):
25+
print "=== socket opened ==="
26+
global thesocket
27+
thesocket = self.request
28+
while True:
29+
try:
30+
data = self.request.recv(4096)
31+
except socket.error:
32+
print "=== socket error ==="
33+
break
34+
except IOError:
35+
print "=== socket closed ==="
36+
break
37+
if data == '':
38+
print "=== socket closed ==="
39+
break
40+
print "received: {}".format(data)
41+
try:
42+
decoded = json.loads(data)
43+
except ValueError:
44+
print "json decoding failed"
45+
decoded = [0, '']
46+
47+
if decoded[1] == 'hello!':
48+
response = "got it"
49+
else:
50+
response = "what?"
51+
encoded = json.dumps([decoded[0], response])
52+
print "sending {}".format(encoded)
53+
self.request.sendall(encoded)
54+
thesocket = None
55+
56+
class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
57+
pass
58+
59+
if __name__ == "__main__":
60+
HOST, PORT = "localhost", 8765
61+
62+
server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
63+
ip, port = server.server_address
64+
65+
# Start a thread with the server -- that thread will then start one
66+
# more thread for each request
67+
server_thread = threading.Thread(target=server.serve_forever)
68+
69+
# Exit the server thread when the main thread terminates
70+
server_thread.daemon = True
71+
server_thread.start()
72+
print "Server loop running in thread: ", server_thread.name
73+
74+
print "Listening on port {}".format(PORT)
75+
while True:
76+
typed = sys.stdin.readline()
77+
if "quit" in typed:
78+
print "Goodbye!"
79+
break
80+
if thesocket is None:
81+
print "No socket yet"
82+
else:
83+
print "sending {}".format(typed)
84+
thesocket.sendall(typed)
85+
86+
server.shutdown()
87+
server.server_close()

0 commit comments

Comments
 (0)