Coverage for C:\Repos\leo-editor\leo\core\leoExternalFiles.py: 17%

370 statements  

« 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.20160306114544.1: * @file leoExternalFiles.py 

4#@@first 

5import getpass 

6import os 

7import subprocess 

8import tempfile 

9from leo.core import leoGlobals as g 

10#@+others 

11#@+node:ekr.20160306110233.1: ** class ExternalFile 

12class ExternalFile: 

13 """A class holding all data about an external file.""" 

14 

15 def __init__(self, c, ext, p, path, time): 

16 """Ctor for ExternalFile class.""" 

17 self.c = c 

18 self.ext = ext 

19 self.p = p and p.copy() # The nearest @<file> node. 

20 self.path = path 

21 # Inhibit endless dialog loop. See efc.idle_check_open_with_file. 

22 self.time = time 

23 

24 def __repr__(self): 

25 return f"<ExternalFile: {self.time:20} {g.shortFilename(self.path)}>" 

26 

27 __str__ = __repr__ 

28 #@+others 

29 #@+node:ekr.20161011174757.1: *3* ef.shortFileName 

30 def shortFileName(self): 

31 return g.shortFilename(self.path) 

32 #@+node:ekr.20161011174800.1: *3* ef.exists 

33 def exists(self): 

34 """Return True if the external file still exists.""" 

35 return g.os_path_exists(self.path) 

36 #@-others 

37#@+node:ekr.20150405073203.1: ** class ExternalFilesController 

38class ExternalFilesController: 

39 """ 

40 A class tracking changes to external files: 

41 

42 - temp files created by open-with commands. 

43 - external files corresponding to @file nodes. 

44 

45 This class raises a dialog when a file changes outside of Leo. 

46 

47 **Naming conventions**: 

48 

49 - d is always a dict created by the @open-with logic. 

50 This dict describes *only* how to open the file. 

51 

52 - ef is always an ExternalFiles instance. 

53 """ 

54 #@+others 

55 #@+node:ekr.20150404083533.1: *3* efc.ctor 

56 def __init__(self, c=None): 

57 """Ctor for ExternalFiles class.""" 

58 self.checksum_d = {} # Keys are full paths, values are file checksums. 

59 # For efc.on_idle. 

60 # Keys are commanders. 

61 # Values are cached @bool check-for-changed-external-file settings. 

62 self.enabled_d = {} 

63 # List of ExternalFile instances created by self.open_with. 

64 self.files = [] 

65 # Keys are commanders. Values are bools. 

66 # Used only to limit traces. 

67 self.has_changed_d = {} 

68 # Copy of g.app.commanders() 

69 self.unchecked_commanders = [] 

70 # Copy of self file. Only one files is checked at idle time. 

71 self.unchecked_files = [] 

72 # Keys are full paths, values are modification times. 

73 # DO NOT alter directly, use set_time(path) and 

74 # get_time(path), see set_time() for notes. 

75 self._time_d = {} 

76 self.yesno_all_answer = None # answer, 'yes-all', or 'no-all' 

77 g.app.idleTimeManager.add_callback(self.on_idle) 

78 #@+node:ekr.20150405105938.1: *3* efc.entries 

79 #@+node:ekr.20150405194745.1: *4* efc.check_overwrite (called from c.checkTimeStamp) 

80 def check_overwrite(self, c, path): 

81 """ 

82 Implements c.checkTimeStamp. 

83 

84 Return True if the file given by fn has not been changed 

85 since Leo read it or if the user agrees to overwrite it. 

86 """ 

87 if c.sqlite_connection and c.mFileName == path: 

88 # sqlite database file is never actually overwriten by Leo 

89 # so no need to check its timestamp. It is modified through 

90 # sqlite methods. 

91 return True 

92 if self.has_changed(path): 

93 val = self.ask(c, path) 

94 return val in ('yes', 'yes-all') # #1888 

95 return True 

96 #@+node:ekr.20031218072017.2613: *4* efc.destroy_frame 

97 def destroy_frame(self, frame): 

98 """ 

99 Close all "Open With" files associated with frame. 

100 Called by g.app.destroyWindow. 

101 """ 

102 files = [ef for ef in self.files if ef.c.frame == frame] 

103 paths = [ef.path for ef in files] 

104 for ef in files: 

105 self.destroy_temp_file(ef) 

106 self.files = [z for z in self.files if z.path not in paths] 

107 #@+node:ekr.20150407141838.1: *4* efc.find_path_for_node (called from vim.py) 

108 def find_path_for_node(self, p): 

109 """ 

110 Find the path corresponding to node p. 

111 called from vim.py. 

112 """ 

113 for ef in self.files: 

114 if ef.p and ef.p.v == p.v: 

115 path = ef.path 

116 break 

117 else: 

118 path = None 

119 return path 

120 #@+node:ekr.20150330033306.1: *4* efc.on_idle & helpers 

121 on_idle_count = 0 

122 

123 def on_idle(self): 

124 """ 

125 Check for changed open-with files and all external files in commanders 

126 for which @bool check_for_changed_external_file is True. 

127 """ 

128 # 

129 # #1240: Note: The "asking" dialog prevents idle time. 

130 # 

131 if not g.app or g.app.killed or g.app.restarting: # #1240. 

132 return 

133 self.on_idle_count += 1 

134 # New in Leo 5.7: always handle delayed requests. 

135 if g.app.windowList: 

136 c = g.app.log and g.app.log.c 

137 if c: 

138 c.outerUpdate() 

139 # Fix #262: Improve performance when @bool check-for-changed-external-files is True. 

140 if self.unchecked_files: 

141 # Check all external files. 

142 while self.unchecked_files: 

143 ef = self.unchecked_files.pop() # #1959: ensure progress. 

144 self.idle_check_open_with_file(c, ef) 

145 elif self.unchecked_commanders: 

146 # Check the next commander for which 

147 # @bool check_for_changed_external_file is True. 

148 c = self.unchecked_commanders.pop() 

149 self.idle_check_commander(c) 

150 else: 

151 # Add all commanders for which 

152 # @bool check_for_changed_external_file is True. 

153 self.unchecked_commanders = [ 

154 z for z in g.app.commanders() if self.is_enabled(z) 

155 ] 

156 self.unchecked_files = [z for z in self.files if z.exists()] 

157 #@+node:ekr.20150404045115.1: *5* efc.idle_check_commander 

158 def idle_check_commander(self, c): 

159 """ 

160 Check all external files corresponding to @<file> nodes in c for 

161 changes. 

162 """ 

163 # #1240: Check the .leo file itself. 

164 self.idle_check_leo_file(c) 

165 # 

166 # #1100: always scan the entire file for @<file> nodes. 

167 # #1134: Nested @<file> nodes are no longer valid, but this will do no harm. 

168 state = 'no' 

169 for p in c.all_unique_positions(): 

170 if not p.isAnyAtFileNode(): 

171 continue 

172 path = g.fullPath(c, p) 

173 if not self.has_changed(path): 

174 continue 

175 # Prevent further checks for path. 

176 self.set_time(path) 

177 self.checksum_d[path] = self.checksum(path) 

178 # Check file. 

179 if p.isAtAsisFileNode() or p.isAtNoSentFileNode(): 

180 # #1081: issue a warning. 

181 self.warn(c, path, p=p) 

182 continue 

183 if state in ('yes', 'no'): 

184 state = self.ask(c, path, p=p) 

185 if state in ('yes', 'yes-all'): 

186 c.redraw(p=p) 

187 c.refreshFromDisk(p) 

188 c.redraw() 

189 #@+node:ekr.20201207055713.1: *5* efc.idle_check_leo_file 

190 def idle_check_leo_file(self, c): 

191 """Check c's .leo file for external changes.""" 

192 path = c.fileName() 

193 if not self.has_changed(path): 

194 return 

195 # Always update the path & time to prevent future warnings. 

196 self.set_time(path) 

197 self.checksum_d[path] = self.checksum(path) 

198 # #1888: 

199 val = self.ask(c, path) 

200 if val in ('yes', 'yes-all'): 

201 # Do a complete restart of Leo. 

202 g.es_print('restarting Leo...') 

203 c.restartLeo() 

204 #@+node:ekr.20150407124259.1: *5* efc.idle_check_open_with_file & helper 

205 def idle_check_open_with_file(self, c, ef): 

206 """Update the open-with node given by ef.""" 

207 assert isinstance(ef, ExternalFile), ef 

208 if not ef.path or not os.path.exists(ef.path): 

209 return 

210 time = self.get_mtime(ef.path) 

211 if not time or time == ef.time: 

212 return 

213 # Inhibit endless dialog loop. 

214 ef.time = time 

215 # #1888: Handle all possible user responses to self.ask. 

216 val = self.ask(c, ef.path, p=ef.p.copy()) 

217 if val == 'yes-all': 

218 for ef in self.unchecked_files: 

219 self.update_open_with_node(ef) 

220 self.unchecked_files = [] 

221 elif val == 'no-all': 

222 self.unchecked_files = [] 

223 elif val == 'yes': 

224 self.update_open_with_node(ef) 

225 elif val == 'no': 

226 pass 

227 #@+node:ekr.20150407205631.1: *6* efc.update_open_with_node 

228 def update_open_with_node(self, ef): 

229 """Update the body text of ef.p to the contents of ef.path.""" 

230 assert isinstance(ef, ExternalFile), ef 

231 c, p = ef.c, ef.p.copy() 

232 g.blue(f"updated {p.h}") 

233 s, e = g.readFileIntoString(ef.path) 

234 p.b = s 

235 if c.config.getBool('open-with-goto-node-on-update'): 

236 c.selectPosition(p) 

237 if c.config.getBool('open-with-save-on-update'): 

238 c.save() 

239 else: 

240 p.setDirty() 

241 c.setChanged() 

242 #@+node:ekr.20150404082344.1: *4* efc.open_with & helpers 

243 def open_with(self, c, d): 

244 """ 

245 Called by c.openWith to handle items in the Open With... menu. 

246 

247 'd' a dict created from an @openwith settings node with these keys: 

248 

249 'args': the command-line arguments to be used to open the file. 

250 'ext': the file extension. 

251 'kind': the method used to open the file, such as subprocess.Popen. 

252 'name': menu label (used only by the menu code). 

253 'p': the nearest @<file> node, or None. 

254 'shortcut': menu shortcut (used only by the menu code). 

255 """ 

256 try: 

257 ext = d.get('ext') 

258 if not g.doHook('openwith1', c=c, p=c.p, v=c.p.v, d=d): 

259 root = d.get('p') 

260 if root: 

261 # Open the external file itself. 

262 path = g.fullPath(c, root) # #1914. 

263 self.open_file_in_external_editor(c, d, path) 

264 else: 

265 # Open a temp file containing just the node. 

266 p = c.p 

267 ext = self.compute_ext(c, p, ext) 

268 path = self.compute_temp_file_path(c, p, ext) 

269 if path: 

270 self.remove_temp_file(p, path) 

271 self.create_temp_file(c, ext, p) 

272 self.open_file_in_external_editor(c, d, path) 

273 g.doHook('openwith2', c=c, p=c.p, v=c.p.v, d=d) 

274 except Exception: 

275 g.es('unexpected exception in c.openWith') 

276 g.es_exception() 

277 #@+node:ekr.20031218072017.2824: *5* efc.compute_ext 

278 def compute_ext(self, c, p, ext): 

279 """Return the file extension to be used in the temp file.""" 

280 if ext: 

281 for ch in ("'", '"'): 

282 if ext.startswith(ch): 

283 ext = ext.strip(ch) 

284 if not ext: 

285 # if node is part of @<file> tree, get ext from file name 

286 for p2 in p.self_and_parents(copy=False): 

287 if p2.isAnyAtFileNode(): 

288 fn = p2.h.split(None, 1)[1] 

289 ext = g.os_path_splitext(fn)[1] 

290 break 

291 if not ext: 

292 theDict = c.scanAllDirectives(c.p) 

293 language = theDict.get('language') 

294 ext = g.app.language_extension_dict.get(language) 

295 if not ext: 

296 ext = '.txt' 

297 if ext[0] != '.': 

298 ext = '.' + ext 

299 return ext 

300 #@+node:ekr.20031218072017.2832: *5* efc.compute_temp_file_path & helpers 

301 def compute_temp_file_path(self, c, p, ext): 

302 """Return the path to the temp file for p and ext.""" 

303 if c.config.getBool('open-with-clean-filenames'): 

304 path = self.clean_file_name(c, ext, p) 

305 else: 

306 path = self.legacy_file_name(c, ext, p) 

307 if not path: 

308 g.error('c.temp_file_path failed') 

309 return path 

310 #@+node:ekr.20150406055221.2: *6* efc.clean_file_name 

311 def clean_file_name(self, c, ext, p): 

312 """Compute the file name when subdirectories mirror the node's hierarchy in Leo.""" 

313 use_extentions = c.config.getBool('open-with-uses-derived-file-extensions') 

314 ancestors, found = [], False 

315 for p2 in p.self_and_parents(copy=False): 

316 h = p2.anyAtFileNodeName() 

317 if not h: 

318 h = p2.h # Not an @file node: use the entire header 

319 elif use_extentions and not found: 

320 # Found the nearest ancestor @<file> node. 

321 found = True 

322 base, ext2 = g.os_path_splitext(h) 

323 if p2 == p: 

324 h = base 

325 if ext2: 

326 ext = ext2 

327 ancestors.append(g.sanitize_filename(h)) 

328 # The base directory is <tempdir>/Leo<id(v)>. 

329 ancestors.append("Leo" + str(id(p.v))) 

330 # Build temporary directories. 

331 td = os.path.abspath(tempfile.gettempdir()) 

332 while len(ancestors) > 1: 

333 td = os.path.join(td, ancestors.pop()) 

334 if not os.path.exists(td): 

335 os.mkdir(td) 

336 # Compute the full path. 

337 name = ancestors.pop() + ext 

338 path = os.path.join(td, name) 

339 return path 

340 #@+node:ekr.20150406055221.3: *6* efc.legacy_file_name 

341 def legacy_file_name(self, c, ext, p): 

342 """Compute a legacy file name for unsupported operating systems.""" 

343 try: 

344 leoTempDir = getpass.getuser() + "_" + "Leo" 

345 except Exception: 

346 leoTempDir = "LeoTemp" 

347 g.es("Could not retrieve your user name.") 

348 g.es(f"Temporary files will be stored in: {leoTempDir}") 

349 td = os.path.join(os.path.abspath(tempfile.gettempdir()), leoTempDir) 

350 if not os.path.exists(td): 

351 os.mkdir(td) 

352 name = g.sanitize_filename(p.h) + '_' + str(id(p.v)) + ext 

353 path = os.path.join(td, name) 

354 return path 

355 #@+node:ekr.20100203050306.5937: *5* efc.create_temp_file 

356 def create_temp_file(self, c, ext, p): 

357 """ 

358 Create the file used by open-with if necessary. 

359 Add the corresponding ExternalFile instance to self.files 

360 """ 

361 path = self.compute_temp_file_path(c, p, ext) 

362 exists = g.os_path_exists(path) 

363 # Compute encoding and s. 

364 d2 = c.scanAllDirectives(p) 

365 encoding = d2.get('encoding', None) 

366 if encoding is None: 

367 encoding = c.config.default_derived_file_encoding 

368 s = g.toEncodedString(p.b, encoding, reportErrors=True) 

369 # Write the file *only* if it doesn't exist. 

370 # No need to read the file: recomputing s above suffices. 

371 if not exists: 

372 try: 

373 with open(path, 'wb') as f: 

374 f.write(s) 

375 f.flush() 

376 except IOError: 

377 g.error(f"exception creating temp file: {path}") 

378 g.es_exception() 

379 return None 

380 # Add or update the external file entry. 

381 time = self.get_mtime(path) 

382 self.files = [z for z in self.files if z.path != path] 

383 self.files.append(ExternalFile(c, ext, p, path, time)) 

384 return path 

385 #@+node:ekr.20031218072017.2829: *5* efc.open_file_in_external_editor 

386 def open_file_in_external_editor(self, c, d, fn, testing=False): 

387 """ 

388 Open a file fn in an external editor. 

389 

390 This will be an entire external file, or a temp file for a single node. 

391 

392 d is a dictionary created from an @openwith settings node. 

393 

394 'args': the command-line arguments to be used to open the file. 

395 'ext': the file extension. 

396 'kind': the method used to open the file, such as subprocess.Popen. 

397 'name': menu label (used only by the menu code). 

398 'p': the nearest @<file> node, or None. 

399 'shortcut': menu shortcut (used only by the menu code). 

400 """ 

401 testing = testing or g.unitTesting 

402 arg_tuple = d.get('args', []) 

403 arg = ' '.join(arg_tuple) 

404 kind = d.get('kind') 

405 try: 

406 # All of these must be supported because they 

407 # could exist in @open-with nodes. 

408 command = '<no command>' 

409 if kind in ('os.system', 'os.startfile'): 

410 # New in Leo 5.7: 

411 # Use subProcess.Popen(..., shell=True) 

412 c_arg = self.join(arg, fn) 

413 if not testing: 

414 try: 

415 subprocess.Popen(c_arg, shell=True) 

416 except OSError: 

417 g.es_print('c_arg', repr(c_arg)) 

418 g.es_exception() 

419 elif kind == 'exec': 

420 g.es_print('open-with exec no longer valid.') 

421 elif kind == 'os.spawnl': 

422 filename = g.os_path_basename(arg) 

423 command = f"os.spawnl({arg},{filename},{fn})" 

424 if not testing: 

425 os.spawnl(os.P_NOWAIT, arg, filename, fn) 

426 elif kind == 'os.spawnv': 

427 filename = os.path.basename(arg_tuple[0]) 

428 vtuple = arg_tuple[1:] 

429 # add the name of the program as the first argument. 

430 # Change suggested by Jim Sizelove. 

431 vtuple.insert(0, filename) 

432 vtuple.append(fn) 

433 command = f"os.spawnv({vtuple})" 

434 if not testing: 

435 os.spawnv(os.P_NOWAIT, arg[0], vtuple) #??? 

436 elif kind == 'subprocess.Popen': 

437 c_arg = self.join(arg, fn) 

438 command = f"subprocess.Popen({c_arg})" 

439 if not testing: 

440 try: 

441 subprocess.Popen(c_arg, shell=True) 

442 except OSError: 

443 g.es_print('c_arg', repr(c_arg)) 

444 g.es_exception() 

445 elif hasattr(kind, '__call__'): 

446 # Invoke openWith like this: 

447 # c.openWith(data=[func,None,None]) 

448 # func will be called with one arg, the filename 

449 command = f"{kind}({fn})" 

450 if not testing: 

451 kind(fn) 

452 else: 

453 command = 'bad command:' + str(kind) 

454 if not testing: 

455 g.trace(command) 

456 return command # for unit testing. 

457 except Exception: 

458 g.es('exception executing open-with command:', command) 

459 g.es_exception() 

460 return f"oops: {command}" 

461 #@+node:ekr.20190123051253.1: *5* efc.remove_temp_file 

462 def remove_temp_file(self, p, path): 

463 """ 

464 Remove any existing *temp* file for p and path, updating self.files. 

465 """ 

466 for ef in self.files: 

467 if path and path == ef.path and p.v == ef.p.v: 

468 self.destroy_temp_file(ef) 

469 self.files = [z for z in self.files if z != ef] 

470 return 

471 #@+node:ekr.20150404092538.1: *4* efc.shut_down 

472 def shut_down(self): 

473 """ 

474 Destroy all temporary open-with files. 

475 This may fail if the files are still open. 

476 

477 Called by g.app.finishQuit. 

478 """ 

479 # Dont call g.es or g.trace! The log stream no longer exists. 

480 for ef in self.files[:]: 

481 self.destroy_temp_file(ef) 

482 self.files = [] 

483 #@+node:ekr.20150405110219.1: *3* efc.utilities 

484 # pylint: disable=no-value-for-parameter 

485 #@+node:ekr.20150405200212.1: *4* efc.ask 

486 def ask(self, c, path, p=None): 

487 """ 

488 Ask user whether to overwrite an @<file> tree. 

489 

490 Return one of ('yes', 'no', 'yes-all', 'no-all') 

491 """ 

492 if g.unitTesting: 

493 return False 

494 if c not in g.app.commanders(): 

495 return False 

496 is_leo = path.endswith(('.leo', '.db')) 

497 is_external_file = not is_leo 

498 # 

499 # Create the message. 

500 message1 = f"{g.splitLongFileName(path)} has changed outside Leo.\n" 

501 if is_leo: 

502 message2 = 'Restart Leo?' 

503 elif p: 

504 message2 = f"Reload {p.h}?" 

505 else: 

506 for ef in self.files: 

507 if ef.path == path: 

508 message2 = f"Reload {ef.p.h}?" 

509 break 

510 else: 

511 message2 = f"Reload {path}?" 

512 # 

513 # #1240: Note: This dialog prevents idle time. 

514 result = g.app.gui.runAskYesNoDialog(c, 

515 'Overwrite the version in Leo?', 

516 message1 + message2, 

517 yes_all=is_external_file, 

518 no_all=is_external_file, 

519 ) 

520 # 

521 # #1961. Re-init the checksum to suppress concurent dialogs. 

522 self.checksum_d[path] = self.checksum(path) 

523 # 

524 # #1888: return one of ('yes', 'no', 'yes-all', 'no-all') 

525 return result.lower() if result else 'no' 

526 #@+node:ekr.20150404052819.1: *4* efc.checksum 

527 def checksum(self, path): 

528 """Return the checksum of the file at the given path.""" 

529 import hashlib 

530 # #1454: Explicitly close the file. 

531 with open(path, 'rb') as f: 

532 s = f.read() 

533 return hashlib.md5(s).hexdigest() 

534 #@+node:ekr.20031218072017.2614: *4* efc.destroy_temp_file 

535 def destroy_temp_file(self, ef): 

536 """Destroy the *temp* file corresponding to ef, an ExternalFile instance.""" 

537 # Do not use g.trace here. 

538 if ef.path and g.os_path_exists(ef.path): 

539 try: 

540 os.remove(ef.path) 

541 except Exception: 

542 pass 

543 #@+node:ekr.20150407204201.1: *4* efc.get_mtime 

544 def get_mtime(self, path): 

545 """Return the modification time for the path.""" 

546 return g.os_path_getmtime(g.os_path_realpath(path)) 

547 #@+node:ekr.20150405122428.1: *4* efc.get_time 

548 def get_time(self, path): 

549 """ 

550 return timestamp for path 

551 

552 see set_time() for notes 

553 """ 

554 return self._time_d.get(g.os_path_realpath(path)) 

555 #@+node:ekr.20150403045207.1: *4* efc.has_changed 

556 def has_changed(self, path): 

557 """Return True if the file at path has changed outside of Leo.""" 

558 if not path: 

559 return False 

560 if not g.os_path_exists(path): 

561 return False 

562 if g.os_path_isdir(path): 

563 return False 

564 # 

565 # First, check the modification times. 

566 old_time = self.get_time(path) 

567 new_time = self.get_mtime(path) 

568 if not old_time: 

569 # Initialize. 

570 self.set_time(path, new_time) 

571 self.checksum_d[path] = self.checksum(path) 

572 return False 

573 if old_time == new_time: 

574 return False 

575 # 

576 # Check the checksums *only* if the mod times don't match. 

577 old_sum = self.checksum_d.get(path) 

578 new_sum = self.checksum(path) 

579 if new_sum == old_sum: 

580 # The modtime changed, but it's contents didn't. 

581 # Update the time, so we don't keep checking the checksums. 

582 # Return False so we don't prompt the user for an update. 

583 self.set_time(path, new_time) 

584 return False 

585 # The file has really changed. 

586 assert old_time, path 

587 return True 

588 #@+node:ekr.20150405104340.1: *4* efc.is_enabled 

589 def is_enabled(self, c): 

590 """Return the cached @bool check_for_changed_external_file setting.""" 

591 d = self.enabled_d 

592 val = d.get(c) 

593 if val is None: 

594 val = c.config.getBool('check-for-changed-external-files', default=False) 

595 d[c] = val 

596 return val 

597 #@+node:ekr.20150404083049.1: *4* efc.join 

598 def join(self, s1, s2): 

599 """Return s1 + ' ' + s2""" 

600 return f"{s1} {s2}" 

601 #@+node:tbrown.20150904102518.1: *4* efc.set_time 

602 def set_time(self, path, new_time=None): 

603 """ 

604 Implements c.setTimeStamp. 

605 

606 Update the timestamp for path. 

607 

608 NOTE: file paths with symbolic links occur with and without those links 

609 resolved depending on the code call path. This inconsistency is 

610 probably not Leo's fault but an underlying Python issue. 

611 Hence the need to call realpath() here. 

612 """ 

613 t = new_time or self.get_mtime(path) 

614 self._time_d[g.os_path_realpath(path)] = t 

615 #@+node:ekr.20190218055230.1: *4* efc.warn 

616 def warn(self, c, path, p): 

617 """ 

618 Warn that an @asis or @nosent node has been changed externally. 

619 

620 There is *no way* to update the tree automatically. 

621 """ 

622 if g.unitTesting or c not in g.app.commanders(): 

623 return 

624 if not p: 

625 g.trace('NO P') 

626 return 

627 g.app.gui.runAskOkDialog( 

628 c=c, 

629 message='\n'.join([ 

630 f"{g.splitLongFileName(path)} has changed outside Leo.\n", 

631 'Leo can not update this file automatically.\n', 

632 f"This file was created from {p.h}.\n", 

633 'Warning: refresh-from-disk will destroy all children.' 

634 ]), 

635 title='External file changed', 

636 ) 

637 #@-others 

638#@-others 

639#@@language python 

640#@@tabwidth -4 

641#@@pagewidth 70 

642#@-leo