Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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. 

130 

131 Only non-sentinel lines increment the global line count, but 

132 @+node sentinels reset the offset within the node. 

133 

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. 

179 

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. 

284 

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