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.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() 

20 # The nearest @<file> node. 

21 self.path = path 

22 self.time = time # Used to inhibit endless dialog loop. 

23 # See efc.idle_check_open_with_file. 

24 

25 def __repr__(self): 

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

27 

28 __str__ = __repr__ 

29 #@+others 

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

31 def shortFileName(self): 

32 return g.shortFilename(self.path) 

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

34 def exists(self): 

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

36 return g.os_path_exists(self.path) 

37 #@-others 

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

39class ExternalFilesController: 

40 """ 

41 A class tracking changes to external files: 

42 

43 - temp files created by open-with commands. 

44 - external files corresponding to @file nodes. 

45 

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

47 

48 **Naming conventions**: 

49 

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

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

52 

53 - ef is always an ExternalFiles instance. 

54 """ 

55 #@+others 

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

57 def __init__(self, c=None): 

58 """Ctor for ExternalFiles class.""" 

59 self.checksum_d = {} 

60 # Keys are full paths, values are file checksums. 

61 self.enabled_d = {} 

62 # For efc.on_idle. 

63 # Keys are commanders. 

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

65 self.files = [] 

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

67 self.has_changed_d = {} 

68 # Keys are commanders. Values are bools. 

69 # Used only to limit traces. 

70 self.unchecked_commanders = [] 

71 # Copy of g.app.commanders() 

72 self.unchecked_files = [] 

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

74 self._time_d = {} 

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

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

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

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

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

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

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

82 def check_overwrite(self, c, path): 

83 """ 

84 Implements c.checkTimeStamp. 

85 

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

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

88 """ 

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

90 # sqlite database file is never actually overwriten by Leo 

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

92 # sqlite methods. 

93 return True 

94 if self.has_changed(path): 

95 val = self.ask(c, path) 

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

97 return True 

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

99 def destroy_frame(self, frame): 

100 """ 

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

102 Called by g.app.destroyWindow. 

103 """ 

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

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

106 for ef in files: 

107 self.destroy_temp_file(ef) 

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

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

110 def find_path_for_node(self, p): 

111 """ 

112 Find the path corresponding to node p. 

113 called from vim.py. 

114 """ 

115 for ef in self.files: 

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

117 path = ef.path 

118 break 

119 else: 

120 path = None 

121 return path 

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

123 on_idle_count = 0 

124 

125 def on_idle(self): 

126 """ 

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

128 for which @bool check_for_changed_external_file is True. 

129 """ 

130 # 

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

132 # 

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

134 return 

135 self.on_idle_count += 1 

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

137 if g.app.windowList: 

138 c = g.app.log and g.app.log.c 

139 if c: 

140 c.outerUpdate() 

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

142 if self.unchecked_files: 

143 # Check all external files. 

144 while self.unchecked_files: 

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

146 self.idle_check_open_with_file(c, ef) 

147 elif self.unchecked_commanders: 

148 # Check the next commander for which 

149 # @bool check_for_changed_external_file is True. 

150 c = self.unchecked_commanders.pop() 

151 self.idle_check_commander(c) 

152 else: 

153 # Add all commanders for which 

154 # @bool check_for_changed_external_file is True. 

155 self.unchecked_commanders = [ 

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

157 ] 

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

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

160 def idle_check_commander(self, c): 

161 """ 

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

163 changes. 

164 """ 

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

166 self.idle_check_leo_file(c) 

167 # 

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

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

170 state = 'no' 

171 for p in c.all_unique_positions(): 

172 if not p.isAnyAtFileNode(): 

173 continue 

174 path = g.fullPath(c, p) 

175 if not self.has_changed(path): 

176 continue 

177 # Prevent further checks for path. 

178 self.set_time(path) 

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

180 # Check file. 

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

182 # #1081: issue a warning. 

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

184 continue 

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

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

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

188 c.redraw(p=p) 

189 c.refreshFromDisk(p) 

190 c.redraw() 

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

192 def idle_check_leo_file(self, c): 

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

194 path = c.fileName() 

195 if not self.has_changed(path): 

196 return 

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

198 self.set_time(path) 

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

200 # #1888: 

201 val = self.ask(c, path) 

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

203 # Do a complete restart of Leo. 

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

205 c.restartLeo() 

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

207 def idle_check_open_with_file(self, c, ef): 

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

209 assert isinstance(ef, ExternalFile), ef 

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

211 return 

212 time = self.get_mtime(ef.path) 

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

214 return 

215 # Inhibit endless dialog loop. 

216 ef.time = time 

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

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

219 if val == 'yes-all': 

220 for ef in self.unchecked_files: 

221 self.update_open_with_node(ef) 

222 self.unchecked_files = [] 

223 elif val == 'no-all': 

224 self.unchecked_files = [] 

225 elif val == 'yes': 

226 self.update_open_with_node(ef) 

227 elif val == 'no': 

228 pass 

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

230 def update_open_with_node(self, ef): 

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

232 assert isinstance(ef, ExternalFile), ef 

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

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

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

236 p.b = s 

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

238 c.selectPosition(p) 

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

240 c.save() 

241 else: 

242 p.setDirty() 

243 c.setChanged() 

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

245 def open_with(self, c, d): 

246 """ 

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

248 

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

250 

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

252 'ext': the file extension. 

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

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

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

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

257 """ 

258 try: 

259 ext = d.get('ext') 

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

261 root = d.get('p') 

262 if root: 

263 # Open the external file itself. 

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

265 self.open_file_in_external_editor(c, d, path) 

266 else: 

267 # Open a temp file containing just the node. 

268 p = c.p 

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

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

271 if path: 

272 self.remove_temp_file(p, path) 

273 self.create_temp_file(c, ext, p) 

274 self.open_file_in_external_editor(c, d, path) 

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

276 except Exception: 

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

278 g.es_exception() 

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

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

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

282 if ext: 

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

284 if ext.startswith(ch): 

285 ext = ext.strip(ch) 

286 if not ext: 

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

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

289 if p2.isAnyAtFileNode(): 

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

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

292 break 

293 if not ext: 

294 theDict = c.scanAllDirectives(c.p) 

295 language = theDict.get('language') 

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

297 if not ext: 

298 ext = '.txt' 

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

300 ext = '.' + ext 

301 return ext 

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

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

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

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

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

307 else: 

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

309 if not path: 

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

311 return path 

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

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

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

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

316 ancestors, found = [], False 

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

318 h = p2.anyAtFileNodeName() 

319 if not h: 

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

321 elif use_extentions and not found: 

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

323 found = True 

324 base, ext2 = g.os_path_splitext(h) 

325 if p2 == p: 

326 h = base 

327 if ext2: 

328 ext = ext2 

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

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

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

332 # Build temporary directories. 

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

334 while len(ancestors) > 1: 

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

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

337 os.mkdir(td) 

338 # Compute the full path. 

339 name = ancestors.pop() + ext 

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

341 return path 

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

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

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

345 try: 

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

347 except Exception: 

348 leoTempDir = "LeoTemp" 

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

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

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

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

353 os.mkdir(td) 

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

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

356 return path 

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

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

359 """ 

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

361 Add the corresponding ExternalFile instance to self.files 

362 """ 

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

364 exists = g.os_path_exists(path) 

365 # Compute encoding and s. 

366 d2 = c.scanAllDirectives(p) 

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

368 if encoding is None: 

369 encoding = c.config.default_derived_file_encoding 

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

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

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

373 if not exists: 

374 try: 

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

376 f.write(s) 

377 f.flush() 

378 except IOError: 

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

380 g.es_exception() 

381 return None 

382 # Add or update the external file entry. 

383 time = self.get_mtime(path) 

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

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

386 return path 

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

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

389 """ 

390 Open a file fn in an external editor. 

391 

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

393 

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

395 

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

397 'ext': the file extension. 

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

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

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

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

402 """ 

403 testing = testing or g.unitTesting 

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

405 arg = ' '.join(arg_tuple) 

406 kind = d.get('kind') 

407 try: 

408 # All of these must be supported because they 

409 # could exist in @open-with nodes. 

410 command = '<no command>' 

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

412 # New in Leo 5.7: 

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

414 c_arg = self.join(arg, fn) 

415 if not testing: 

416 try: 

417 subprocess.Popen(c_arg, shell=True) 

418 except OSError: 

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

420 g.es_exception() 

421 elif kind == 'exec': 

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

423 elif kind == 'os.spawnl': 

424 filename = g.os_path_basename(arg) 

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

426 if not testing: 

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

428 elif kind == 'os.spawnv': 

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

430 vtuple = arg_tuple[1:] 

431 vtuple.insert(0, filename) 

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

433 # Change suggested by Jim Sizelove. 

434 vtuple.append(fn) 

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

436 if not testing: 

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

438 elif kind == 'subprocess.Popen': 

439 c_arg = self.join(arg, fn) 

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

441 if not testing: 

442 try: 

443 subprocess.Popen(c_arg, shell=True) 

444 except OSError: 

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

446 g.es_exception() 

447 elif hasattr(kind, '__call__'): 

448 # Invoke openWith like this: 

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

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

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

452 if not testing: 

453 kind(fn) 

454 else: 

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

456 if not testing: 

457 g.trace(command) 

458 return command # for unit testing. 

459 except Exception: 

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

461 g.es_exception() 

462 return f"oops: {command}" 

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

464 def remove_temp_file(self, p, path): 

465 """ 

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

467 """ 

468 for ef in self.files: 

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

470 self.destroy_temp_file(ef) 

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

472 return 

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

474 def shut_down(self): 

475 """ 

476 Destroy all temporary open-with files. 

477 This may fail if the files are still open. 

478 

479 Called by g.app.finishQuit. 

480 """ 

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

482 for ef in self.files[:]: 

483 self.destroy_temp_file(ef) 

484 self.files = [] 

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

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

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

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

489 """ 

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

491 

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

493 """ 

494 if g.unitTesting: 

495 return False 

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

497 return False 

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

499 is_external_file = not is_leo 

500 # 

501 # Create the message. 

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

503 if is_leo: 

504 message2 = 'Restart Leo?' 

505 elif p: 

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

507 else: 

508 for ef in self.files: 

509 if ef.path == path: 

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

511 break 

512 else: 

513 message2 = f"Reload {path}?" 

514 # 

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

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

517 'Overwrite the version in Leo?', 

518 message1 + message2, 

519 yes_all=is_external_file, 

520 no_all=is_external_file, 

521 ) 

522 # 

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

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

525 # 

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

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

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

529 def checksum(self, path): 

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

531 import hashlib 

532 # #1454: Explicitly close the file. 

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

534 s = f.read() 

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

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

537 def destroy_temp_file(self, ef): 

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

539 # Do not use g.trace here. 

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

541 try: 

542 os.remove(ef.path) 

543 except Exception: 

544 pass 

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

546 def get_mtime(self, path): 

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

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

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

550 def get_time(self, path): 

551 """ 

552 return timestamp for path 

553 

554 see set_time() for notes 

555 """ 

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

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

558 def has_changed(self, path): 

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

560 if not path: 

561 return False 

562 if not g.os_path_exists(path): 

563 return False 

564 if g.os_path_isdir(path): 

565 return False 

566 # 

567 # First, check the modification times. 

568 old_time = self.get_time(path) 

569 new_time = self.get_mtime(path) 

570 if not old_time: 

571 # Initialize. 

572 self.set_time(path, new_time) 

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

574 return False 

575 if old_time == new_time: 

576 return False 

577 # 

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

579 old_sum = self.checksum_d.get(path) 

580 new_sum = self.checksum(path) 

581 if new_sum == old_sum: 

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

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

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

585 self.set_time(path, new_time) 

586 return False 

587 # The file has really changed. 

588 assert old_time, path 

589 return True 

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

591 def is_enabled(self, c): 

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

593 d = self.enabled_d 

594 val = d.get(c) 

595 if val is None: 

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

597 d[c] = val 

598 return val 

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

600 def join(self, s1, s2): 

601 """Return s1 + ' ' + s2""" 

602 return f"{s1} {s2}" 

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

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

605 """ 

606 Implements c.setTimeStamp. 

607 

608 Update the timestamp for path. 

609 

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

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

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

613 Hence the need to call realpath() here. 

614 """ 

615 t = new_time or self.get_mtime(path) 

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

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

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

619 """ 

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

621 

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

623 """ 

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

625 return 

626 if not p: 

627 g.trace('NO P') 

628 return 

629 g.app.gui.runAskOkDialog( 

630 c=c, 

631 message='\n'.join([ 

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

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

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

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

636 ]), 

637 title='External file changed', 

638 ) 

639 #@-others 

640#@-others 

641#@@language python 

642#@@tabwidth -4 

643#@@pagewidth 70 

644#@-leo