##// END OF EJS Templates
Fix line_at_cursor for end of last line without trailing newline
Thomas Kluyver -
Show More
@@ -1,127 +1,133 b''
1 1 """Tests for tokenutil"""
2 2 # Copyright (c) IPython Development Team.
3 3 # Distributed under the terms of the Modified BSD License.
4 4
5 5 import nose.tools as nt
6 6
7 7 from IPython.utils.tokenutil import token_at_cursor, line_at_cursor
8 8
9 9 def expect_token(expected, cell, cursor_pos):
10 10 token = token_at_cursor(cell, cursor_pos)
11 11 offset = 0
12 12 for line in cell.splitlines():
13 13 if offset + len(line) >= cursor_pos:
14 14 break
15 15 else:
16 16 offset += len(line)+1
17 17 column = cursor_pos - offset
18 18 line_with_cursor = '%s|%s' % (line[:column], line[column:])
19 19 nt.assert_equal(token, expected,
20 20 "Expected %r, got %r in: %r (pos %i)" % (
21 21 expected, token, line_with_cursor, cursor_pos)
22 22 )
23 23
24 24 def test_simple():
25 25 cell = "foo"
26 26 for i in range(len(cell)):
27 27 expect_token("foo", cell, i)
28 28
29 29 def test_function():
30 30 cell = "foo(a=5, b='10')"
31 31 expected = 'foo'
32 32 # up to `foo(|a=`
33 33 for i in range(cell.find('a=') + 1):
34 34 expect_token("foo", cell, i)
35 35 # find foo after `=`
36 36 for i in [cell.find('=') + 1, cell.rfind('=') + 1]:
37 37 expect_token("foo", cell, i)
38 38 # in between `5,|` and `|b=`
39 39 for i in range(cell.find(','), cell.find('b=')):
40 40 expect_token("foo", cell, i)
41 41
42 42 def test_multiline():
43 43 cell = '\n'.join([
44 44 'a = 5',
45 45 'b = hello("string", there)'
46 46 ])
47 47 expected = 'hello'
48 48 start = cell.index(expected) + 1
49 49 for i in range(start, start + len(expected)):
50 50 expect_token(expected, cell, i)
51 51 expected = 'hello'
52 52 start = cell.index(expected) + 1
53 53 for i in range(start, start + len(expected)):
54 54 expect_token(expected, cell, i)
55 55
56 56 def test_multiline_token():
57 57 cell = '\n'.join([
58 58 '"""\n\nxxxxxxxxxx\n\n"""',
59 59 '5, """',
60 60 'docstring',
61 61 'multiline token',
62 62 '""", [',
63 63 '2, 3, "complicated"]',
64 64 'b = hello("string", there)'
65 65 ])
66 66 expected = 'hello'
67 67 start = cell.index(expected) + 1
68 68 for i in range(start, start + len(expected)):
69 69 expect_token(expected, cell, i)
70 70 expected = 'hello'
71 71 start = cell.index(expected) + 1
72 72 for i in range(start, start + len(expected)):
73 73 expect_token(expected, cell, i)
74 74
75 75 def test_nested_call():
76 76 cell = "foo(bar(a=5), b=10)"
77 77 expected = 'foo'
78 78 start = cell.index('bar') + 1
79 79 for i in range(start, start + 3):
80 80 expect_token(expected, cell, i)
81 81 expected = 'bar'
82 82 start = cell.index('a=')
83 83 for i in range(start, start + 3):
84 84 expect_token(expected, cell, i)
85 85 expected = 'foo'
86 86 start = cell.index(')') + 1
87 87 for i in range(start, len(cell)-1):
88 88 expect_token(expected, cell, i)
89 89
90 90 def test_attrs():
91 91 cell = "a = obj.attr.subattr"
92 92 expected = 'obj'
93 93 idx = cell.find('obj') + 1
94 94 for i in range(idx, idx + 3):
95 95 expect_token(expected, cell, i)
96 96 idx = cell.find('.attr') + 2
97 97 expected = 'obj.attr'
98 98 for i in range(idx, idx + 4):
99 99 expect_token(expected, cell, i)
100 100 idx = cell.find('.subattr') + 2
101 101 expected = 'obj.attr.subattr'
102 102 for i in range(idx, len(cell)):
103 103 expect_token(expected, cell, i)
104 104
105 105 def test_line_at_cursor():
106 106 cell = ""
107 107 (line, offset) = line_at_cursor(cell, cursor_pos=11)
108 assert line == "", ("Expected '', got %r" % line)
109 assert offset == 0, ("Expected '', got %r" % line)
108 nt.assert_equal(line, "")
109 nt.assert_equal(offset, 0)
110 110
111 111 # The position after a newline should be the start of the following line.
112 112 cell = "One\nTwo\n"
113 113 (line, offset) = line_at_cursor(cell, cursor_pos=4)
114 114 nt.assert_equal(line, "Two\n")
115 115 nt.assert_equal(offset, 4)
116 116
117 # The end of a cell should be on the last line
118 cell = "pri\npri"
119 (line, offset) = line_at_cursor(cell, cursor_pos=7)
120 nt.assert_equal(line, "pri")
121 nt.assert_equal(offset, 4)
122
117 123 def test_muliline_statement():
118 124 cell = """a = (1,
119 125 3)
120 126
121 127 int()
122 128 map()
123 129 """
124 130 for c in range(16, 22):
125 131 yield lambda: expect_token("int", cell, c)
126 132 for c in range(22, 28):
127 133 yield lambda: expect_token("map", cell, c)
@@ -1,125 +1,130 b''
1 1 """Token-related utilities"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from collections import namedtuple
7 7 from io import StringIO
8 8 from keyword import iskeyword
9 9
10 10 from . import tokenize2
11 11
12 12
13 13 Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line'])
14 14
15 15 def generate_tokens(readline):
16 16 """wrap generate_tokens to catch EOF errors"""
17 17 try:
18 18 for token in tokenize2.generate_tokens(readline):
19 19 yield token
20 20 except tokenize2.TokenError:
21 21 # catch EOF error
22 22 return
23 23
24 24 def line_at_cursor(cell, cursor_pos=0):
25 25 """Return the line in a cell at a given cursor position
26 26
27 27 Used for calling line-based APIs that don't support multi-line input, yet.
28 28
29 29 Parameters
30 30 ----------
31 31
32 32 cell: str
33 33 multiline block of text
34 34 cursor_pos: integer
35 35 the cursor position
36 36
37 37 Returns
38 38 -------
39 39
40 40 (line, offset): (string, integer)
41 41 The line with the current cursor, and the character offset of the start of the line.
42 42 """
43 43 offset = 0
44 44 lines = cell.splitlines(True)
45 45 for line in lines:
46 46 next_offset = offset + len(line)
47 if not line.endswith('\n'):
48 # If the last line doesn't have a trailing newline, treat it as if
49 # it does so that the cursor at the end of the line still counts
50 # as being on that line.
51 next_offset += 1
47 52 if next_offset > cursor_pos:
48 53 break
49 54 offset = next_offset
50 55 else:
51 56 line = ""
52 57 return (line, offset)
53 58
54 59 def token_at_cursor(cell, cursor_pos=0):
55 60 """Get the token at a given cursor
56 61
57 62 Used for introspection.
58 63
59 64 Function calls are prioritized, so the token for the callable will be returned
60 65 if the cursor is anywhere inside the call.
61 66
62 67 Parameters
63 68 ----------
64 69
65 70 cell : unicode
66 71 A block of Python code
67 72 cursor_pos : int
68 73 The location of the cursor in the block where the token should be found
69 74 """
70 75 names = []
71 76 tokens = []
72 77 call_names = []
73 78
74 79 offsets = {1: 0} # lines start at 1
75 80 for tup in generate_tokens(StringIO(cell).readline):
76 81
77 82 tok = Token(*tup)
78 83
79 84 # token, text, start, end, line = tup
80 85 start_line, start_col = tok.start
81 86 end_line, end_col = tok.end
82 87 if end_line + 1 not in offsets:
83 88 # keep track of offsets for each line
84 89 lines = tok.line.splitlines(True)
85 90 for lineno, line in enumerate(lines, start_line + 1):
86 91 if lineno not in offsets:
87 92 offsets[lineno] = offsets[lineno-1] + len(line)
88 93
89 94 offset = offsets[start_line]
90 95 # allow '|foo' to find 'foo' at the beginning of a line
91 96 boundary = cursor_pos + 1 if start_col == 0 else cursor_pos
92 97 if offset + start_col >= boundary:
93 98 # current token starts after the cursor,
94 99 # don't consume it
95 100 break
96 101
97 102 if tok.token == tokenize2.NAME and not iskeyword(tok.text):
98 103 if names and tokens and tokens[-1].token == tokenize2.OP and tokens[-1].text == '.':
99 104 names[-1] = "%s.%s" % (names[-1], tok.text)
100 105 else:
101 106 names.append(tok.text)
102 107 elif tok.token == tokenize2.OP:
103 108 if tok.text == '=' and names:
104 109 # don't inspect the lhs of an assignment
105 110 names.pop(-1)
106 111 if tok.text == '(' and names:
107 112 # if we are inside a function call, inspect the function
108 113 call_names.append(names[-1])
109 114 elif tok.text == ')' and call_names:
110 115 call_names.pop(-1)
111 116
112 117 tokens.append(tok)
113 118
114 119 if offsets[end_line] + end_col > cursor_pos:
115 120 # we found the cursor, stop reading
116 121 break
117 122
118 123 if call_names:
119 124 return call_names[-1]
120 125 elif names:
121 126 return names[-1]
122 127 else:
123 128 return ''
124 129
125 130
General Comments 0
You need to be logged in to leave comments. Login now