diff --git a/PySnipEmu.py b/PySnipEmu.py index f385f37..ffd14ad 100644 --- a/PySnipEmu.py +++ b/PySnipEmu.py @@ -5,19 +5,81 @@ import vim import string import re +class Mirror(object): + def __init__(self, ts, idx, start): + self._ts = ts + self._start = (idx, start) + self._end = (idx,start) + self._delta_rows = 0 + self._delta_cols = 0 + + def delta_rows(): + doc = "The RW foo property." + def fget(self): + return self._delta_rows + def fset(self, value): + self._delta_rows = value + return locals() + delta_rows = property(**delta_rows()) + + def delta_cols(): + doc = "The RW foo property." + def fget(self): + return self._delta_cols + def fset(self, value): + self._delta_cols = value + return locals() + delta_cols = property(**delta_cols()) + + @property + def tabstop(self): + return self._ts + + @property + def number(self): + return self._no + + @property + def start(self): + "The RO foo property." + return self._start + + @property + def end(self): + "The RO foo property." + return self._end + + + def update_span(self): + start = self._start + lines = self._ts.current_text.splitlines() + if len(lines) == 1: + self._end = (start[0]+len(lines)-1,len(lines[-1])) + elif len(lines) > 1: + self._end = (start[0],start[1]+len(lines[0]) ) + class TabStop(object): def __init__(self, no, idx, span, default_text = ""): self._no = no self._default_text = default_text - self._span = span + self._start = span[0] self._lineidx = idx + self._ct = default_text + + def current_text(): + def fget(self): + return self._ct + def fset(self, value): + self._ct = value + return locals() + current_text = property(**current_text()) def line_idx(self): return self._lineidx line_idx = property(line_idx) def span(self): - return self._span + return (self._start,self._start+len(self._ct)) span = property(span) def default_text(self): @@ -29,10 +91,11 @@ class TabStop(object): number = property(number) class SnippetInstance(object): - def __init__(self,start,end, ts): + def __init__(self,start,end, ts, mirrors): self._start = start self._end = end self._ts = ts + self._mirrors = mirrors self._cts = 1 @@ -59,7 +122,6 @@ class SnippetInstance(object): vim.current.window.cursor = newline, newcol # Select the word - # Depending on the current mode and position, we # might need to move escape out of the mode and this # will move our cursor one left @@ -72,8 +134,72 @@ class SnippetInstance(object): vim.command(r'call feedkeys("\%sv%il\")' % (move_one_right, endcol-newcol-1)) + def _update_mirrors(self,for_ts): + for m in self._mirrors: + if m.tabstop == for_ts: + mirror_line = m.start[0] + line = vim.current.buffer[mirror_line] + line = line[:m.start[1]+m.delta_cols] + for_ts.current_text # + line[m.end[1]+m.delta_cols:] + vim.current.buffer[mirror_line] = line + + # m.update_span() + + # Now update all mirrors and tabstops, that they have moved + # lines = for_ts.current_text.splitlines() + # if len(lines): + # for om in self._mirrors: + # om.update_span() + # + # # if om.start[0] > m.start[0]: + # # om.delta_rows += len(lines)-1 + # if om.start[0] == m.start[0]: + # if om.start[1] >= m.start[1]: + # om.delta_cols += len(lines[-1]) + + def _move_to_on_line(self,amount): + lineno,col = vim.current.window.cursor + lineno -= 1 + for m in self._mirrors: + if m.start[0] != lineno: + continue + if m.start[1] > col: + m.delta_cols += amount + + + def backspace(self,count): + if self._cts not in self._ts: + if 0 in self._ts: + self._cts = 0 + else: + self._cts = 1 + + cts = self._ts[self._cts] + cts.current_text = cts.current_text[:-count] + + # self._move_to_on_line(-count) + + self._update_mirrors(cts) + + def chars_entered(self, chars): + if self._cts not in self._ts: + if 0 in self._ts: + self._cts = 0 + else: + self._cts = 1 + + cts = self._ts[self._cts] + cts.current_text += chars + + self._move_to_on_line(len(chars)) + + self._update_mirrors(cts) + + class Snippet(object): - _TB_EXPR = re.compile(r'\$(?:(?:{(\d+):(.*?)})|(\d+))') + _TABSTOP = re.compile(r'''(?xms) +(?:\${(\d+):(.*?)})| # A simple tabstop with default value +(?:\$(\d+)) # A mirror or a tabstop without default value. +''') def __init__(self,trigger,value): self._t = trigger @@ -83,47 +209,70 @@ class Snippet(object): return self._t trigger = property(trigger) - def _find_text_tabstops(self, lines): + def _handle_tabstop(self, m, val, tabstops, mirrors): + no = int(m.group(1)) + def_text = m.group(2) + + start, end = m.span() + val = val[:start] + def_text + val[end:] + + line_idx = val[:start].count('\n') + line_start = val[:start].rfind('\n') + 1 + start_in_line = start - line_start + ts = TabStop(no, line_idx, (start_in_line,start_in_line+len(def_text)), def_text) + + tabstops[ts.number] = ts + + return val + + def _handle_ts_or_mirror(self, m, val, tabstops, mirrors): + no = int(m.group(3)) + + start, end = m.span() + val = val[:start] + val[end:] + + line_idx = val[:start].count('\n') + line_start = val[:start].rfind('\n') + 1 + start_in_line = start - line_start + + if no in tabstops: + m = Mirror(tabstops[no], line_idx, start_in_line) + mirrors.append(m) + else: + ts = TabStop(no, line_idx, (start_in_line,start_in_line), "") + tabstops[ts.number] = ts + + return val + + def _find_tabstops(self, val): tabstops = {} + mirrors = [] - for idx in range(len(lines)): - line = lines[idx] - m = self._TB_EXPR.search(line) - while m is not None: - if m.group(1): - no = int(m.group(1)) - def_text = m.group(2) - else: - no = int(m.group(3)) - def_text = "" + while 1: + m = self._TABSTOP.search(val) + if m is not None: + if m.group(1) is not None: # ${1:hallo} + val = self._handle_tabstop(m,val,tabstops,mirrors) + elif m.group(3) is not None: # $1 + val = self._handle_ts_or_mirror(m,val,tabstops,mirrors) + else: + break - start, end = m.span() - line = line[:start] + def_text + line[end:] - - ts = TabStop(no, idx, (start,start+len(def_text)), def_text) - - lines[idx] = line - - tabstops[ts.number] = ts - - m = self._TB_EXPR.search(line) - - return tabstops + return tabstops, mirrors, val def _replace_tabstops(self): - lines = self._v.split('\n') + ts, mirrors, val = self._find_tabstops(self._v) + lines = val.split('\n') - ts = self._find_text_tabstops(lines) - - return ts, lines + return ts, mirrors, lines def launch(self, before, after): lineno,col = vim.current.window.cursor col -= len(self._t) - ts,lines = self._replace_tabstops() + ts, mirrors, lines = self._replace_tabstops() endcol = None newline = 1 @@ -142,8 +291,8 @@ class Snippet(object): vim.current.window.cursor = newline, newcol - if len(ts): - s = SnippetInstance( (lineno,col), (newline,newcol), ts) + if len(ts) or len(mirrors): + s = SnippetInstance( (lineno,col), (newline,newcol), ts, mirrors) s.select_next_tab() @@ -153,6 +302,7 @@ class Snippet(object): class SnippetManager(object): def __init__(self): self.reset() + self._last_cursor_pos = None def reset(self): self._snippets = {} @@ -183,7 +333,24 @@ class SnippetManager(object): self._current_snippets.append(s) def cursor_moved(self): - pass + cp = vim.current.window.cursor + + if and len(self._current_snippets) and self._last_cursor_pos is not None: + lineno,col = cp + llineo,lcol = self._last_cursor_pos + # If we moved the line, somethings fishy. + if lineno == self._last_cursor_pos[0]: + cs = self._current_snippets[-1] + + if lcol > col: # Some deleting was going on + cs.backspace(lcol-col) + else: + line = vim.current.line + + chars = line[lcol:col] + cs.chars_entered(chars) + + self._last_cursor_pos = cp def entered_insert_mode(self): pass diff --git a/PySnipEmu.vim b/PySnipEmu.vim index 603f29c..7bb1dc5 100644 --- a/PySnipEmu.vim +++ b/PySnipEmu.vim @@ -26,19 +26,6 @@ python from PySnipEmu import PySnipSnippets inoremap =PyVimSnips_ExpandSnippet() snoremap :call PyVimSnips_ExpandSnippet() -python PySnipSnippets.add_snippet("echo","echo ${1:Hallo}") -python PySnipSnippets.add_snippet("hello", "Hallo Welt!\nUnd Wie gehts?") -python PySnipSnippets.add_snippet("hallo", "hallo ${0:End} ${1:Beginning}") - - -python << EOF -PySnipSnippets.add_snippet("if", -"""if(${1:/* condition */}) { - ${2:/* code */} -} -""") -EOF - au CursorMovedI * py PySnipSnippets.cursor_moved() au InsertEnter * py PySnipSnippets.entered_insert_mode() diff --git a/test.py b/test.py index 4267770..c5ad3b8 100755 --- a/test.py +++ b/test.py @@ -14,7 +14,7 @@ class _VimTest(unittest.TestCase): expansion can take place """ def _send(s): - os.system("screen -X stuff '%s'" % s) + os.system("screen -x %s -X stuff '%s'" % (self.session,s)) splits = str.split('\t') for w in splits[:-1]: @@ -35,27 +35,28 @@ class _VimTest(unittest.TestCase): PySnipSnippets.add_snippet("%s","""%s""") EOF ''' % (sv,content)) - + # Clear the buffer self.type("bggVGd") - # Enter insert mode - self.type("i") + if not self.interrupt: + # Enter insert mode + self.type("i") - # Execute the command - self.cmd() + # Execute the command + self.cmd() - handle, fn = tempfile.mkstemp(prefix="PySnipEmuTest",suffix=".txt") - os.close(handle) + handle, fn = tempfile.mkstemp(prefix="PySnipEmuTest",suffix=".txt") + os.close(handle) - self.escape() - self.type(":w! %s\n" % fn) + self.escape() + self.type(":w! %s\n" % fn) - # Give screen a chance to send the cmd and vim to write the file - time.sleep(.01) + # Give screen a chance to send the cmd and vim to write the file + time.sleep(.01) - # Read the output, chop the trailing newline - self.output = open(fn,"r").read()[:-1] + # Read the output, chop the trailing newline + self.output = open(fn,"r").read()[:-1] def cmd(self): """Overwrite these in the children""" @@ -170,22 +171,109 @@ class TextTabStopSimpleReplace_ExceptCorrectResult(_VimTest): def runTest(self): self.assertEqual(self.output,"hallo Du Nase na") +class TextTabStopSimpleReplace_ExceptCorrectResult(_VimTest): + snippets = ( + ("hallo", "hallo ${0:End} ${1:Beginning}"), + ) + + def cmd(self): + self.type("hallo\tna\tDu Nase") + + def runTest(self): + self.assertEqual(self.output,"hallo Du Nase na") + +# TODO: multiline mirrors + +class TextTabStopSimpleMirror_ExceptCorrectResult(_VimTest): + snippets = ( + ("test", "$1\n$1"), + ) + def cmd(self): + self.type("test\thallo") + + def runTest(self): + self.assertEqual(self.output,"hallo\nhallo") + +class TextTabStopSimpleMirrorDelete_ExceptCorrectResult(_VimTest): + snippets = ( + ("test", "$1\n$1"), + ) + def cmd(self): + self.type("test\thallo") + self.type("\b\b") + + def runTest(self): + self.assertEqual(self.output,"hal\nhal") + +class TextTabStopSimpleMirrorSameLine_ExceptCorrectResult(_VimTest): + snippets = ( + ("test", "$1 $1"), + ) + def cmd(self): + self.type("test\thallo") + + def runTest(self): + self.assertEqual(self.output,"hal\nhal") + +class TextTabStopSimpleMirrorDeleteSomeEnterSome_ExceptCorrectResult(_VimTest): + snippets = ( + ("test", "$1\n$1"), + ) + def cmd(self): + self.type("test\thallo\b\bhups") + + def runTest(self): + self.assertEqual(self.output,"halhups\nhalhups") + +class TextTabStopMirrorMoreInvolved_ExceptCorrectResult(_VimTest): + snippets = ( + ("for", "for(size_t ${2:i} = 0; $2 < ${1:count}; ${3:++$2})\n{\n\t${0:/* code */}\n}"), + ) + + def cmd(self): + self.type("for\t") + + def runTest(self): + self.assertEqual(self.output,"hallo Du Nase na") + if __name__ == '__main__': import sys + import optparse - tests = [ - SimpleExpand_ExceptCorrectResult(), - SimpleExpandTypeAfterExpand_ExceptCorrectResult(), - SimpleExpandTypeAfterExpand1_ExceptCorrectResult(), - DoNotExpandAfterSpace_ExceptCorrectResult(), - ExpandInTheMiddleOfLine_ExceptCorrectResult(), - MultilineExpand_ExceptCorrectResult(), - MultilineExpandTestTyping_ExceptCorrectResult(), - ExitTabStop_ExceptCorrectResult(), - TextTabStopNoReplace_ExceptCorrectResult(), - TextTabStopSimpleReplace_ExceptCorrectResult(), - ] - # suite = unittest.TestLoader(.loadTestsFromModule(__import__("test")) + def parse_args(): + p = optparse.OptionParser("%prog [OPTIONS] ") + + p.set_defaults(session="vim", interrupt=False) + + p.add_option("-s", "--session", dest="session", metavar="SESSION", + help="send commands to screen session SESSION [%default]") + p.add_option("-i", "--interrupt", dest="interrupt", + action="store_true", + help="Stop after defining the snippet. This allows the user" \ + "to interactively test the snippet in vim. You must give exactly" \ + "one test case on the cmdline. The test will always fail." + ) + + o, args = p.parse_args() + return o, args + + options,selected_tests = parse_args() + + # The next line doesn't work in python 2.3 + all_test_suites = unittest.TestLoader().loadTestsFromModule(__import__("test")) + + # Inform all test case which screen session to use suite = unittest.TestSuite() - suite.addTests(tests) + for s in all_test_suites: + for test in s: + test.session = options.session + test.interrupt = options.interrupt + if len(selected_tests): + id = test.id().split('.')[1] + if id not in selected_tests: + continue + suite.addTest(test) + + res = unittest.TextTestRunner().run(suite) +