Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 466 lines (373 sloc) 10.592 kb
683f63c Ryan Hileman initial commit
authored
1 import sublime, sublime_plugin
2
b050199 Ryan Hileman better reloading of lib.util
authored
3 import lib.util
226dc8e Ryan Hileman return errors from executed commands, reload util on update
authored
4 import sys
5 # reload lib.util on update/reload of primary module
6 # so improvements will be loaded without a sublime restart
b050199 Ryan Hileman better reloading of lib.util
authored
7 sys.modules['lib.util'] = reload(lib.util)
885df6c Ryan Hileman remove attempt to run xiki indirectly through ruby
authored
8 from lib.util import communicate, popen, create_environment
226dc8e Ryan Hileman return errors from executed commands, reload util on update
authored
9
2dde16b Ryan Hileman terminate subprocesses on closing their parent view (fix #13)
authored
10 from collections import defaultdict
8b7aa09 Ryan Hileman implemented paths
authored
11 import os
2dde16b Ryan Hileman terminate subprocesses on closing their parent view (fix #13)
authored
12 import platform
13 import Queue
683f63c Ryan Hileman initial commit
authored
14 import re
959137c Ryan Hileman basic shell command support, various improvements
authored
15 import shlex
3a95d35 Ryan Hileman several updates
authored
16 import subprocess
17 import thread
18 import time
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
19 import traceback
3a95d35 Ryan Hileman several updates
authored
20
a1255a2 Ryan Hileman indentation is now two spaces, various fixes
authored
21 INDENTATION = ' '
c68fcd4 Ryan Hileman add basic support for backspaces in text output
authored
22 backspace_re = re.compile('.\b')
a1255a2 Ryan Hileman indentation is now two spaces, various fixes
authored
23
24 class BoundaryError(Exception): pass
25
3a95d35 Ryan Hileman several updates
authored
26 if not 'already' in globals():
27 already = True
2dde16b Ryan Hileman terminate subprocesses on closing their parent view (fix #13)
authored
28 commands = defaultdict(dict)
3a95d35 Ryan Hileman several updates
authored
29
30 def spawn(view, edit, indent, cmd, sel):
2dde16b Ryan Hileman terminate subprocesses on closing their parent view (fix #13)
authored
31 local_commands = commands[view.id()]
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
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]
61fcd3d Ryan Hileman handle a couple of common errors better
authored
40 try:
41 area = lines.pop()
42 except IndexError:
43 return
44
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
45 for sub in lines:
46 area = area.cover(sub)
47
48 view.unfold(area)
49 view.fold(area)
50
7e0b060 Ryan Hileman greatly improve output performance, other fixes
authored
51 def merge(region):
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
52 if q.empty(): return
29d915f Ryan Hileman handle merge overlap
authored
53 regions = view.get_regions(region)
54 if not regions: return
09c21f2 Ryan Hileman include support for stderr in long-running commands
authored
55
29d915f Ryan Hileman handle merge overlap
authored
56 pos = view.line(regions[0].end() - 1)
7e0b060 Ryan Hileman greatly improve output performance, other fixes
authored
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
3a95d35 Ryan Hileman several updates
authored
64 edit = view.begin_edit()
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
65 try:
66 start = time.time()
67 lines = []
7e0b060 Ryan Hileman greatly improve output performance, other fixes
authored
68 while time.time() - start < 0.05 and len(lines) < 200:
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
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
09c21f2 Ryan Hileman include support for stderr in long-running commands
authored
78 fold(region)
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
79 except:
80 print traceback.format_exc()
81 finally:
7e0b060 Ryan Hileman greatly improve output performance, other fixes
authored
82 for sel in restore_sel:
83 view.sel().add(sel)
84
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
85 view.end_edit(edit)
3a95d35 Ryan Hileman several updates
authored
86
5ca1df0 Ryan Hileman improve stderr handling
authored
87 def poll(p, region, fd):
09c21f2 Ryan Hileman include support for stderr in long-running commands
authored
88 while p.poll() is None:
7e0b060 Ryan Hileman greatly improve output performance, other fixes
authored
89 line = fd.readline().decode('utf-8')
c68fcd4 Ryan Hileman add basic support for backspaces in text output
authored
90 line = backspace_re.sub('', line)
5ca1df0 Ryan Hileman improve stderr handling
authored
91 if line:
92 q.put(line.rstrip('\r\n'))
09c21f2 Ryan Hileman include support for stderr in long-running commands
authored
93
94 # if the process wasn't terminated
95 if p.returncode >= 0:
5ca1df0 Ryan Hileman improve stderr handling
authored
96 out = fd.read()
97 if out:
98 q.put(out.rstrip('\r\n'))
7e0b060 Ryan Hileman greatly improve output performance, other fixes
authored
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
88459c2 Ryan Hileman cancel output printing if process is killed
authored
111 if p.returncode not in (-9, -15):
2dde16b Ryan Hileman terminate subprocesses on closing their parent view (fix #13)
authored
112 del local_commands[region]
88459c2 Ryan Hileman cancel output printing if process is killed
authored
113 while not q.empty():
114 sublime.set_timeout(make_callback(merge, region), 10)
115 time.sleep(0.05)
7e0b060 Ryan Hileman greatly improve output performance, other fixes
authored
116
117 sublime.set_timeout(make_callback(view.erase_regions, region), 150)
09c21f2 Ryan Hileman include support for stderr in long-running commands
authored
118
5ca1df0 Ryan Hileman improve stderr handling
authored
119 def stderr(p, region):
120 poll(p, region, p.stderr)
09c21f2 Ryan Hileman include support for stderr in long-running commands
authored
121
5ca1df0 Ryan Hileman improve stderr handling
authored
122 def stdout(p, region):
123 poll(p, region, p.stdout)
65f891e Ryan Hileman improve long-running command output
authored
124
3a95d35 Ryan Hileman several updates
authored
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)
2dde16b Ryan Hileman terminate subprocesses on closing their parent view (fix #13)
authored
130 local_commands[region] = p
3a95d35 Ryan Hileman several updates
authored
131 view.add_regions(region, [spread], 'keyword', '', sublime.DRAW_OUTLINED)
132
09c21f2 Ryan Hileman include support for stderr in long-running commands
authored
133 thread.start_new_thread(stdout, (p, region))
134 thread.start_new_thread(stderr, (p, region))
7e0b060 Ryan Hileman greatly improve output performance, other fixes
authored
135 thread.start_new_thread(out, (p, region))
3a95d35 Ryan Hileman several updates
authored
136 else:
137 insert(view, edit, sel, 'Error: ' + p, indent + INDENTATION)
138
683f63c Ryan Hileman initial commit
authored
139 def xiki(view):
972e4d4 Ryan Hileman fix #8, cleanup
authored
140 if is_xiki_buffer(view):
3a95d35 Ryan Hileman several updates
authored
141 for sel in view.sel():
142 output = None
143 cmd = None
144 persist = False
145 oldcwd = None
382b3d4 Ryan Hileman improved sign handling, fixed memoization, possibly fixed path bug
authored
146
3a95d35 Ryan Hileman several updates
authored
147 view.sel().subtract(sel)
63fd5ae Ryan Hileman implement sign flip / collapsing
authored
148 edit = view.begin_edit()
3a95d35 Ryan Hileman several updates
authored
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
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
158 do_clean = True
3a95d35 Ryan Hileman several updates
authored
159 check = sublime.Region(sel.b, sel.b)
2dde16b Ryan Hileman terminate subprocesses on closing their parent view (fix #13)
authored
160 for name, process in commands[view.id()].items():
3a95d35 Ryan Hileman several updates
authored
161 regions = view.get_regions(name)
162 for region in regions:
163 if region.contains(check):
2dde16b Ryan Hileman terminate subprocesses on closing their parent view (fix #13)
authored
164 try:
165 process.terminate()
166 except OSError:
167 pass
168
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
169 do_clean = False
3a95d35 Ryan Hileman several updates
authored
170
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
171 if do_clean:
172 cleanup(view, edit, pos, indent + INDENTATION)
3a95d35 Ryan Hileman several updates
authored
173 # select(view, pos)
3f2a14a Ryan Hileman added $$ verb for invoking a command via your shell
authored
174 elif sign == '$' or sign == '$$':
3a95d35 Ryan Hileman several updates
authored
175 if path:
2d5e642 Ryan Hileman better highlighting, filename escapes
authored
176 p = dirname(path, tree, tag)
177
3a95d35 Ryan Hileman several updates
authored
178 oldcwd = os.getcwd()
2d5e642 Ryan Hileman better highlighting, filename escapes
authored
179 os.chdir(p)
3a95d35 Ryan Hileman several updates
authored
180
3f2a14a Ryan Hileman added $$ verb for invoking a command via your shell
authored
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)
61fcd3d Ryan Hileman handle a couple of common errors better
authored
193
3a95d35 Ryan Hileman several updates
authored
194 persist = True
195 elif path:
196 # directory listing or file open
197 target = os.path.join(path, tree)
2d5e642 Ryan Hileman better highlighting, filename escapes
authored
198 d, f = os.path.split(target)
199 f = unslash(f)
200 target = os.path.join(d, f)
201
3a95d35 Ryan Hileman several updates
authored
202 if os.path.isfile(target):
709fd22 Ryan Hileman add workaround for issue #15
authored
203 if platform.system() == 'Windows':
204 target = os.path.abspath(target)
205
3a95d35 Ryan Hileman several updates
authored
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:
c5e0267 Ryan Hileman allow transversal of / and show errors in transversal
authored
217 absolute = os.path.join(target, entry)
218 if os.path.isdir(absolute):
219 dirs += '+ %s/\n' % entry
220 else:
2d5e642 Ryan Hileman better highlighting, filename escapes
authored
221 entry = slash(entry, '\\+$-')
c5e0267 Ryan Hileman allow transversal of / and show errors in transversal
authored
222 files += '%s\n' % entry
8b7aa09 Ryan Hileman implemented paths
authored
223
3a95d35 Ryan Hileman several updates
authored
224 output = (dirs + files) or '\n'
225 elif sign == '-':
226 # dunno here
2d5e642 Ryan Hileman better highlighting, filename escapes
authored
227 pass
3a95d35 Ryan Hileman several updates
authored
228 elif tree:
885df6c Ryan Hileman remove attempt to run xiki indirectly through ruby
authored
229 cmd = ['xiki']
3a95d35 Ryan Hileman several updates
authored
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)
1cfc23c Ryan Hileman fallback to launching xiki directly if ruby is not in path
authored
238
3a95d35 Ryan Hileman several updates
authored
239 if oldcwd:
240 os.chdir(oldcwd)
959137c Ryan Hileman basic shell command support, various improvements
authored
241
3a95d35 Ryan Hileman several updates
authored
242 if output:
243 if sign == '+':
244 replace_line(view, edit, pos, indent + '- ' + tag)
8b7aa09 Ryan Hileman implemented paths
authored
245
3a95d35 Ryan Hileman several updates
authored
246 insert(view, edit, sel, output, indent + INDENTATION)
8b7aa09 Ryan Hileman implemented paths
authored
247
3a95d35 Ryan Hileman several updates
authored
248 view.sel().add(sel)
249 view.end_edit(edit)
683f63c Ryan Hileman initial commit
authored
250
3a95d35 Ryan Hileman several updates
authored
251 def find_tree(view, row):
3f2a14a Ryan Hileman added $$ verb for invoking a command via your shell
authored
252 regex = re.compile(r'^(\s*)(\$\$|[-+$]\s*)?(.*)$')
959137c Ryan Hileman basic shell command support, various improvements
authored
253
3a95d35 Ryan Hileman several updates
authored
254 line = get_line(view, row)
959137c Ryan Hileman basic shell command support, various improvements
authored
255 match = regex.match(line)
8b7aa09 Ryan Hileman implemented paths
authored
256
683f63c Ryan Hileman initial commit
authored
257 line_indent = last_indent = match.group(1)
63fd5ae Ryan Hileman implement sign flip / collapsing
authored
258 sign = (match.group(2) or '').strip()
683f63c Ryan Hileman initial commit
authored
259 tag = match.group(3)
260 tree = [tag]
8b7aa09 Ryan Hileman implemented paths
authored
261 if tag.startswith('/'):
262 sign = '/'
683f63c Ryan Hileman initial commit
authored
263
264 offset = -1
265 while last_indent != '':
a1255a2 Ryan Hileman indentation is now two spaces, various fixes
authored
266 try:
3a95d35 Ryan Hileman several updates
authored
267 line = get_line(view, row+offset)
a1255a2 Ryan Hileman indentation is now two spaces, various fixes
authored
268 except BoundaryError:
269 break
270
683f63c Ryan Hileman initial commit
authored
271 offset -= 1
272
959137c Ryan Hileman basic shell command support, various improvements
authored
273 match = regex.match(line)
683f63c Ryan Hileman initial commit
authored
274 if match:
275 indent = match.group(1)
8b7aa09 Ryan Hileman implemented paths
authored
276 part = match.group(3)
683f63c Ryan Hileman initial commit
authored
277
8b7aa09 Ryan Hileman implemented paths
authored
278 if len(indent) < len(last_indent) and part:
683f63c Ryan Hileman initial commit
authored
279 last_indent = indent
c5e0267 Ryan Hileman allow transversal of / and show errors in transversal
authored
280 tree.insert(0, part)
683f63c Ryan Hileman initial commit
authored
281
282 new_tree = []
8b7aa09 Ryan Hileman implemented paths
authored
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
08f0f7d Ryan Hileman enable path expansion of ~ to home directory
authored
289 elif part.startswith('~'):
290 path = os.path.expanduser(part)
8b7aa09 Ryan Hileman implemented paths
authored
291 else:
292 new_tree.insert(0, part)
08f0f7d Ryan Hileman enable path expansion of ~ to home directory
authored
293 continue
294
295 break
683f63c Ryan Hileman initial commit
authored
296
c5e0267 Ryan Hileman allow transversal of / and show errors in transversal
authored
297 return line_indent, sign, path, tag, '/'.join(new_tree).replace('//', '/')
683f63c Ryan Hileman initial commit
authored
298
299 # helpers
300
2d5e642 Ryan Hileman better highlighting, filename escapes
authored
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
3a95d35 Ryan Hileman several updates
authored
321 def replace_line(view, edit, point, text):
63fd5ae Ryan Hileman implement sign flip / collapsing
authored
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
683f63c Ryan Hileman initial commit
authored
328 def cleanup(view, edit, pos, indent):
329 line, _ = view.rowcol(pos)
330
37fdfa4 Ryan Hileman greatly improve block cleanup performance
authored
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
683f63c Ryan Hileman initial commit
authored
337 else:
338 break
339
37fdfa4 Ryan Hileman greatly improve block cleanup performance
authored
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 )
683f63c Ryan Hileman initial commit
authored
346
37fdfa4 Ryan Hileman greatly improve block cleanup performance
authored
347 view.erase(edit, region)
348
349 def insert(view, edit, sel, text, indent='', cleanup=True):
350 line_end = view.line(sel.b).b
683f63c Ryan Hileman initial commit
authored
351
352 for line in reversed(text.split('\n')):
c6f35c5 Ryan Hileman compress extremely long output from commands
authored
353 line = '\n' + indent + line
37fdfa4 Ryan Hileman greatly improve block cleanup performance
authored
354 view.insert(edit, line_end, line)
683f63c Ryan Hileman initial commit
authored
355
3a95d35 Ryan Hileman several updates
authored
356 def get_line(view, row=0):
357 point = view.text_point(row, 0)
358 if row < 0:
a1255a2 Ryan Hileman indentation is now two spaces, various fixes
authored
359 raise BoundaryError
683f63c Ryan Hileman initial commit
authored
360
361 line = view.line(point)
362 return view.substr(line).strip('\r\n')
363
3a95d35 Ryan Hileman several updates
authored
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
63fd5ae Ryan Hileman implement sign flip / collapsing
authored
371
3a95d35 Ryan Hileman several updates
authored
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
8200fd1 Ryan Hileman use xiki syntax mode instead of magic setting to signify xiki buffers
authored
391 def is_xiki_buffer(view):
baef7a4 Ryan Hileman fixed exception when you activate a view without a syntax setting
authored
392 if view is None or not view.settings().has('syntax'):
972e4d4 Ryan Hileman fix #8, cleanup
authored
393 return False
394
8200fd1 Ryan Hileman use xiki syntax mode instead of magic setting to signify xiki buffers
authored
395 return view.settings().get('syntax').endswith('/Xiki.tmLanguage')
396
3a95d35 Ryan Hileman several updates
authored
397 # sublime event classes
398
2466217 Ryan Hileman fix issue #9
authored
399 class XikiListener(sublime_plugin.EventListener):
3a95d35 Ryan Hileman several updates
authored
400 def on_query_completions(self, view, prefix, locations):
8200fd1 Ryan Hileman use xiki syntax mode instead of magic setting to signify xiki buffers
authored
401 if is_xiki_buffer(view):
3a95d35 Ryan Hileman several updates
authored
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)
683f63c Ryan Hileman initial commit
authored
414
2466217 Ryan Hileman fix issue #9
authored
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
a04bd71 Ryan Hileman use on_activated instead of on_selection_modified to fix #14
authored
421 def on_activated(self, view):
2466217 Ryan Hileman fix issue #9
authored
422 self.set_xiki(view)
423
2dde16b Ryan Hileman terminate subprocesses on closing their parent view (fix #13)
authored
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
683f63c Ryan Hileman initial commit
authored
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()
8200fd1 Ryan Hileman use xiki syntax mode instead of magic setting to signify xiki buffers
authored
444 if is_xiki_buffer(view):
683f63c Ryan Hileman initial commit
authored
445 return True
446
447 class NewXiki(sublime_plugin.WindowCommand):
448 def run(self):
449 view = self.window.new_file()
3a95d35 Ryan Hileman several updates
authored
450 settings = view.settings()
451
2466217 Ryan Hileman fix issue #9
authored
452 settings.set('xiki', True)
3a95d35 Ryan Hileman several updates
authored
453 settings.set('tab_size', 2)
454 settings.set('translate_tabs_to_spaces', True)
455 settings.set('syntax', 'Packages/SublimeXiki/Xiki.tmLanguage')
683f63c Ryan Hileman initial commit
authored
456
457 class XikiClick(sublime_plugin.WindowCommand):
458 def run(self):
459 view = self.window.active_view()
8200fd1 Ryan Hileman use xiki syntax mode instead of magic setting to signify xiki buffers
authored
460 if is_xiki_buffer(view):
683f63c Ryan Hileman initial commit
authored
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.