--- a/src/eric7/QScintilla/Editor.py Fri Feb 02 14:55:14 2024 +0100 +++ b/src/eric7/QScintilla/Editor.py Mon Feb 05 11:15:47 2024 +0100 @@ -68,6 +68,7 @@ from eric7.EricWidgets.EricApplication import ericApp from eric7.Globals import recentNameBreakpointConditions from eric7.SystemUtilities import FileSystemUtilities, OSUtilities, PythonUtilities +from eric7.RemoteServerInterface import EricServerFileDialog from eric7.UI import PythonDisViewer from eric7.Utilities import MouseUtilities @@ -264,6 +265,11 @@ self.project = ericApp().getObject("Project") self.setFileName(fn) + self.__remotefsInterface = ericApp().getObject( + "EricServer" + ).getServiceInterface("FileSystem") + + # clear some variables self.lastHighlight = None # remember the last highlighted line self.lastErrorMarker = None # remember the last error line @@ -478,11 +484,10 @@ if not res: raise OSError() - self.readFile(self.fileName, True) + self.readFile(self.fileName, createIt=True) elif FileSystemUtilities.isRemoteFileName(self.fileName): - # TODO: read the remote file from the server - pass + self.readFile(self.fileName, createIt=True, isRemote=True) self.__bindLexer(self.fileName) self.__bindCompleter(self.fileName) @@ -2160,8 +2165,8 @@ @param m modification status @type bool """ - if not m and bool(self.fileName) and pathlib.Path(self.fileName).exists(): - self.lastModified = pathlib.Path(self.fileName).stat().st_mtime + if not m: + self.recordModificationTime() self.modificationStatusChanged.emit(m, self) self.undoAvailable.emit(self.isUndoAvailable()) self.redoAvailable.emit(self.isRedoAvailable()) @@ -3489,7 +3494,7 @@ break # Couldn't find the unmodified state - def readFile(self, fn, createIt=False, encoding="", noempty=False): + def readFile(self, fn, createIt=False, encoding="", noempty=False, isRemote=False): """ Public method to read the text from a file. @@ -3503,26 +3508,44 @@ @type str (optional) @param noempty flag indicating to not set an empty text (defaults to False) @type bool (optional) - """ - self.__loadEditorConfig(fileName=fn) + @param isRemote flag indicating a remote file (defaults to False) + @type bool (optional) + """ + if FileSystemUtilities.isPlainFileName(fn): + self.__loadEditorConfig(fileName=fn) try: with EricOverrideCursor(): - if createIt and not os.path.exists(fn): - with open(fn, "w"): - pass - if encoding == "": - encoding = self.__getEditorConfig("DefaultEncoding", nodefault=True) - if encoding: - txt, self.encoding = Utilities.readEncodedFileWithEncoding( - fn, encoding + if FileSystemUtilities.isRemoteFileName(fn) or isRemote: + title = self.tr("Open Remote File") + bText = self.__remotefsInterface.readFile( + FileSystemUtilities.plainFileName(fn), create=True ) + if encoding: + txt, self.encoding = Utilities.decodeWithEncoding( + bText, encoding + ) + else: + txt, self.encoding = Utilities.decode(bText) else: - txt, self.encoding = Utilities.readEncodedFile(fn) + title = self.tr("Open File") + if createIt and not os.path.exists(fn): + with open(fn, "w"): + pass + if encoding == "": + encoding = self.__getEditorConfig( + "DefaultEncoding", nodefault=True + ) + if encoding: + txt, self.encoding = Utilities.readEncodedFileWithEncoding( + fn, encoding + ) + else: + txt, self.encoding = Utilities.readEncodedFile(fn) except (OSError, UnicodeDecodeError) as why: EricMessageBox.critical( self.vm, - self.tr("Open File"), + title, self.tr( "<p>The file <b>{0}</b> could not be opened.</p>" "<p>Reason: {1}</p>" @@ -3552,7 +3575,7 @@ self.extractTasks() - self.lastModified = pathlib.Path(fn).stat().st_mtime + self.recordModificationTime(filename=fn) @pyqtSlot() def __convertTabs(self): @@ -3604,7 +3627,10 @@ @return flag indicating success @rtype bool """ - config = self.__loadEditorConfigObject(fn) + if self.fileName and fn == self.fileName: + config = None + else: + config = self.__loadEditorConfigObject(fn) eol = self.__getEditorConfig("EOLMode", nodefault=True, config=config) if eol is not None: @@ -3626,7 +3652,7 @@ # create a backup file, if the option is set createBackup = backup and Preferences.getEditor("CreateBackupFile") - if createBackup: + if createBackup and FileSystemUtilities.isPlainFileName(fn): if os.path.islink(fn): fn = os.path.realpath(fn) bfn = "{0}~".format(fn) @@ -3646,28 +3672,40 @@ editorConfigEncoding = self.__getEditorConfig( "DefaultEncoding", nodefault=True, config=config ) - self.encoding = Utilities.writeEncodedFile( - fn, txt, self.encoding, forcedEncoding=editorConfigEncoding - ) - if createBackup and perms_valid: - os.chmod(fn, permissions) + if FileSystemUtilities.isPlainFileName(fn): + title = self.tr("Save File") + self.encoding = Utilities.writeEncodedFile( + fn, txt, self.encoding, forcedEncoding=editorConfigEncoding + ) + if createBackup and perms_valid: + os.chmod(fn, permissions) + else: + title = self.tr("Save Remote File") + bText, self.encoding = Utilities.encode( + txt, self.encoding, forcedEncoding=editorConfigEncoding + ) + self.__remotefsInterface.writeFile( + FileSystemUtilities.plainFileName(fn), bText, createBackup + ) return True except (OSError, UnicodeError, Utilities.CodingError) as why: EricMessageBox.critical( self, - self.tr("Save File"), + title, self.tr( "<p>The file <b>{0}</b> could not be saved.<br/>Reason: {1}</p>" ).format(fn, str(why)), ) return False - def __getSaveFileName(self, path=None): + def __getSaveFileName(self, path=None, remote=False): """ Private method to get the name of the file to be saved. - @param path directory to save the file in - @type str + @param path directory to save the file in (defaults to None) + @type str (optional) + @param remote flag indicating to save as a remote file (defaults to False) + @type bool (optional) @return file name @rtype str """ @@ -3681,7 +3719,13 @@ if not path and self.fileName: path = os.path.dirname(self.fileName) if not path: - path = Preferences.getMultiProject("Workspace") or OSUtilities.getHomeDir() + if remote: + path = "" + else: + path = ( + Preferences.getMultiProject("Workspace") + or OSUtilities.getHomeDir() + ) if self.fileName: filterPattern = "(*{0})".format(os.path.splitext(self.fileName)[1]) @@ -3693,14 +3737,26 @@ defaultFilter = Preferences.getEditor("DefaultSaveFilter") else: defaultFilter = Preferences.getEditor("DefaultSaveFilter") - fn, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( - self, - self.tr("Save File"), - path, - Lexers.getSaveFileFiltersList(True, True), - defaultFilter, - EricFileDialog.DontConfirmOverwrite, - ) + + if remote or FileSystemUtilities.isRemoteFileName(path): + title = self.tr("Save Remote File") + fn, selectedFilter = EricServerFileDialog.getSaveFileNameAndFilter( + self, + title, + path, + Lexers.getSaveFileFiltersList(True, True), + defaultFilter, + ) + else: + title = self.tr("Save File") + fn, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( + self, + title, + path, + Lexers.getSaveFileFiltersList(True, True), + defaultFilter, + EricFileDialog.DontConfirmOverwrite, + ) if fn: if fn.endswith("."): @@ -3711,10 +3767,16 @@ ex = selectedFilter.split("(*")[1].split(")")[0] if ex: fpath = fpath.with_suffix(ex) - if fpath.exists(): + if ( + ( + FileSystemUtilities.isRemoteFileName(str(fpath)) + and self.__remotefsInterface.exists(str(fpath)) + ) + or (FileSystemUtilities.isPlainFileName(str(fpath)) and fpath.exists()) + ): res = EricMessageBox.yesNo( self, - self.tr("Save File"), + title, self.tr( "<p>The file <b>{0}</b> already exists. Overwrite it?</p>" ).format(fpath), @@ -3747,21 +3809,21 @@ return res - def saveFile(self, saveas=False, path=None): + def saveFile(self, saveas=False, path=None, remote=False): """ Public method to save the text to a file. - @param saveas flag indicating a 'save as' action - @type bool - @param path directory to save the file in - @type str + @param saveas flag indicating a 'save as' action (defaults to False) + @type bool (optional) + @param path directory to save the file in (defaults to None) + @type str (optional) + @param remote flag indicating to save as a remote file (defaults to False) + @type bool (optional) @return flag indicating success @rtype bool """ - if not saveas and ( - not self.isModified() or FileSystemUtilities.isRemoteFileName(self.fileName) - ): - # do nothing if text wasn't changed or is a remote file + if not saveas and not self.isModified(): + # do nothing if text was not changed return False if FileSystemUtilities.isDeviceFileName(self.fileName): @@ -3771,7 +3833,7 @@ if saveas or self.fileName == "": saveas = True - fn = self.__getSaveFileName(path) + fn = self.__getSaveFileName(path=path, remote=remote) if not fn: return False @@ -3790,13 +3852,13 @@ else: fn = self.fileName - self.__loadEditorConfig(fn) self.editorAboutToBeSaved.emit(self.fileName) if self.__autosaveTimer.isActive(): self.__autosaveTimer.stop() if self.writeFile(fn): if saveas: self.__clearBreakpoints(self.fileName) + self.__loadEditorConfig(fileName=fn) self.setFileName(fn) self.setModified(False) self.setReadOnly(False) @@ -3818,7 +3880,7 @@ self.setLanguage(self.fileName) - self.lastModified = pathlib.Path(fn).stat().st_mtime + self.recordModificationTime() if newName is not None: self.vm.addToRecentList(newName) self.editorSaved.emit(self.fileName) @@ -3828,21 +3890,21 @@ self.__checkEncoding() return True else: - self.lastModified = ( - pathlib.Path(fn).stat().st_mtime if pathlib.Path(fn).exists() else 0 - ) + self.recordModificationTime(filename=fn) return False - def saveFileAs(self, path=None): + def saveFileAs(self, path=None, remote=False): """ Public method to save a file with a new name. - @param path directory to save the file in - @type str + @param path directory to save the file in (defaults to None) + @type str (optional) + @param remote flag indicating to save as a remote file (defaults to False) + @type bool (optional) @return tuple containing a success indicator and the name of the saved file @rtype tuple of (bool, str) """ - return self.saveFile(True, path) + return self.saveFile(True, path=path, remote=remote) def __saveDeviceFile(self, saveas=False): """ @@ -3903,7 +3965,7 @@ if self.lexer_ is None: self.setLanguage(self.fileName) - self.lastModified = pathlib.Path(fn).stat().st_mtime + self.recordModificationTime() self.vm.setEditorName(self, self.fileName) self.__updateReadOnly(True) @@ -8019,6 +8081,15 @@ self.__updateReadOnly(False) self.setCursorFlashTime(QApplication.cursorFlashTime()) + if ( + self.fileName + and FileSystemUtilities.isRemoteFileName(self.fileName) + and not self.inReopenPrompt + ): + self.inReopenPrompt = True + self.checkRereadFile() + self.inReopenPrompt = False + super().focusInEvent(event) def focusOutEvent(self, event): @@ -8028,7 +8099,11 @@ @param event the event object @type QFocusEvent """ - if Preferences.getEditor("AutosaveOnFocusLost") and self.__shouldAutosave(): + if ( + Preferences.getEditor("AutosaveOnFocusLost") + and self.__shouldAutosave() + and not self.inReopenPrompt + ): self.saveFile() self.vm.editorActGrp.setEnabled(False) @@ -8201,7 +8276,7 @@ signal if there was an attribute change. @type bool """ - if self.fileName == "" or not FileSystemUtilities.isPlainFileName( + if self.fileName == "" or FileSystemUtilities.isDeviceFileName( self.fileName ): return @@ -8225,9 +8300,17 @@ @rtype bool """ return ( - FileSystemUtilities.isPlainFileName(self.fileName) - and not os.access(self.fileName, os.W_OK) - ) or self.isReadOnly() + ( + FileSystemUtilities.isPlainFileName(self.fileName) + and not os.access(self.fileName, os.W_OK) + ) + or ( + FileSystemUtilities.isRemoteFileName(self.fileName) + and not self.__remotefsInterface.access( + FileSystemUtilities.plainFileName(self.fileName), "write") + ) + or self.isReadOnly() + ) @pyqtSlot() def checkRereadFile(self): @@ -8235,13 +8318,9 @@ Public slot to check, if the file needs to be re-read, and refresh it if needed. """ - if ( - self.fileName - and pathlib.Path(self.fileName).exists() - and pathlib.Path(self.fileName).stat().st_mtime != self.lastModified - ): + if self.checkModificationTime(): if Preferences.getEditor("AutoReopen") and not self.isModified(): - self.refresh() + self.__refresh() else: msg = self.tr( """<p>The file <b>{0}</b> has been changed while it""" @@ -8262,23 +8341,74 @@ yesDefault=yesDefault, ) if res: - self.refresh() + self.__refresh() else: # do not prompt for this change again... - self.lastModified = pathlib.Path(self.fileName).stat().st_mtime - - @pyqtSlot() - def recordModificationTime(self): + self.recordModificationTime() + + @pyqtSlot() + def recordModificationTime(self, filename=""): """ Public slot to record the modification time of our file. - """ - if self.fileName and pathlib.Path(self.fileName).exists(): - self.lastModified = pathlib.Path(self.fileName).stat().st_mtime - - @pyqtSlot() - def refresh(self): - """ - Public slot to refresh the editor contents. + + @param filename name of the file to record the modification tome for + (defaults to "") + @type str (optional) + """ + if not filename: + filename = self.fileName + + if filename: + if FileSystemUtilities.isRemoteFileName(filename): + filename = FileSystemUtilities.plainFileName(filename) + if self.__remotefsInterface.exists(filename): + mtime = self.__remotefsInterface.stat( + FileSystemUtilities.plainFileName(filename), ["st_mtime"] + )["st_mtime"] + self.lastModified = mtime if mtime is not None else 0 + else: + self.lastModified = 0 + elif pathlib.Path(filename).exists(): + self.lastModified = pathlib.Path(filename).stat().st_mtime + else: + self.lastModified = 0 + + else: + self.lastModified = 0 + + def checkModificationTime(self, filename=""): + """ + Public method to check, if the modification time of the file is different + from the recorded one. + + @param filename name of the file to check against (defaults to "") + @type str (optional) + @return flag indicating that the file modification time is different. For + non-existent files a 'False' value will be reported. + @rtype bool + """ + if not filename: + filename = self.fileName + + if filename: + if FileSystemUtilities.isRemoteFileName(filename): + plainFilename = FileSystemUtilities.plainFileName(filename) + if self.__remotefsInterface.exists(plainFilename): + mtime = self.__remotefsInterface.stat( + plainFilename, ["st_mtime"] + )["st_mtime"] + return mtime != self.lastModified + + elif FileSystemUtilities.isPlainFileName(filename): + if pathlib.Path(filename).exists(): + return pathlib.Path(filename).stat().st_mtime != self.lastModified + + return False + + @pyqtSlot() + def __refresh(self): + """ + Private slot to refresh the editor contents. """ # save cursor position cline, cindex = self.getCursorPosition() @@ -8298,11 +8428,6 @@ self.markerDeleteHandle(handle) self.breaks.clear() - if not os.path.exists(self.fileName): - # close the file, if it was deleted in the background - self.close() - return - # reread the file try: self.readFile(self.fileName, noempty=True) @@ -9703,7 +9828,7 @@ """ editorConfig = {} - if fileName and FileSystemUtilities.isPlainFileName(self.fileName): + if fileName and FileSystemUtilities.isPlainFileName(fileName): try: editorConfig = editorconfig.get_properties(fileName) except editorconfig.EditorConfigError: @@ -9741,16 +9866,16 @@ if config is None: config = self.__editorConfig - if not config: - if nodefault: - return None - else: - value = self.__getOverrideValue(option) - if value is None: - # no override - value = Preferences.getEditor(option) - return value - + ##if not config: + ##if nodefault: + ##return None + ##else: + ##value = self.__getOverrideValue(option) + ##if value is None: + ### no override + ##value = Preferences.getEditor(option) + ##return value +## try: if option == "EOLMode": value = config["end_of_line"]