From ce8ff76b3119d600280b1f8ab8b20b2745b4e7c5 Mon Sep 17 00:00:00 2001 From: Andrew Gibiansky Date: Fri, 17 Oct 2014 17:44:04 -0700 Subject: [PATCH 1/8] Fixing python to conform to PEP8. --- python/haskell-textobj.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/python/haskell-textobj.py b/python/haskell-textobj.py index 67c3743..92b2188 100755 --- a/python/haskell-textobj.py +++ b/python/haskell-textobj.py @@ -1,29 +1,35 @@ #!/usr/bin/python import vim + def isTopBinding(text): - return False if len(text) == 0 or (text.startswith("--") or text[0].isspace()) else True + return not (len(text) == 0 or text.startswith("--") or text[0].isspace()) + def isStatement(text): - return True if len(text) > 0 and (text[0].isspace() and len(text.strip()) > 0) else False + return len(text) > 0 and text[0].isspace() and len(text.strip()) > 0 + def isTypeSignature(text): words = text.strip().split(" ") - return True if len(words) > 3 and words[1] == "::" else False + return len(words) > 3 and words[1] == "::" + def find(content, index, cmpF, iterF): i = index - while (i >= 0 and i < len(content)): + while i >= 0 and i < len(content): if cmpF(content[i]): return (True, i) i = iterF(i) return (False, i) + def setRetValue(start, end, lines): startPos = [0, start+1, 1, 0] endPos = [0, end+1, len(lines[end]), 0] vim.command("let g:haskell_textobj_ret="+str([startPos, endPos])) + def selectHaskellBinding(lines, cursor, includeType): """ Extract function binding from index in content @@ -31,13 +37,17 @@ def selectHaskellBinding(lines, cursor, includeType): cursor: zero-based line index return: [String] top level binding index resides in """ - backward = lambda x : x - 1 - forward = lambda x : x + 1 + backward = lambda x: x - 1 + forward = lambda x: x + 1 index = cursor - 1 found, bStart = find(lines, index, isTopBinding, backward) - found, bNext = find(lines, index+1, isTopBinding, forward) - found, bEnd = find(lines, bNext, isStatement, backward) if found else (True, bNext-1) + found, bNext = find(lines, index+1, isTopBinding, forward) + if found: + found, bEnd = find(lines, bNext, isStatement, backward) + else: + found = True + bEnd = bNext - 1 bEnd = bStart if bEnd < bStart else bEnd if includeType and bStart > 0 and isTypeSignature(lines[bStart-1]): From 807a3654b1d9b3837ad3a986ba76c7d0d622d55d Mon Sep 17 00:00:00 2001 From: Andrew Gibiansky Date: Mon, 27 Oct 2014 02:44:35 -0700 Subject: [PATCH 2/8] Refactoring, cleaning, updates. --- autoload/textobj/haskell.vim | 13 ++- plugin/textobj/haskell.vim | 5 +- python/haskell-textobj.py | 188 +++++++++++++++++++++++++++-------- 3 files changed, 156 insertions(+), 50 deletions(-) diff --git a/autoload/textobj/haskell.vim b/autoload/textobj/haskell.vim index 4d7f108..fe1da98 100644 --- a/autoload/textobj/haskell.vim +++ b/autoload/textobj/haskell.vim @@ -4,14 +4,17 @@ if !has('python') endif function! textobj#haskell#select_i() - if g:haskell_textobj_include_types == 0 - python selectHaskellBinding(vim.current.buffer, vim.current.window.cursor[0], False) - else - python selectHaskellBinding(vim.current.buffer, vim.current.window.cursor[0], True) - endif + python select_haskell_block(vim.current.buffer, vim.current.window.cursor[0], False) let start_position = g:haskell_textobj_ret[0] let end_position = g:haskell_textobj_ret[1] return ['v', start_position, end_position] endfunction +function! textobj#haskell#select_a() + python select_haskell_block(vim.current.buffer, vim.current.window.cursor[0], True) + + let start_position = g:haskell_textobj_ret[0] + let end_position = g:haskell_textobj_ret[1] + return ['v', start_position, end_position] +endfunction diff --git a/plugin/textobj/haskell.vim b/plugin/textobj/haskell.vim index 5fb6e9e..65a326d 100644 --- a/plugin/textobj/haskell.vim +++ b/plugin/textobj/haskell.vim @@ -11,16 +11,13 @@ if !exists('g:haskell_textobj_path') endif endif -if !exists('g:haskell_textobj_include_types') - let g:haskell_textobj_include_types = 0 -endif - python import vim execute 'pyfile ' . g:haskell_textobj_path call textobj#user#plugin('haskell', { \ '-': { \ 'select-i': 'ih', '*select-i-function*': 'textobj#haskell#select_i', + \ 'select-a': 'ah', '*select-a-function*': 'textobj#haskell#select_a', \ }, \}) diff --git a/python/haskell-textobj.py b/python/haskell-textobj.py index 92b2188..c745af2 100755 --- a/python/haskell-textobj.py +++ b/python/haskell-textobj.py @@ -1,56 +1,162 @@ #!/usr/bin/python -import vim +VIM_RETURN_VAR = 'haskell_textobj_ret' -def isTopBinding(text): - return not (len(text) == 0 or text.startswith("--") or text[0].isspace()) +import sys +try: + import vim +except ImportError: + print "Warning: Not running inside Vim." -def isStatement(text): - return len(text) > 0 and text[0].isspace() and len(text.strip()) > 0 +def vim_return(start_line, end_line, lines): + """ + Return the selection extent to Vim by setting a flag variable. + The format returned is described in the Vim documentation for getpos(). + """ + buf_num = 0 + offset = 0 + start_col = 1 + start_pos = [buf_num, start_line + 1, start_col, offset] -def isTypeSignature(text): - words = text.strip().split(" ") - return len(words) > 3 and words[1] == "::" + end_col = len(lines[end_line]) + end_pos = [buf_num, end_line + 1, end_col, offset] + cmd = "let g:%s=%s" % (VIM_RETURN_VAR, str([start_pos, end_pos])) + vim.command(cmd) -def find(content, index, cmpF, iterF): - i = index - while i >= 0 and i < len(content): - if cmpF(content[i]): - return (True, i) - i = iterF(i) - return (False, i) +def select_haskell_block(lines, cursor, around): + """ + Find the start and end location of the current haskell text object. -def setRetValue(start, end, lines): - startPos = [0, start+1, 1, 0] - endPos = [0, end+1, len(lines[end]), 0] - vim.command("let g:haskell_textobj_ret="+str([startPos, endPos])) + Arguments: + - lines: list of haskell source lines + - cursor: line index + - around: include auxiliary blocks? -def selectHaskellBinding(lines, cursor, includeType): + When `around` is true, the return value will select more. Specifically, + it will: + - Select all import statements in a block. + - Select all clauses of a function as well as the type signature. """ - Extract function binding from index in content - content: [String] list of haskell source lines - cursor: zero-based line index - return: [String] top level binding index resides in + start_line, end_line = find_block(lines, cursor, around) + vim_return(start_line, end_line, lines) + + +def indent_level(line): + """Return the indent level of the line (measured in spaces)""" + # Make sure it has something on it. + if not line.strip(): + raise ValueError("Empty line has no indent level") + + level = 0 + for i, char in enumerate(line): + if char != ' ' and char != '\t': + return level + elif char == ' ': + level += 1 + elif char == '\t': + level += 8 + + +def empty(line): + return not line.strip() + + +def indented(line): + return indent_level(line) > 0 + + +def has_start_block(line): + """Whether this line is the beginning of a block.""" + maybe_type = len(line.split("::")) == 2 + if maybe_type: + before, after = line.split("::") + if before.count(" ") <= 0: + return True + return any(line.startswith(start_token) + for start_token in ["data", "newtype", "import"]) + + +def is_comment(line): + line = line.strip() + return line.startswith("--") or line.startswith("{-") + + +def find_block(lines, index, around=False): + """Find a block that the cursor is in. + + Arguments: + - lines: all the lines in the file. + - index: the line number of the cursor. + - around: Whether to include surrounding blocks. """ - backward = lambda x: x - 1 - forward = lambda x: x + 1 - index = cursor - 1 - - found, bStart = find(lines, index, isTopBinding, backward) - found, bNext = find(lines, index+1, isTopBinding, forward) - if found: - found, bEnd = find(lines, bNext, isStatement, backward) - else: - found = True - bEnd = bNext - 1 - bEnd = bStart if bEnd < bStart else bEnd - - if includeType and bStart > 0 and isTypeSignature(lines[bStart-1]): - bStart = bStart - 1 - - setRetValue(bStart, bEnd, lines) + # Move the cursor until we find a non-empty line. + while index < len(lines) and empty(lines[index]): + index += 1 + + # Start by only including the line we're on. + start_index = index + end_index = index + + # Expand the selection upwards. + def expand_upwards(): + # Can't go past file start. + if start_index == 0: + return False + + current_line = lines[start_index] + previous_line = lines[start_index - 1] + + if is_comment(previous_line): + return True + + # If this is the start of a block, go no further. + if not empty(current_line) and not indented(current_line): + return False + + return True + + while expand_upwards(): + start_index -= 1 + + def expand_downwards(): + # Can't go past file end. + if end_index == len(lines) - 1: + return False + + current_line = lines[end_index] + next_line = lines[end_index + 1] + + if is_comment(current_line) and is_comment(next_line): + return True + + # Can't encroach on the next start block. + if has_start_block(next_line): + return False + + return empty(next_line) or indented(next_line) + + while expand_downwards(): + end_index += 1 + + # Trim the selection to avoid newlines. + while empty(lines[start_index]): + start_index += 1 + while empty(lines[end_index]): + end_index -= 1 + + return start_index, end_index + +if __name__ == "__main__": + lines = open(sys.argv[1]).readlines() + for ind in xrange(len(lines)): + start, end = find_block(lines, ind) + for i in xrange(start, end + 1): + print lines[i][:-1] + print ind, find_block(lines, ind) + raw_input() + print '---' From 057a5b7e61391e203034c0d454d03573f1bf393f Mon Sep 17 00:00:00 2001 From: Andrew Gibiansky Date: Mon, 27 Oct 2014 14:48:49 -0700 Subject: [PATCH 3/8] Adding around selection support --- python/haskell-textobj.py | 44 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/python/haskell-textobj.py b/python/haskell-textobj.py index c745af2..7063aad 100755 --- a/python/haskell-textobj.py +++ b/python/haskell-textobj.py @@ -42,10 +42,50 @@ def select_haskell_block(lines, cursor, around): - Select all import statements in a block. - Select all clauses of a function as well as the type signature. """ - start_line, end_line = find_block(lines, cursor, around) + start_line, end_line = find_block(lines, cursor) + if around: + # Take care of expanding imports. + while is_import(start_line, end_line, lines): + start_line, end_line = extend_imports(start_line, end_line, lines) + + # Take care of extending pattern matches. + if is_decl(start_line, end_line, lines): + start_line, end_line = extend_typesig(start_line, end_line, lines) vim_return(start_line, end_line, lines) +def extend_imports(start_line, end_line, lines): + start2, end2 = find_block(lines, start_line - 1) + if start2 != start_line and is_import(start2, end2, lines): + return start2, end_line + + start3, end3 = find_block(lines, end_line + 1) + if end3 != end_line and is_import(start3, end3, lines): + return start_line, end3 + + +def is_import(start, end, lines): + return lines[start].strip().startswith("import") + + +def is_decl(start, end, lines): + line = lines[start].strip() + decl = "=" in line + if not decl: + return False + + if any(tok in line for tok in ["data", "newtype"]): + return False + + return True + + +def extend_typesig(start_line, end_line, lines): + start2, end2 = find_block(lines, start_line - 1) + if "::" in lines[start2].split()[1]: + return start2, end_line + + def indent_level(line): """Return the indent level of the line (measured in spaces)""" # Make sure it has something on it. @@ -86,7 +126,7 @@ def is_comment(line): return line.startswith("--") or line.startswith("{-") -def find_block(lines, index, around=False): +def find_block(lines, index): """Find a block that the cursor is in. Arguments: From 7a32f09141c16c55824b72a9d0a348964528cdba Mon Sep 17 00:00:00 2001 From: Andrew Gibiansky Date: Mon, 27 Oct 2014 14:53:26 -0700 Subject: [PATCH 4/8] Minor fixes after testing. --- python/haskell-textobj.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python/haskell-textobj.py b/python/haskell-textobj.py index 7063aad..3dd67ab 100755 --- a/python/haskell-textobj.py +++ b/python/haskell-textobj.py @@ -42,11 +42,15 @@ def select_haskell_block(lines, cursor, around): - Select all import statements in a block. - Select all clauses of a function as well as the type signature. """ - start_line, end_line = find_block(lines, cursor) + start_line, end_line = find_block(lines, cursor - 1) if around: # Take care of expanding imports. while is_import(start_line, end_line, lines): - start_line, end_line = extend_imports(start_line, end_line, lines) + new = extend_imports(start_line, end_line, lines) + if new is not None: + start_line, end_line = new + else: + break # Take care of extending pattern matches. if is_decl(start_line, end_line, lines): @@ -84,6 +88,8 @@ def extend_typesig(start_line, end_line, lines): start2, end2 = find_block(lines, start_line - 1) if "::" in lines[start2].split()[1]: return start2, end_line + else: + return start_line, end_line def indent_level(line): From e2f3b5fa20c48538cad9c892026c9ddba03cdb3c Mon Sep 17 00:00:00 2001 From: Andrew Gibiansky Date: Mon, 27 Oct 2014 14:56:31 -0700 Subject: [PATCH 5/8] Dont let comments confuse the selector. --- python/haskell-textobj.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python/haskell-textobj.py b/python/haskell-textobj.py index 3dd67ab..a42bd55 100755 --- a/python/haskell-textobj.py +++ b/python/haskell-textobj.py @@ -86,7 +86,12 @@ def is_decl(start, end, lines): def extend_typesig(start_line, end_line, lines): start2, end2 = find_block(lines, start_line - 1) - if "::" in lines[start2].split()[1]: + + first_line = start2 + while is_comment(lines[first_line]): + first_line += 1 + + if "::" in lines[first_line].split()[1]: return start2, end_line else: return start_line, end_line @@ -129,7 +134,8 @@ def has_start_block(line): def is_comment(line): line = line.strip() - return line.startswith("--") or line.startswith("{-") + is_pragma = line.startswith("{-#") + return line.startswith("--") or (line.startswith("{-") and not is_pragma) def find_block(lines, index): From 21c7a06ebd198870434987013e8fdca6dd23f9af Mon Sep 17 00:00:00 2001 From: Andrew Gibiansky Date: Mon, 27 Oct 2014 14:57:32 -0700 Subject: [PATCH 6/8] Dont run main on startup --- python/haskell-textobj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/haskell-textobj.py b/python/haskell-textobj.py index a42bd55..c94da31 100755 --- a/python/haskell-textobj.py +++ b/python/haskell-textobj.py @@ -203,7 +203,7 @@ def expand_downwards(): return start_index, end_index -if __name__ == "__main__": +if __name__ == "__main__" and 'vim' not in sys.modules: lines = open(sys.argv[1]).readlines() for ind in xrange(len(lines)): start, end = find_block(lines, ind) From a432cee7e9c1646331d1662f04192f9e7f2b6465 Mon Sep 17 00:00:00 2001 From: Andrew Gibiansky Date: Mon, 27 Oct 2014 15:11:36 -0700 Subject: [PATCH 7/8] Extend decls as well as imports --- python/haskell-textobj.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/python/haskell-textobj.py b/python/haskell-textobj.py index c94da31..d1c7524 100755 --- a/python/haskell-textobj.py +++ b/python/haskell-textobj.py @@ -45,19 +45,40 @@ def select_haskell_block(lines, cursor, around): start_line, end_line = find_block(lines, cursor - 1) if around: # Take care of expanding imports. - while is_import(start_line, end_line, lines): + if is_import(start_line, end_line, lines): new = extend_imports(start_line, end_line, lines) - if new is not None: + while new is not None: start_line, end_line = new - else: - break + new = extend_imports(start_line, end_line, lines) - # Take care of extending pattern matches. + # Take care of expanding pattern matches. + if is_decl(start_line, end_line, lines): + new = extend_decls(start_line, end_line, lines) + while new is not None: + start_line, end_line = new + new = extend_decls(start_line, end_line, lines) + + # Add a type signature if necessary. if is_decl(start_line, end_line, lines): start_line, end_line = extend_typesig(start_line, end_line, lines) vim_return(start_line, end_line, lines) +def extend_decls(start_line, end_line, lines): + start2, end2 = find_block(lines, start_line - 1) + if start2 != start_line and is_decl(start2, end2, lines): + if lines[start_line].startswith(lines[start2].split()[0]): + return start2, end_line + + start3, end3 = find_block(lines, end_line + 1) + if end3 != end_line and is_decl(start3, end3, lines): + if lines[start_line].startswith(lines[start3].split()[0]): + return start_line, end3 + + # No more cases + return None + + def extend_imports(start_line, end_line, lines): start2, end2 = find_block(lines, start_line - 1) if start2 != start_line and is_import(start2, end2, lines): @@ -67,6 +88,9 @@ def extend_imports(start_line, end_line, lines): if end3 != end_line and is_import(start3, end3, lines): return start_line, end3 + # Signal no more imports to add + return None + def is_import(start, end, lines): return lines[start].strip().startswith("import") From ca656e98ea31e201f5bc543909398a6c8bb5d537 Mon Sep 17 00:00:00 2001 From: Andrew Gibiansky Date: Mon, 27 Oct 2014 15:15:44 -0700 Subject: [PATCH 8/8] Dont let extending reach file boundaries --- python/haskell-textobj.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/python/haskell-textobj.py b/python/haskell-textobj.py index d1c7524..ad6457c 100755 --- a/python/haskell-textobj.py +++ b/python/haskell-textobj.py @@ -65,28 +65,32 @@ def select_haskell_block(lines, cursor, around): def extend_decls(start_line, end_line, lines): - start2, end2 = find_block(lines, start_line - 1) - if start2 != start_line and is_decl(start2, end2, lines): - if lines[start_line].startswith(lines[start2].split()[0]): - return start2, end_line - - start3, end3 = find_block(lines, end_line + 1) - if end3 != end_line and is_decl(start3, end3, lines): - if lines[start_line].startswith(lines[start3].split()[0]): - return start_line, end3 + if start_line - 1 >= 0: + start2, end2 = find_block(lines, start_line - 1) + if start2 != start_line and is_decl(start2, end2, lines): + if lines[start_line].startswith(lines[start2].split()[0]): + return start2, end_line + + if end_line + 1 <= len(lines) - 1: + start3, end3 = find_block(lines, end_line + 1) + if end3 != end_line and is_decl(start3, end3, lines): + if lines[start_line].startswith(lines[start3].split()[0]): + return start_line, end3 # No more cases return None def extend_imports(start_line, end_line, lines): - start2, end2 = find_block(lines, start_line - 1) - if start2 != start_line and is_import(start2, end2, lines): - return start2, end_line + if start_line - 1 >= 0: + start2, end2 = find_block(lines, start_line - 1) + if start2 != start_line and is_import(start2, end2, lines): + return start2, end_line - start3, end3 = find_block(lines, end_line + 1) - if end3 != end_line and is_import(start3, end3, lines): - return start_line, end3 + if end_line + 1 <= len(lines) - 1: + start3, end3 = find_block(lines, end_line + 1) + if end3 != end_line and is_import(start3, end3, lines): + return start_line, end3 # Signal no more imports to add return None