Coverage for C:\Repos\leo-editor\leo\commands\gotoCommands.py: 31%
248 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20150624112334.1: * @file ../commands/gotoCommands.py
4#@@first
5"""Leo's goto commands."""
6import re
7from leo.core import leoGlobals as g
8#@+others
9#@+node:ekr.20150625050355.1: ** class GoToCommands
10class GoToCommands:
11 """A class implementing goto-global-line."""
12 #@+others
13 #@+node:ekr.20100216141722.5621: *3* goto.ctor
14 def __init__(self, c):
15 """Ctor for GoToCommands class."""
16 self.c = c
17 #@+node:ekr.20100216141722.5622: *3* goto.find_file_line
18 def find_file_line(self, n, p=None):
19 """
20 Place the cursor on the n'th line (one-based) of an external file.
21 Return (p, offset, found) for unit testing.
22 """
23 c = self.c
24 if n < 0:
25 return None, -1, False
26 p = p or c.p
27 root, fileName = self.find_root(p)
28 if root:
29 # Step 1: Get the lines of external files *with* sentinels,
30 # even if the actual external file actually contains no sentinels.
31 sentinels = root.isAtFileNode()
32 s = self.get_external_file_with_sentinels(root)
33 lines = g.splitLines(s)
34 # Step 2: scan the lines for line n.
35 if sentinels:
36 # All sentinels count as real lines.
37 gnx, h, offset = self.scan_sentinel_lines(lines, n, root)
38 else:
39 # Not all sentinels cound as real lines.
40 gnx, h, offset = self.scan_nonsentinel_lines(lines, n, root)
41 p, found = self.find_gnx(root, gnx, h)
42 if gnx and found:
43 self.success(n, offset, p)
44 return p, offset, True
45 self.fail(lines, n, root)
46 return None, -1, False
47 return self.find_script_line(n, p)
48 #@+node:ekr.20160921210529.1: *3* goto.find_node_start
49 def find_node_start(self, p, s=None):
50 """Return the global line number of the first line of p.b"""
51 # See #283.
52 root, fileName = self.find_root(p)
53 if not root:
54 return None
55 assert root.isAnyAtFileNode()
56 if s is None:
57 s = self.get_external_file_with_sentinels(root)
58 delim1, delim2 = self.get_delims(root)
59 # Match only the node with the correct gnx.
60 node_pat = re.compile(r'\s*%s@\+node:%s:' % (
61 re.escape(delim1), re.escape(p.gnx)))
62 for i, s in enumerate(g.splitLines(s)):
63 if node_pat.match(s):
64 return i + 1
65 return None
66 #@+node:ekr.20150622140140.1: *3* goto.find_script_line
67 def find_script_line(self, n, root):
68 """
69 Go to line n (zero based) of the script with the given root.
70 Return p, offset, found for unit testing.
71 """
72 c = self.c
73 if n < 0:
74 return None, -1, False
75 script = g.getScript(c, root, useSelectedText=False)
76 lines = g.splitLines(script)
77 # Script lines now *do* have gnx's.
78 gnx, h, offset = self.scan_sentinel_lines(lines, n, root)
79 p, found = self.find_gnx(root, gnx, h)
80 if gnx and found:
81 self.success(n, offset, p)
82 return p, offset, True
83 self.fail(lines, n, root)
84 return None, -1, False
85 #@+node:ekr.20181003080042.1: *3* goto.node_offset_to_file_line
86 def node_offset_to_file_line(self, target_offset, target_p, root):
87 """
88 Given a zero-based target_offset within target_p.b, return the line
89 number of the corresponding line within root's file.
90 """
91 delim1, delim2 = self.get_delims(root)
92 file_s = self.get_external_file_with_sentinels(root)
93 gnx, h, n, node_offset, target_gnx = None, None, -1, None, target_p.gnx
94 stack = []
95 for s in g.splitLines(file_s):
96 n += 1 # All lines contribute to the file's line count.
97 # g.trace('%4s %4r %40r %s' % (n, node_offset, h, s.rstrip()))
98 if self.is_sentinel(delim1, delim2, s):
99 s2 = s.strip()[len(delim1) :]
100 # Common code for the visible sentinels.
101 if s2.startswith(('@+others', '@+<<', '@@'),):
102 if target_offset == node_offset and gnx == target_gnx:
103 return n
104 if node_offset is not None:
105 node_offset += 1
106 # These sentinels change nodes...
107 if s2.startswith('@+node'):
108 gnx, h = self.get_script_node_info(s, delim2)
109 node_offset = 0
110 elif s2.startswith('@-node'):
111 gnx = node_offset = None
112 elif s2.startswith(('@+others', '@+<<'),):
113 stack.append([gnx, h, node_offset])
114 gnx, node_offset = None, None
115 elif s2.startswith(('@-others', '@-<<'),):
116 gnx, h, node_offset = stack.pop()
117 else:
118 # All non-sentinel lines are visible.
119 if target_offset == node_offset and gnx == target_gnx:
120 return n
121 if node_offset is not None:
122 node_offset += 1
123 g.trace('\nNot found', target_offset, target_gnx)
124 return None
125 #@+node:ekr.20150624085605.1: *3* goto.scan_nonsentinel_lines
126 def scan_nonsentinel_lines(self, lines, n, root):
127 """
128 Scan a list of lines containing sentinels, looking for the node and
129 offset within the node of the n'th (one-based) line.
131 Only non-sentinel lines increment the global line count, but
132 @+node sentinels reset the offset within the node.
134 Return gnx, h, offset:
135 gnx: the gnx of the #@+node
136 h: the headline of the #@+node
137 offset: the offset of line n within the node.
138 """
139 delim1, delim2 = self.get_delims(root)
140 count, gnx, h, offset = 0, root.gnx, root.h, 0
141 stack = [(gnx, h, offset),]
142 for s in lines:
143 is_sentinel = self.is_sentinel(delim1, delim2, s)
144 if is_sentinel:
145 s2 = s.strip()[len(delim1) :]
146 if s2.startswith('@+node'):
147 # Invisible, but resets the offset.
148 offset = 0
149 gnx, h = self.get_script_node_info(s, delim2)
150 elif s2.startswith('@+others') or s2.startswith('@+<<'):
151 stack.append((gnx, h, offset),)
152 # @others is visible in the outline, but *not* in the file.
153 offset += 1
154 elif s2.startswith('@-others') or s2.startswith('@-<<'):
155 gnx, h, offset = stack.pop()
156 # @-others is invisible.
157 offset += 1
158 elif s2.startswith('@@'):
159 # Directives are visible in the outline, but *not* in the file.
160 offset += 1
161 else:
162 # All other sentinels are invisible to the user.
163 offset += 1
164 else:
165 # Non-sentinel lines are visible both in the outline and the file.
166 count += 1
167 offset += 1
168 if count == n:
169 # Count is the real, one-based count.
170 break
171 else:
172 gnx, h, offset = None, None, -1
173 return gnx, h, offset
174 #@+node:ekr.20150623175314.1: *3* goto.scan_sentinel_lines
175 def scan_sentinel_lines(self, lines, n, root):
176 """
177 Scan a list of lines containing sentinels, looking for the node and
178 offset within the node of the n'th (one-based) line.
180 Return gnx, h, offset:
181 gnx: the gnx of the #@+node
182 h: the headline of the #@+node
183 offset: the offset of line n within the node.
184 """
185 delim1, delim2 = self.get_delims(root)
186 gnx, h, offset = root.gnx, root.h, 0
187 stack = [(gnx, h, offset),]
188 for i, s in enumerate(lines):
189 if self.is_sentinel(delim1, delim2, s):
190 s2 = s.strip()[len(delim1) :]
191 if s2.startswith('@+node'):
192 offset = 0
193 gnx, h = self.get_script_node_info(s, delim2)
194 elif s2.startswith('@+others') or s2.startswith('@+<<'):
195 stack.append((gnx, h, offset),)
196 offset += 1
197 elif s2.startswith('@-others') or s2.startswith('@-<<'):
198 gnx, h, offset = stack.pop()
199 offset += 1
200 else:
201 offset += 1
202 else:
203 offset += 1
204 if i + 1 == n: # Bug fix 2017/04/01: n is one based.
205 break
206 else:
207 gnx, h, offset = None, None, -1
208 return gnx, h, offset
209 #@+node:ekr.20150624142449.1: *3* goto.Utils
210 #@+node:ekr.20150625133523.1: *4* goto.fail
211 def fail(self, lines, n, root):
212 """Select the last line of the last node of root's tree."""
213 c = self.c
214 w = c.frame.body.wrapper
215 c.selectPosition(root)
216 c.redraw()
217 if not g.unitTesting:
218 if len(lines) < n:
219 g.warning('only', len(lines), 'lines')
220 else:
221 g.warning('line', n, 'not found')
222 c.frame.clearStatusLine()
223 c.frame.putStatusLine(f"goto-global-line not found: {n}")
224 # Put the cursor on the last line of body text.
225 w.setInsertPoint(len(root.b))
226 c.bodyWantsFocus()
227 w.seeInsertPoint()
228 #@+node:ekr.20100216141722.5626: *4* goto.find_gnx
229 def find_gnx(self, root, gnx, vnodeName):
230 """
231 Scan root's tree for a node with the given gnx and vnodeName.
232 return (p,found)
233 """
234 if gnx:
235 gnx = g.toUnicode(gnx)
236 for p in root.self_and_subtree(copy=False):
237 if p.matchHeadline(vnodeName):
238 if p.v.fileIndex == gnx:
239 return p.copy(), True
240 return None, False
241 return root, False
242 #@+node:ekr.20100216141722.5627: *4* goto.find_root
243 def find_root(self, p):
244 """
245 Find the closest ancestor @<file> node, except @all nodes and @edit nodes.
246 return root, fileName.
247 """
248 c = self.c
249 p1 = p.copy()
250 # First look for ancestor @file node.
251 for p in p.self_and_parents(copy=False):
252 if not p.isAtEditNode() and not p.isAtAllNode():
253 fileName = p.anyAtFileNodeName()
254 if fileName:
255 return p.copy(), fileName
256 # Search the entire tree for joined nodes.
257 # Bug fix: Leo 4.5.1: *must* search *all* positions.
258 for p in c.all_positions():
259 if p.v == p1.v and p != p1:
260 # Found a joined position.
261 for p2 in p.self_and_parents():
262 fileName = not p2.isAtAllNode() and p2.anyAtFileNodeName()
263 if fileName:
264 return p2.copy(), fileName
265 return None, None
266 #@+node:ekr.20150625123747.1: *4* goto.get_delims
267 def get_delims(self, root):
268 """Return the deliminters in effect at root."""
269 c = self.c
270 old_target_language = c.target_language
271 try:
272 c.target_language = g.getLanguageAtPosition(c, root)
273 d = c.scanAllDirectives(root)
274 finally:
275 c.target_language = old_target_language
276 delims1, delims2, delims3 = d.get('delims')
277 if delims1:
278 return delims1, None
279 return delims2, delims3
280 #@+node:ekr.20150624143903.1: *4* goto.get_external_file_with_sentinels
281 def get_external_file_with_sentinels(self, root):
282 """
283 root is an @<file> node.
285 Return the result of writing the file *with* sentinels, even if the
286 external file would normally *not* have sentinels.
287 """
288 c = self.c
289 if root.isAtAutoNode():
290 # Special case @auto nodes:
291 # Leo does not write sentinels in the root @auto node.
292 at = c.atFileCommands
293 ivar = 'force_sentinels'
294 try:
295 setattr(at, ivar, True)
296 s = at.atAutoToString(root)
297 finally:
298 if hasattr(at, ivar):
299 delattr(at, ivar)
300 return s
301 return g.composeScript( # Fix # 429.
302 c=c,
303 p=root,
304 s=root.b,
305 forcePythonSentinels=False, # See #247.
306 useSentinels=True)
307 #@+node:ekr.20150623175738.1: *4* goto.get_script_node_info
308 def get_script_node_info(self, s, delim2):
309 """Return the gnx and headline of a #@+node."""
310 i = s.find(':', 0)
311 j = s.find(':', i + 1)
312 if i == -1 or j == -1:
313 g.error("bad @+node sentinel", s)
314 return None, None
315 gnx = s[i + 1 : j]
316 h = s[j + 1 :]
317 h = self.remove_level_stars(h).strip()
318 if delim2:
319 h = h.rstrip(delim2)
320 return gnx, h
321 #@+node:ekr.20150625124027.1: *4* goto.is_sentinel
322 def is_sentinel(self, delim1, delim2, s):
323 """Return True if s is a sentinel line with the given delims."""
324 assert delim1
325 i = s.find(delim1 + '@')
326 if delim2:
327 j = s.find(delim2)
328 return -1 < i < j
329 return -1 < i
330 #@+node:ekr.20100728074713.5843: *4* goto.remove_level_stars
331 def remove_level_stars(self, s):
332 i = g.skip_ws(s, 0)
333 # Remove leading stars.
334 while i < len(s) and s[i] == '*':
335 i += 1
336 # Remove optional level number.
337 while i < len(s) and s[i].isdigit():
338 i += 1
339 # Remove trailing stars.
340 while i < len(s) and s[i] == '*':
341 i += 1
342 # Remove one blank.
343 if i < len(s) and s[i] == ' ':
344 i += 1
345 return s[i:]
346 #@+node:ekr.20100216141722.5638: *4* goto.success
347 def success(self, n, n2, p):
348 """Place the cursor on line n2 of p.b."""
349 c = self.c
350 w = c.frame.body.wrapper
351 # Select p and make it visible.
352 c.selectPosition(p)
353 c.redraw(p)
354 # Put the cursor on line n2 of the body text.
355 s = w.getAllText()
356 ins = g.convertRowColToPythonIndex(s, n2 - 1, 0)
357 c.frame.clearStatusLine()
358 c.frame.putStatusLine(f"goto-global-line found: {n2}")
359 w.setInsertPoint(ins)
360 c.bodyWantsFocus()
361 w.seeInsertPoint()
362 #@-others
363#@+node:ekr.20180517041303.1: ** show-file-line
364@g.command('show-file-line')
365def show_file_line(event):
366 c = event.get('c')
367 if not c:
368 return
369 w = c.frame.body.wrapper
370 if not w:
371 return
372 n0 = GoToCommands(c).find_node_start(p=c.p)
373 if n0 is None:
374 return
375 i = w.getInsertPoint()
376 s = w.getAllText()
377 row, col = g.convertPythonIndexToRowCol(s, i)
378 g.es_print(1 + n0 + row)
379#@-others
380#@-leo