Skip to content
This repository
Newer
Older
100644 465 lines (373 sloc) 10.592 kb
683f63cd »
2012-09-13 initial commit
1 import sublime, sublime_plugin
2
b0501990 »
2012-09-17 better reloading of lib.util
3 import lib.util
226dc8e2 »
2012-09-14 return errors from executed commands, reload util on update
4 import sys
5 # reload lib.util on update/reload of primary module
6 # so improvements will be loaded without a sublime restart
b0501990 »
2012-09-17 better reloading of lib.util
7 sys.modules['lib.util'] = reload(lib.util)
885df6cc »
2012-10-04 remove attempt to run xiki indirectly through ruby
8 from lib.util import communicate, popen, create_environment
226dc8e2 »
2012-09-14 return errors from executed commands, reload util on update
9
2dde16b6 »
2012-10-09 terminate subprocesses on closing their parent view (fix #13)
10 from collections import defaultdict
8b7aa09a »
2012-09-14 implemented paths
11 import os
2dde16b6 »
2012-10-09 terminate subprocesses on closing their parent view (fix #13)
12 import platform
13 import Queue
683f63cd »
2012-09-13 initial commit
14 import re
959137c2 »
2012-09-14 basic shell command support, various improvements
15 import shlex
3a95d35c »
2012-09-17 several updates
16 import subprocess
17 import thread
18 import time
c6f35c55 »
2012-09-18 compress extremely long output from commands
19 import traceback
3a95d35c »
2012-09-17 several updates
20
a1255a2d »
2012-09-14 indentation is now two spaces, various fixes
21 INDENTATION = ' '
c68fcd44 »
2012-09-20 add basic support for backspaces in text output
22 backspace_re = re.compile('.\b')
a1255a2d »
2012-09-14 indentation is now two spaces, various fixes
23
24 class BoundaryError(Exception): pass
25
3a95d35c »
2012-09-17 several updates
26 if not 'already' in globals():
27 already = True
2dde16b6 »
2012-10-09 terminate subprocesses on closing their parent view (fix #13)
28 commands = defaultdict(dict)
3a95d35c »
2012-09-17 several updates
29
30 def spawn(view, edit, indent, cmd, sel):
2dde16b6 »
2012-10-09 terminate subprocesses on closing their parent view (fix #13)
31 local_commands = commands[view.id()]
c6f35c55 »
2012-09-18 compress extremely long output from commands
32 q = Queue.Queue()
33 def fold(region):
34 regions = view.get_regions(region)
35 for region in regions:
36 lines = view.split_by_newlines(region)
37 if len(lines) > 24:
38
39 lines = lines[1:-24]
61fcd3db »
2012-09-20 handle a couple of common errors better
40 try:
41 area = lines.pop()
42 except IndexError:
43 return
44
c6f35c55 »
2012-09-18 compress extremely long output from commands
45 for sub in lines:
46 area = area.cover(sub)
47
48 view.unfold(area)
49 view.fold(area)
50
7e0b0602 »
2012-09-20 greatly improve output performance, other fixes
51 def merge(region):
c6f35c55 »
2012-09-18 compress extremely long output from commands
52 if q.empty(): return
29d915f0 »
2012-09-18 handle merge overlap
53 regions = view.get_regions(region)
54 if not regions: return
09c21f29 »
2012-09-18 include support for stderr in long-running commands
55
29d915f0 »
2012-09-18 handle merge overlap
56 pos = view.line(regions[0].end() - 1)
7e0b0602 »
2012-09-20 greatly improve output performance, other fixes
57
58 restore_sel = []
59 for sel in view.sel():
60 if pos.end() in (sel.a, sel.b):
61 restore_sel.append(sel)
62 view.sel().subtract(sel)
63
3a95d35c »
2012-09-17 several updates
64 edit = view.begin_edit()
c6f35c55 »
2012-09-18 compress extremely long output from commands
65 try:
66 start = time.time()
67 lines = []
7e0b0602 »
2012-09-20 greatly improve output performance, other fixes
68 while time.time() - start < 0.05 and len(lines) < 200:
c6f35c55 »
2012-09-18 compress extremely long output from commands
69 try:
70 lines.append(q.get(False))
71 q.task_done()
72 except Queue.Empty:
73 break
74
75 if not lines: return
76 insert(view, edit, pos, '\n'.join(lines), indent + INDENTATION)
77
09c21f29 »
2012-09-18 include support for stderr in long-running commands
78 fold(region)
c6f35c55 »
2012-09-18 compress extremely long output from commands
79 except:
80 print traceback.format_exc()
81 finally:
7e0b0602 »
2012-09-20 greatly improve output performance, other fixes
82 for sel in restore_sel:
83 view.sel().add(sel)
84
c6f35c55 »
2012-09-18 compress extremely long output from commands
85 view.end_edit(edit)
3a95d35c »
2012-09-17 several updates
86
5ca1df03 »
2012-09-18 improve stderr handling
87 def poll(p, region, fd):
09c21f29 »
2012-09-18 include support for stderr in long-running commands
88 while p.poll() is None:
7e0b0602 »
2012-09-20 greatly improve output performance, other fixes
89 line = fd.readline().decode('utf-8')
c68fcd44 »
2012-09-20 add basic support for backspaces in text output
90 line = backspace_re.sub('', line)
5ca1df03 »
2012-09-18 improve stderr handling
91 if line:
92 q.put(line.rstrip('\r\n'))
09c21f29 »
2012-09-18 include support for stderr in long-running commands
93
94 # if the process wasn't terminated
95 if p.returncode >= 0:
5ca1df03 »
2012-09-18 improve stderr handling
96 out = fd.read()
97 if out:
98 q.put(out.rstrip('\r\n'))
7e0b0602 »
2012-09-20 greatly improve output performance, other fixes
99 sublime.set_timeout(make_callback(merge, region), 100)
100
101 def out(p, region):
102 last = 0
103 while p.poll() is None:
104 since = time.time() - last
105 if since > 0.05 or since > 0.01 and q.qsize() < 10:
106 last = time.time()
107 sublime.set_timeout(make_callback(merge, region), 10)
108 else:
109 time.sleep(max(0.1 - since, 0.1))
110
88459c20 »
2012-09-20 cancel output printing if process is killed
111 if p.returncode not in (-9, -15):
2dde16b6 »
2012-10-09 terminate subprocesses on closing their parent view (fix #13)
112 del local_commands[region]
88459c20 »
2012-09-20 cancel output printing if process is killed
113 while not q.empty():
114 sublime.set_timeout(make_callback(merge, region), 10)
115 time.sleep(0.05)
7e0b0602 »
2012-09-20 greatly improve output performance, other fixes
116
117 sublime.set_timeout(make_callback(view.erase_regions, region), 150)
09c21f29 »
2012-09-18 include support for stderr in long-running commands
118
5ca1df03 »
2012-09-18 improve stderr handling
119 def stderr(p, region):
120 poll(p, region, p.stderr)
09c21f29 »
2012-09-18 include support for stderr in long-running commands
121
5ca1df03 »
2012-09-18 improve stderr handling
122 def stdout(p, region):
123 poll(p, region, p.stdout)
65f891ee »
2012-09-18 improve long-running command output
124
3a95d35c »
2012-09-17 several updates
125 p = popen(cmd, return_error=True)
126 if isinstance(p, subprocess.Popen):
127 region = 'xiki sub %i' % p.pid
128 line = view.full_line(sel.b)
129 spread = sublime.Region(line.a, line.b)
2dde16b6 »
2012-10-09 terminate subprocesses on closing their parent view (fix #13)
130 local_commands[region] = p
3a95d35c »
2012-09-17 several updates
131 view.add_regions(region, [spread], 'keyword', '', sublime.DRAW_OUTLINED)
132
09c21f29 »
2012-09-18 include support for stderr in long-running commands
133 thread.start_new_thread(stdout, (p, region))
134 thread.start_new_thread(stderr, (p, region))
7e0b0602 »
2012-09-20 greatly improve output performance, other fixes
135 thread.start_new_thread(out, (p, region))
3a95d35c »
2012-09-17 several updates
136 else:
137 insert(view, edit, sel, 'Error: ' + p, indent + INDENTATION)
138
683f63cd »
2012-09-13 initial commit
139 def xiki(view):
972e4d4f »
2012-09-21 fix #8, cleanup
140 if is_xiki_buffer(view):
3a95d35c »
2012-09-17 several updates
141 for sel in view.sel():
142 output = None
143 cmd = None
144 persist = False
145 oldcwd = None
382b3d4d »
2012-09-13 improved sign handling, fixed memoization, possibly fixed path bug
146
3a95d35c »
2012-09-17 several updates
147 view.sel().subtract(sel)
63fd5ae6 »
2012-09-13 implement sign flip / collapsing
148 edit = view.begin_edit()
3a95d35c »
2012-09-17 several updates
149
150 row, _ = view.rowcol(sel.b)
151 indent, sign, path, tag, tree = find_tree(view, row)
152
153 pos = view.line(sel.b).b
154 if get_line(view, row+1).startswith(indent + INDENTATION):
155 if sign == '-':
156 replace_line(view, edit, pos, indent + '+ ' + tag)
157
c6f35c55 »
2012-09-18 compress extremely long output from commands
158 do_clean = True
3a95d35c »
2012-09-17 several updates
159 check = sublime.Region(sel.b, sel.b)
2dde16b6 »
2012-10-09 terminate subprocesses on closing their parent view (fix #13)
160 for name, process in commands[view.id()].items():
3a95d35c »
2012-09-17 several updates
161 regions = view.get_regions(name)
162 for region in regions:
163 if region.contains(check):
2dde16b6 »
2012-10-09 terminate subprocesses on closing their parent view (fix #13)
164 try:
165 process.terminate()
166 except OSError:
167 pass
168
c6f35c55 »
2012-09-18 compress extremely long output from commands
169 do_clean = False
3a95d35c »
2012-09-17 several updates
170
c6f35c55 »
2012-09-18 compress extremely long output from commands
171 if do_clean:
172 cleanup(view, edit, pos, indent + INDENTATION)
3a95d35c »
2012-09-17 several updates
173 # select(view, pos)
3f2a14a9 »
2012-09-20 added $$ verb for invoking a command via your shell
174 elif sign == '$' or sign == '$$':
3a95d35c »
2012-09-17 several updates
175 if path:
2d5e6425 »
2012-09-18 better highlighting, filename escapes
176 p = dirname(path, tree, tag)
177
3a95d35c »
2012-09-17 several updates
178 oldcwd = os.getcwd()
2d5e6425 »
2012-09-18 better highlighting, filename escapes
179 os.chdir(p)
3a95d35c »
2012-09-17 several updates
180
3f2a14a9 »
2012-09-20 added $$ verb for invoking a command via your shell
181 tag = tag.encode('ascii', 'replace')
182
183 env = create_environment()
184 if sign == '$$' and 'SHELL' in env:
185 shell = os.path.basename(env['SHELL'])
186 cmd = [shell, '-c', tag]
187
188 if not cmd:
189 try:
190 cmd = shlex.split(tag, True)
191 except ValueError, err:
192 output = 'Error: ' + str(err)
61fcd3db »
2012-09-20 handle a couple of common errors better
193
3a95d35c »
2012-09-17 several updates
194 persist = True
195 elif path:
196 # directory listing or file open
197 target = os.path.join(path, tree)
2d5e6425 »
2012-09-18 better highlighting, filename escapes
198 d, f = os.path.split(target)
199 f = unslash(f)
200 target = os.path.join(d, f)
201
3a95d35c »
2012-09-17 several updates
202 if os.path.isfile(target):
709fd22c »
2012-10-09 add workaround for issue #15
203 if platform.system() == 'Windows':
204 target = os.path.abspath(target)
205
3a95d35c »
2012-09-17 several updates
206 sublime.active_window().open_file(target)
207 elif os.path.isdir(target):
208 dirs = ''
209 files = ''
210 listing = []
211 try:
212 listing = os.listdir(target)
213 except OSError, err:
214 dirs = '- ' + err.strerror + '\n'
215
216 for entry in listing:
c5e0267c »
2012-09-15 allow transversal of / and show errors in transversal
217 absolute = os.path.join(target, entry)
218 if os.path.isdir(absolute):
219 dirs += '+ %s/\n' % entry
220 else:
2d5e6425 »
2012-09-18 better highlighting, filename escapes
221 entry = slash(entry, '\\+$-')
c5e0267c »
2012-09-15 allow transversal of / and show errors in transversal
222 files += '%s\n' % entry
8b7aa09a »
2012-09-14 implemented paths
223
3a95d35c »
2012-09-17 several updates
224 output = (dirs + files) or '\n'
225 elif sign == '-':
226 # dunno here
2d5e6425 »
2012-09-18 better highlighting, filename escapes
227 pass
3a95d35c »
2012-09-17 several updates
228 elif tree:
885df6cc »
2012-10-04 remove attempt to run xiki indirectly through ruby
229 cmd = ['xiki']
3a95d35c »
2012-09-17 several updates
230 cmd += tree.split(' ')
231
232 if cmd:
233 if persist:
234 insert(view, edit, sel, '', indent + INDENTATION)
235 spawn(view, edit, indent, cmd, sel)
236 else:
237 output = communicate(cmd, None, 3, return_error=True)
1cfc23cf »
2012-09-13 fallback to launching xiki directly if ruby is not in path
238
3a95d35c »
2012-09-17 several updates
239 if oldcwd:
240 os.chdir(oldcwd)
959137c2 »
2012-09-14 basic shell command support, various improvements
241
3a95d35c »
2012-09-17 several updates
242 if output:
243 if sign == '+':
244 replace_line(view, edit, pos, indent + '- ' + tag)
8b7aa09a »
2012-09-14 implemented paths
245
3a95d35c »
2012-09-17 several updates
246 insert(view, edit, sel, output, indent + INDENTATION)
8b7aa09a »
2012-09-14 implemented paths
247
3a95d35c »
2012-09-17 several updates
248 view.sel().add(sel)
249 view.end_edit(edit)
683f63cd »
2012-09-13 initial commit
250
3a95d35c »
2012-09-17 several updates
251 def find_tree(view, row):
3f2a14a9 »
2012-09-20 added $$ verb for invoking a command via your shell
252 regex = re.compile(r'^(\s*)(\$\$|[-+$]\s*)?(.*)$')
959137c2 »
2012-09-14 basic shell command support, various improvements
253
3a95d35c »
2012-09-17 several updates
254 line = get_line(view, row)
959137c2 »
2012-09-14 basic shell command support, various improvements
255 match = regex.match(line)
8b7aa09a »
2012-09-14 implemented paths
256
683f63cd »
2012-09-13 initial commit
257 line_indent = last_indent = match.group(1)
63fd5ae6 »
2012-09-13 implement sign flip / collapsing
258 sign = (match.group(2) or '').strip()
683f63cd »
2012-09-13 initial commit
259 tag = match.group(3)
260 tree = [tag]
8b7aa09a »
2012-09-14 implemented paths
261 if tag.startswith('/'):
262 sign = '/'
683f63cd »
2012-09-13 initial commit
263
264 offset = -1
265 while last_indent != '':
a1255a2d »
2012-09-14 indentation is now two spaces, various fixes
266 try:
3a95d35c »
2012-09-17 several updates
267 line = get_line(view, row+offset)
a1255a2d »
2012-09-14 indentation is now two spaces, various fixes
268 except BoundaryError:
269 break
270
683f63cd »
2012-09-13 initial commit
271 offset -= 1
272
959137c2 »
2012-09-14 basic shell command support, various improvements
273 match = regex.match(line)
683f63cd »
2012-09-13 initial commit
274 if match:
275 indent = match.group(1)
8b7aa09a »
2012-09-14 implemented paths
276 part = match.group(3)
683f63cd »
2012-09-13 initial commit
277
8b7aa09a »
2012-09-14 implemented paths
278 if len(indent) < len(last_indent) and part:
683f63cd »
2012-09-13 initial commit
279 last_indent = indent
c5e0267c »
2012-09-15 allow transversal of / and show errors in transversal
280 tree.insert(0, part)
683f63cd »
2012-09-13 initial commit
281
282 new_tree = []
8b7aa09a »
2012-09-14 implemented paths
283 path = None
284 for part in reversed(tree):
285 if part.startswith('@'):
286 new_tree.insert(0, part.strip('@'))
287 elif part.startswith('/'):
288 path = part
08f0f7d2 »
2012-09-17 enable path expansion of ~ to home directory
289 elif part.startswith('~'):
290 path = os.path.expanduser(part)
8b7aa09a »
2012-09-14 implemented paths
291 else:
292 new_tree.insert(0, part)
08f0f7d2 »
2012-09-17 enable path expansion of ~ to home directory
293 continue
294
295 break
683f63cd »
2012-09-13 initial commit
296
c5e0267c »
2012-09-15 allow transversal of / and show errors in transversal
297 return line_indent, sign, path, tag, '/'.join(new_tree).replace('//', '/')
683f63cd »
2012-09-13 initial commit
298
299 # helpers
300
2d5e6425 »
2012-09-18 better highlighting, filename escapes
301 def slash(s, chars):
302 if re.match(r'^[%s]' % re.escape(chars), s):
303 s = '\\' + s
304
305 return s
306
307 def unslash(s):
308 out = ''
309 escaped = False
310 for c in s:
311 if escaped:
312 escaped = False
313 out += c
314 elif c == '\\':
315 escaped = True
316 else:
317 out += c
318
319 return out
320
3a95d35c »
2012-09-17 several updates
321 def replace_line(view, edit, point, text):
63fd5ae6 »
2012-09-13 implement sign flip / collapsing
322 text = text.rstrip()
323 line = view.full_line(point)
324
325 view.insert(edit, line.b, text + '\n')
326 view.erase(edit, line)
327
683f63cd »
2012-09-13 initial commit
328 def cleanup(view, edit, pos, indent):
329 line, _ = view.rowcol(pos)
330
37fdfa48 »
2012-09-20 greatly improve block cleanup performance
331 point = view.text_point(line + 1, 0)
332 text = view.substr(sublime.Region(point, view.size()))
333 count = 0
334 for l in text.split('\n'):
335 if l.startswith(indent):
336 count += 1
683f63cd »
2012-09-13 initial commit
337 else:
338 break
339
37fdfa48 »
2012-09-20 greatly improve block cleanup performance
340 start = view.text_point(line + 1, 0)
341 end = view.text_point(line + count, 0)
342 region = sublime.Region(
343 view.full_line(start).begin(),
344 view.full_line(end).end()
345 )
683f63cd »
2012-09-13 initial commit
346
37fdfa48 »
2012-09-20 greatly improve block cleanup performance
347 view.erase(edit, region)
348
349 def insert(view, edit, sel, text, indent='', cleanup=True):
350 line_end = view.line(sel.b).b
683f63cd »
2012-09-13 initial commit
351
352 for line in reversed(text.split('\n')):
c6f35c55 »
2012-09-18 compress extremely long output from commands
353 line = '\n' + indent + line
37fdfa48 »
2012-09-20 greatly improve block cleanup performance
354 view.insert(edit, line_end, line)
683f63cd »
2012-09-13 initial commit
355
3a95d35c »
2012-09-17 several updates
356 def get_line(view, row=0):
357 point = view.text_point(row, 0)
358 if row < 0:
a1255a2d »
2012-09-14 indentation is now two spaces, various fixes
359 raise BoundaryError
683f63cd »
2012-09-13 initial commit
360
361 line = view.line(point)
362 return view.substr(line).strip('\r\n')
363
3a95d35c »
2012-09-17 several updates
364 def dirname(path, tree, tag):
365 path_re = r'^(.+)/%s$' % re.escape(tag)
366 match = re.match(path_re, tree)
367 if match:
368 return os.path.join(path, match.group(1))
369 else:
370 return path
63fd5ae6 »
2012-09-13 implement sign flip / collapsing
371
3a95d35c »
2012-09-17 several updates
372 def completions(base, partial, executable=False):
373 if os.path.isdir(base):
374 ret = []
375 partial = partial.lower()
376
377 for name in os.listdir(base):
378 path = os.path.join(base, name)
379 if name.lower().startswith(partial):
380 if not executable or os.access(path, os.X_OK):
381 ret.append(name)
382
383 return ret
384
385 def make_callback(func, *args, **kwargs):
386 def wrapper():
387 return func(*args, **kwargs)
388
389 return wrapper
390
8200fd19 »
2012-09-20 use xiki syntax mode instead of magic setting to signify xiki buffers
391 def is_xiki_buffer(view):
baef7a45 »
2012-10-09 fixed exception when you activate a view without a syntax setting
392 if view is None or not view.settings().has('syntax'):
972e4d4f »
2012-09-21 fix #8, cleanup
393 return False
394
8200fd19 »
2012-09-20 use xiki syntax mode instead of magic setting to signify xiki buffers
395 return view.settings().get('syntax').endswith('/Xiki.tmLanguage')
396
3a95d35c »
2012-09-17 several updates
397 # sublime event classes
398
24662179 »
2012-09-21 fix issue #9
399 class XikiListener(sublime_plugin.EventListener):
3a95d35c »
2012-09-17 several updates
400 def on_query_completions(self, view, prefix, locations):
8200fd19 »
2012-09-20 use xiki syntax mode instead of magic setting to signify xiki buffers
401 if is_xiki_buffer(view):
3a95d35c »
2012-09-17 several updates
402 sel = view.sel()
403 if len(sel) == 1:
404 row, _ = view.rowcol(sel[0].b)
405 indent, sign, path, tag, tree = find_tree(view, row)
406
407 if sign == '$':
408 # command completion
409 pass
410 elif path:
411 # directory/file completion
412 target, partial = os.path.split(dirname(path, tree, tag))
413 return completions(target, partial)
683f63cd »
2012-09-13 initial commit
414
24662179 »
2012-09-21 fix issue #9
415 def set_xiki(self, view):
416 if is_xiki_buffer(view):
417 view.settings().set('xiki', True)
418 else:
419 view.settings().set('xiki', False)
420
a04bd712 »
2012-09-27 use on_activated instead of on_selection_modified to fix #14
421 def on_activated(self, view):
24662179 »
2012-09-21 fix issue #9
422 self.set_xiki(view)
423
2dde16b6 »
2012-10-09 terminate subprocesses on closing their parent view (fix #13)
424 def on_load(self, view):
425 self.set_xiki(view)
426
427 def on_close(self, view):
428 vid = view.id()
429 for process in commands[vid].values():
430 try:
431 process.terminate()
432 except OSError:
433 pass
434
435 del commands[vid]
436
683f63cd »
2012-09-13 initial commit
437 class Xiki(sublime_plugin.WindowCommand):
438 def run(self):
439 view = self.window.active_view()
440 xiki(view)
441
442 def is_enabled(self):
443 view = self.window.active_view()
8200fd19 »
2012-09-20 use xiki syntax mode instead of magic setting to signify xiki buffers
444 if is_xiki_buffer(view):
683f63cd »
2012-09-13 initial commit
445 return True
446
447 class NewXiki(sublime_plugin.WindowCommand):
448 def run(self):
449 view = self.window.new_file()
3a95d35c »
2012-09-17 several updates
450 settings = view.settings()
451
24662179 »
2012-09-21 fix issue #9
452 settings.set('xiki', True)
3a95d35c »
2012-09-17 several updates
453 settings.set('tab_size', 2)
454 settings.set('translate_tabs_to_spaces', True)
455 settings.set('syntax', 'Packages/SublimeXiki/Xiki.tmLanguage')
683f63cd »
2012-09-13 initial commit
456
457 class XikiClick(sublime_plugin.WindowCommand):
458 def run(self):
459 view = self.window.active_view()
8200fd19 »
2012-09-20 use xiki syntax mode instead of magic setting to signify xiki buffers
460 if is_xiki_buffer(view):
683f63cd »
2012-09-13 initial commit
461 xiki(view)
462 else:
463 # emulate the default double-click behavior
464 # if we're not in a xiki buffer
465 view.run_command('expand_selection', {'to': 'word'})
Something went wrong with that request. Please try again.