1
# -*- coding: utf-8 -*-
2
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
3
# $Id: gdkvim.py 453 2005-07-24 15:24:10Z aafshar $
4
#Copyright (c) 2005 Ali Afshar aafshar@gmail.com
6
#Permission is hereby granted, free of charge, to any person obtaining a copy
7
#of this software and associated documentation files (the "Software"), to deal
8
#in the Software without restriction, including without limitation the rights
9
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
#copies of the Software, and to permit persons to whom the Software is
11
#furnished to do so, subject to the following conditions:
13
#The above copyright notice and this permission notice shall be included in
14
#all copies or substantial portions of the Software.
16
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
A library to control vim -g using its X protocol interface (with gdk).
30
=== General Communication ===
32
The Vim client/server protocol communicates by sending messages to and from an
33
X communication window. The details are explained in the Vim source.
34
Essentially, Vim understands two sorts of messages over this interface.
36
;asynchronous key sends : that are exactly equivalent to to the user of the
37
remote Vim typing commands.
39
;synchronous expression evaluations : these are Vim expressions that are
40
evaluated by the remote Vim, and an answer is replied with the result over the
43
Although the synchronous messages are called synchronous, the reply itself, in
44
programming terms is entirely asynchronous, in that there is no way of knowing
45
when a reply will be received, and one cannot block for it.
47
Thus, this library allows you to make both of these calls to remote Vims.
48
Synchronous expressions must provide a call back function that will be called
49
when the message is replied to.
51
=== The Server List ===
53
(It has been an utter nightmare.)
55
The primary problem is that GTK does not actually know accurately whether
56
a window with a given window ID has been destroyed. This is how Vim does
57
it (using the X libraries) after checking an attribute for registered Vim
58
sessions with the X root window. This way each Vim doesn't need to
59
unregister itself with the X root window on dying, it just assumes that
60
any other client attempting to connect to it will know that the window
61
has been destroyed. As mentioned, GTK totally fails to do what the X
62
library does, and ascertain whether the window is alive. It succeeds
63
sometimes, but not at others. The result is a GDK window that appears
64
alive, and ready to communicate with, but which causes an uncatchable and
65
fatal application error.
67
Step in other potential methods of getting an accurate list of servers.
68
Firstly, and most obviously, one can call the command 'vim --serverlist'
69
on a simple system pipe and read the list off. This is entirely reliable,
70
and effective, but the cost of forking a process and starting Vim each
71
time is not fun, and effectively blocks.
73
Another option is to force users to start Vim through Pida and keep an
74
account of the child processes. This would work very effectively, but it
75
restricts the user, and the entire system.
77
The final, and current solution is to start Vim itself on a
78
pseudoterminal as a hidden instance, and then communicate with that over
79
the Vim protocol. The reason this can be reliably done, is that since the
80
process is a child, it can be polled to check whether it is alive. This
81
is performed each time the serverlist is requested, and if the hidden
82
instance has been destroyed (eg by the user) a new one is spawned, thus
83
preventing an attempt to communicate with an already-destroyed GDK
86
The cost of this solution is that we spawn an extra Vim process. I
87
believe that the added solidity it brings to the entire system is easily
88
worth it, and it ensures that Pida can communicate with Vim it started
89
and Vim it didn't start.
100
import pida.base as base
102
class VimHidden(base.pidaobject):
104
An instance of Vim on a pseudoterminal which can be reliably polled.
106
This class is used to provide an instance of Vim which can be communicated
107
with using the Vim client/server protocol, in order to retrieve an accurate
108
and current server list, and also which can be polled accurately as to
109
whether it is alive before communicating with it.
111
This method is much cheaper in resources than running vim --serverlist each
112
time, and much more accurate than using the root window's VimRegistry
113
property, and also more accurate than using GDK methods for assessing
114
whether a window is alive.
121
Create a temporary and unique name for use as the servername, and
122
initialise the instance variables.
124
@param cb: An instance of the main application class.
125
@type cb: pida.main.Application.
127
# Prefacing with '__' means it will be ignored in the internal server
129
self.name = '__%s_PIDA_HIDDEN' % time.time()
130
# Checked to evaluate False on starting.
135
Start the Vim instance if it is not already running.
137
This command forks in a pseudoterminal, and starts Vim, if Vim is not
138
already running. The pid is stored for later use.
141
# Get the console vim executable path
142
command = self.prop_main_registry.commands.vim.value()
143
# Fork using pty.fork to prevent Vim taking the terminal
151
# Child, execute Vim with the correct servername argument
152
os.execvp(command, ['gvim', '-f', '--servername', self.name,
153
'--socketid', '%s' % xid])
155
# os.system('%s -v --servername %s' % (command, self.name))
157
# Parent, store the pid, and file descriptor for later.
160
self.do_action('accountfork', self.pid)
164
Check if the Vim instance is alive.
166
This method uses os.waitpid, with no blocking to determine whether the
167
process is still alive. If it is not, it sets the internal pid
168
attribute to None, so that it may be restarted.
171
@rtype alive: boolean
175
# call os.waitpid, returns 0 if the pid is alive
176
pid, sts = os.waitpid(self.pid, os.WNOHANG)
178
# might still be starting up
191
class VimWindow(base.pidaobject, gtk.Window):
193
A GTK window that can communicate with any number Vim instances.
195
This is an actual GTK window (which it must be to accurately detect
196
property events inside the GTK main loop) but has its GDK window correctly
197
set to receive such events. This is notably the "Vim" property which must
198
be present and set to a version string, in this case "6.0" is used.
204
The Window is instantiated, the properties are correctly set, the event
205
mask is modified, and the instance variables are initialized.
207
@param cb: An instance of the main Application class.
208
@type cb: pida.main.Application.
210
gtk.Window.__init__(self)
211
# Window needs to be realized to do anything useful with it. Realizing
212
# does not show the window to the user, so we can use it, but not have
213
# an ugly blank frame while it loads.
216
self.window.property_change("Vim", gdk.SELECTION_TYPE_STRING, 8,
217
gdk.PROP_MODE_REPLACE, "6.0")
218
# Set the correct event mask and connect the notify event
219
self.add_events(gtk.gdk.PROPERTY_CHANGE_MASK)
220
self.connect('property-notify-event', self.cb_notify)
221
# The serial number used for sending synchronous messages
223
# A dictionary of callbacks for synchronous messages. The key is the
224
# serial number, and the value is a callable that will be called with
225
# the result of the synchronous evaluation.
227
# A dictionary to store the working directories for each Vim so they
228
# only have to be fetched once.
229
self.server_cwds = {}
230
# A list of the last fetched server list, used todecide whther to feed
231
# the most recent server list to the client.
232
self.oldservers = None
233
# An instance of the root window, so it only has to be fetched once.
234
self.root_window = gdk.get_default_root_window()
235
# Instantiate and start the hidden communication window, used for
236
# fetching accurate and reliable server lists.
237
self.vim_hidden = VimHidden()
238
self.vim_hidden.start()
239
# Start the timer for fetching the server list at the specified number
240
# of milliseconds (500).
241
gobject.timeout_add(1000, self.fetch_serverlist)
243
def fetch_serverlist(self):
245
Fetch the serverlist, and if it has changed, feed it to the client.
247
The serverlist is requested asynchrnously, and passed the gotservers
248
function as a callback. The gotservers function is then called with the
249
server list, gets the appropriate working directory (if required) and
250
feeds the new server list to the client if it has changed.
252
def gotservers(serverlist):
254
Called back on receiving the serverlist.
256
Fetch working directories for new Vim instances, and feed the
257
server list to the client if it has changed.
259
for server in serverlist:
260
# Check if we already have the working directory.
261
if server not in self.server_cwds:
263
self.fetch_cwd(server)
264
# Check if the server list has changed
265
if serverlist != self.oldservers:
266
self.oldservers = serverlist
267
# A ew serverlist to feed to the client.
268
self.feed_serverlist(serverlist)
269
# Fetch the server list from the hidden vim instance with the
270
# gotservers function as a callback.
271
self.get_hidden_serverlist(gotservers)
272
# Return True so that the timer continues
275
def get_rootwindow_serverlist(self):
277
Get the X root window's version of the current Vim serverlist.
279
On starting with the client-server feature, GVim or Vim with the
280
--servername option registers its server name and X window id as part
281
of the "VimRegistry" parameter on the X root window.
283
This method extracts and parses that property, and returns the server
286
Note: Vim does not actually unregister itself with the root window on
287
dying, so the presence of a server in the root window list is no
288
gurantee that it is alive.
291
@rtype servers: dict of ("server", "window id") key, value
295
vimregistry = self.root_window.property_get("VimRegistry")
298
# Get the list of servers by splitting with '\0'
299
vimservers = vimregistry[-1].split('\0')
300
# Parse each individual server and add to the results list
301
for rawserver in vimservers:
302
# Sometimes blank servers exist in the list
304
# split the raw value for the name and id
305
name_id = rawserver.split()
306
# Set the value in the results dict, remembering to convert
307
# the window id to a long int.
308
servers[name_id[1]] = long(int(name_id[0], 16))
309
# return the list of resuts
312
def get_shell_serverlist(self):
314
Get the server list by starting console Vim on a Pipe.
316
This blocks, so we don't use it. It is one of the alternative methods
317
of retrieving an accurate serverlist. It is slow, and expensive.
319
vimcom = prop_main_registry.commands.vim_console.value()
320
p = os.popen('%s --serverlist' % vimcom)
323
return servers.splitlines()
325
def get_hidden_serverlist(self, callbackfunc):
327
Get the serverlist from the hidden Vim instance and call the callback
328
function with the results.
330
This method checks first whther the Vim instance is alive, and then
331
evaluates the serverlist() function remotely in it, with a local call
332
back function which parses the result and calls the user-provided
335
@param callbackfunc: The call back function to be called with the
337
@type callbackfunc: callable
339
def cb(serverstring):
341
Called back with the raw server list.
343
Parse the lines and call the call back function, ignoring any
344
instances starting with "__" which represent hidden instances. If
345
the hidden Vim instance is not alive, it is restarted.
347
servers = serverstring.splitlines()
348
# Call the callback function
349
callbackfunc([svr for svr in servers if not svr.startswith('__')])
350
# Check if the hidden Vim is alive.
351
if self.vim_hidden.is_alive():
352
# It is alive, get the serverlist.
353
self.send_expr(self.vim_hidden.name, 'serverlist()', cb)
355
# It is not alive, restart it.
356
self.vim_hidden.start()
358
def get_server_wid(self, servername):
360
Get the X Window id for a named Vim server.
362
This function returns the id from the root window server list, if it
363
exists, or None if it does not.
365
@param servername: The name of the server
366
@type servername: str
372
# get the window id from the root window
373
wid = self.get_rootwindow_serverlist()[servername]
375
# The server is not registered in the root window so return None
377
# Return wid if it is not none, or None
378
return wid and long(wid) or None
380
def get_server_window(self, wid):
382
Create and return a GDK window for a given window ID.
384
This method simply calls gdk.window_foreign_new, which should return
385
None if the window has been destroyed, but does not, in some cases.
387
@param wid: The window ID.
390
return gtk.gdk.window_foreign_new(wid)
392
def feed_serverlist(self, serverlist):
394
Feed the given list of servers to the client.
396
This is achieved by calling the clients serverlist event. In Pida, this
397
event is passed on to all the plugins.
399
@param serverlist: The list of servers.
400
@type serverlist: list
403
self.do_evt('serverlist', serverlist)
405
def fetch_cwd(self, servername):
407
Fetch the working directory for a named server and store the result.
411
Called back on receiving the working directory, store it for later
414
self.server_cwds[servername] = cwd
415
# Evaluate the expression with the gotcwd callback
416
self.send_expr(servername, "getcwd()", gotcwd)
418
def abspath(self, servername, filename):
420
Return the absolute path of a buffer name in the context of the named
423
# Only alter non-absolute paths
424
if not filename.startswith('/'):
426
# Try to find the current working directory
427
cwd = self.server_cwds[servername]
429
# The working directory is not set
430
# Use a sane default, and fetch it
431
cwd = os.path.expanduser('~')
432
self.fetch_cwd(servername)
433
filename = os.path.join(cwd, filename)
436
def generate_message(self, server, cork, message, sourceid):
440
# Increment the serial number used for synchronous messages
442
self.serial = self.serial + 1
443
# Pick an arbitrary number where we recycle.
444
if self.serial > 65530:
446
# return the generated string
447
return '\0%s\0-n %s\0-s %s\0-r %x %s\0' % (cork,
453
def parse_message(self, message):
455
Parse a received message and return the message atributes as a
459
for t in [s.split(' ') for s in message.split('\0')]:
461
if t[0].startswith('-'):
462
#attributes start with a '-', strip it and set the value
464
messageattrs[t[0][1:]] = t[1]
466
# Otherwise set the t attribute
467
messageattrs['t'] = t[0]
471
def send_message(self, servername, message, asexpr, callback):
472
wid = self.get_server_wid(servername)
474
cork = (asexpr and 'c') or 'k'
475
sw = self.get_server_window(wid)
476
if sw and sw.property_get("Vim"):
477
mp = self.generate_message(servername, cork, message,
479
sw.property_change("Comm", gdk.TARGET_STRING, 8,
480
gdk.PROP_MODE_APPEND, mp)
481
if asexpr and callback:
482
self.callbacks['%s' % (self.serial)] = callback
484
def send_expr(self, server, message, callback):
485
self.send_message(server, message, True, callback)
487
def send_keys(self, server, message):
488
self.send_message(server, message, False, False)
490
def send_esc(self, server):
491
self.send_keys(server, '<C-\><C-N>')
493
def send_ret(self, server):
494
self.send_keys(server, '<RETURN>')
496
def send_ex(self, server, message):
497
self.send_esc(server)
498
self.send_keys(server, ':%s' % message)
499
self.send_ret(server)
501
def get_option(self, server, option, callbackfunc):
502
self.send_expr(server, '&%s' % option, callbackfunc)
505
def foreground(self, server):
508
self.send_expr(server, 'foreground()', cb)
510
def change_buffer(self, server, nr):
511
self.send_ex(server, 'b!%s' % nr)
513
def close_buffer(self, server):
514
self.send_ex(server, 'confirm bw')
516
def change_cursor(self, server, x, y):
517
self.send_message(server, 'cursor(%s, %s)' % (y, x), True, False)
518
self.send_esc(server)
520
def save_session(self, name):
521
self.send_ex('mks %s' % name)
523
def escape_filename(self, name):
524
for s in ['\\', '?', '*', ' ', "'", '"', '[', ' ', '$']:
525
name = name.replace (s, '\\%s' % s)
528
def open_file(self, server, name):
529
self.send_ex(server, 'confirm e %s' % self.escape_filename(name))
531
def preview_file(self, server, fn):
532
self.send_ex(server, 'pc')
533
self.send_ex(server, 'set nopreviewwindow')
534
self.send_ex(server, 'pedit %s' % fn)
536
def get_bufferlist(self, server):
539
l = [i.split(':') for i in bl.strip(';').split(';')]
542
if not n[0].startswith('E'):
543
L.append([n[0], self.abspath(server, n[1])])
544
self.do_evt('bufferlist', L)
545
#self.get_cwd(server)
546
self.send_expr(server, 'Bufferlist()', cb)
548
def get_current_buffer(self, server):
551
bn[1] = self.abspath(server, bn[1])
552
self.do_evt('bufferchange', *bn)
553
#self.get_cwd(server)
554
self.send_expr(server, "bufnr('%').','.bufname('%')", cb)
556
def quit(self, server):
557
self.send_ex(server, 'q')
559
def cb_notify(self, *a):
561
if hasattr(ev, 'atom'):
562
if ev.atom == 'Comm':
563
message = self.window.property_get('Comm', pdelete=True)
565
self.cb_reply(message[-1])
568
def cb_reply(self, data):
569
mdict = self.parse_message(data)
570
if mdict['t'] == 'r':
571
if mdict['s'] in self.callbacks:
572
self.callbacks[mdict['s']](mdict['r'])
574
s = [t for t in data.split('\0') if t.startswith('-n')].pop()[3:]
575
self.cb_reply_async(s)
577
def cb_reply_async(self, data):
579
evt, d = data.split(',', 1)
580
self.do_evt(evt, *d.split(','))
582
self.do_log('bad async reply', data, 10)