--- a/src/eric7/QScintilla/Editor.py Sun Jun 02 09:51:47 2024 +0200 +++ b/src/eric7/QScintilla/Editor.py Wed Jul 03 09:20:41 2024 +0200 @@ -57,7 +57,7 @@ QToolTip, ) -from eric7 import Globals, Preferences, Utilities +from eric7 import EricUtilities, Globals, Preferences, Utilities from eric7.CodeFormatting.BlackFormattingAction import BlackFormattingAction from eric7.CodeFormatting.BlackUtilities import aboutBlack from eric7.CodeFormatting.IsortFormattingAction import IsortFormattingAction @@ -68,6 +68,7 @@ from eric7.EricWidgets import EricFileDialog, EricMessageBox from eric7.EricWidgets.EricApplication import ericApp from eric7.Globals import recentNameBreakpointConditions +from eric7.RemoteServerInterface import EricServerFileDialog from eric7.SystemUtilities import FileSystemUtilities, OSUtilities, PythonUtilities from eric7.UI import PythonDisViewer from eric7.Utilities import MouseUtilities @@ -265,6 +266,10 @@ 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 @@ -445,11 +450,20 @@ if not Utilities.MimeTypes.isTextFile(self.fileName): raise OSError() - if ( - FileSystemUtilities.isPlainFileName(self.fileName) - and pathlib.Path(self.fileName).exists() - ): + if FileSystemUtilities.isRemoteFileName(self.fileName): + fileIsRemote = True + fileExists = self.__remotefsInterface.exists(self.fileName) + fileSizeKB = ( + self.__remotefsInterface.stat(self.fileName, ["st_size"])[ + "st_size" + ] + // 1024 + ) + else: + fileIsRemote = False + fileExists = pathlib.Path(self.fileName).exists() fileSizeKB = pathlib.Path(self.fileName).stat().st_size // 1024 + if fileExists: if fileSizeKB > Preferences.getEditor("RejectFilesize"): EricMessageBox.warning( None, @@ -479,7 +493,7 @@ if not res: raise OSError() - self.readFile(self.fileName, True) + self.readFile(self.fileName, createIt=True, isRemote=fileIsRemote) self.__bindLexer(self.fileName) self.__bindCompleter(self.fileName) @@ -1054,6 +1068,11 @@ self.tr("Save As..."), self.__contextSaveAs, ) + self.menuActs["SaveAsRemote"] = self.menu.addAction( + EricPixmapCache.getIcon("fileSaveAsRemote"), + self.tr("Save as (Remote)..."), + self.__contextSaveAsRemote, + ) self.menu.addAction( EricPixmapCache.getIcon("fileSaveCopy"), self.tr("Save Copy..."), @@ -1181,7 +1200,9 @@ """ menu = QMenu(self.tr("Show")) - menu.addAction(self.tr("Code metrics..."), self.__showCodeMetrics) + self.codeMetricsAct = menu.addAction( + self.tr("Code metrics..."), self.__showCodeMetrics + ) self.coverageMenuAct = menu.addAction( self.tr("Code coverage..."), self.__showCodeCoverage ) @@ -1304,7 +1325,7 @@ @rtype QMenu """ menu = QMenu(self.tr("Re-Open With Encoding")) - menu.setIcon(EricPixmapCache.getIcon("open")) + menu.setIcon(EricPixmapCache.getIcon("documentOpen")) for encoding in sorted(Utilities.supportedCodecs): act = menu.addAction(encoding) @@ -2161,8 +2182,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()) @@ -2822,7 +2843,7 @@ # get recently used breakpoint conditions rs = Preferences.Prefs.rsettings.value(recentNameBreakpointConditions) condHistory = ( - Preferences.toList(rs)[: Preferences.getDebugger("RecentNumber")] + EricUtilities.toList(rs)[: Preferences.getDebugger("RecentNumber")] if rs is not None else [] ) @@ -3458,11 +3479,7 @@ self, self.tr("File Modified"), self.tr("<p>The file <b>{0}</b> has unsaved changes.</p>").format(fn), - ( - self.saveFile - if not FileSystemUtilities.isRemoteFileName(self.fileName) - else None - ), + self.saveFile, ) if res: self.vm.setEditorName(self, self.fileName) @@ -3490,7 +3507,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. @@ -3504,26 +3521,46 @@ @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") + if encoding: + ( + txt, + self.encoding, + ) = self.__remotefsInterface.readEncodedFileWithEncoding( + fn, encoding, create=True + ) + else: + txt, self.encoding = self.__remotefsInterface.readEncodedFile( + fn, create=True + ) 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>" @@ -3553,7 +3590,7 @@ self.extractTasks() - self.lastModified = pathlib.Path(fn).stat().st_mtime + self.recordModificationTime(filename=fn) @pyqtSlot() def __convertTabs(self): @@ -3605,7 +3642,11 @@ @return flag indicating success @rtype bool """ - config = self.__loadEditorConfigObject(fn) + config = ( + None + if self.fileName and fn == self.fileName + else self.__loadEditorConfigObject(fn) + ) eol = self.__getEditorConfig("EOLMode", nodefault=True, config=config) if eol is not None: @@ -3627,7 +3668,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) @@ -3647,28 +3688,41 @@ 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") + self.encoding = self.__remotefsInterface.writeEncodedFile( + fn, + txt, + self.encoding, + forcedEncoding=editorConfigEncoding, + withBackup=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 """ @@ -3682,7 +3736,12 @@ 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]) @@ -3694,14 +3753,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("."): @@ -3712,10 +3783,13 @@ 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), @@ -3748,21 +3822,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): @@ -3772,7 +3846,7 @@ if saveas or self.fileName == "": saveas = True - fn = self.__getSaveFileName(path) + fn = self.__getSaveFileName(path=path, remote=remote) if not fn: return False @@ -3791,13 +3865,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) @@ -3819,7 +3893,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) @@ -3829,21 +3903,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): """ @@ -3904,7 +3978,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) @@ -6335,6 +6409,9 @@ ) self.menuActs["Reload"].setEnabled(bool(self.fileName)) self.menuActs["Save"].setEnabled(self.isModified()) + self.menuActs["SaveAsRemote"].setEnabled( + ericApp().getObject("EricServer").isServerConnected() + ) self.menuActs["Undo"].setEnabled(self.isUndoAvailable()) self.menuActs["Redo"].setEnabled(self.isRedoAvailable()) self.menuActs["Revert"].setEnabled(self.isModified()) @@ -6352,6 +6429,9 @@ self.menuActs["Diagrams"].setEnabled(True) else: self.menuActs["Diagrams"].setEnabled(False) + self.menuActs["Formatting"].setEnabled( + not FileSystemUtilities.isRemoteFileName(self.fileName) + ) if not self.miniMenu: if self.lexer_ is not None: self.menuActs["Comment"].setEnabled(self.lexer_.canBlockComment()) @@ -6613,6 +6693,15 @@ self.vm.setEditorName(self, self.fileName) @pyqtSlot() + def __contextSaveAsRemote(self): + """ + Private slot handling the save as (remote) context menu entry. + """ + ok = self.saveFileAs(remote=True) + if ok: + self.vm.setEditorName(self, self.fileName) + + @pyqtSlot() def __contextSaveCopy(self): """ Private slot handling the save copy context menu entry. @@ -6968,9 +7057,26 @@ self.__coverageFile = fn if fn: - cover = Coverage(data_file=fn) - cover.load() - missing = cover.analysis2(self.fileName)[3] + if FileSystemUtilities.isRemoteFileName(fn): + coverageInterface = ( + ericApp().getObject("EricServer").getServiceInterface("Coverage") + ) + ok, error = coverageInterface.loadCoverageData(fn) + if not ok and not silent: + EricMessageBox.critical( + self, + self.tr("Load Coverage Data"), + self.tr( + "<p>The coverage data could not be loaded from file" + " <b>{0}</b>.</p><p>Reason: {1}</p>" + ).format(self.cfn, error), + ) + return + missing = coverageInterface.analyzeFile(self.fileName)[3] + else: + cover = Coverage(data_file=fn) + cover.load() + missing = cover.analysis2(self.fileName)[3] if missing: for line in missing: handle = self.markerAdd(line - 1, self.notcovered) @@ -8034,9 +8140,19 @@ with contextlib.suppress(AttributeError): self.setCaretWidth(self.caretWidth) - self.__updateReadOnly(False) + if not self.dbs.isDebugging: + 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): @@ -8046,7 +8162,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) @@ -8219,9 +8339,7 @@ signal if there was an attribute change. @type bool """ - if self.fileName == "" or not FileSystemUtilities.isPlainFileName( - self.fileName - ): + if self.fileName == "" or FileSystemUtilities.isDeviceFileName(self.fileName): return readOnly = self.checkReadOnly() @@ -8243,9 +8361,18 @@ @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): @@ -8253,13 +8380,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""" @@ -8280,10 +8403,10 @@ 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 + self.recordModificationTime() def getModificationTime(self): """ @@ -8295,17 +8418,73 @@ return self.lastModified @pyqtSlot() - def recordModificationTime(self): + 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) + and not self.dbs.isDebugging + ): + 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) + and 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() @@ -8325,11 +8504,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) @@ -8379,7 +8553,7 @@ else True ) if ok: - self.refresh() + self.__refresh() def setMonospaced(self, on): """ @@ -8626,11 +8800,18 @@ if not self.checkDirty(): return - package = ( - os.path.isdir(self.fileName) - and self.fileName - or os.path.dirname(self.fileName) - ) + if FileSystemUtilities.isRemoteFileName(self.fileName): # noqa: Y108 + package = ( + self.fileName + if self.__remotefsInterface.isdir(self.fileName) + else self.__remotefsInterface.dirname(self.fileName) + ) + else: + package = ( + self.fileName + if os.path.isdir(self.fileName) + else os.path.dirname(self.fileName) + ) res = EricMessageBox.yesNo( self, self.tr("Package Diagram"), @@ -9752,9 +9933,18 @@ """ editorConfig = {} - if fileName and FileSystemUtilities.isPlainFileName(self.fileName): + if fileName: try: - editorConfig = editorconfig.get_properties(fileName) + if FileSystemUtilities.isRemoteFileName(fileName): + if ericApp().getObject("EricServer").isServerConnected(): + editorConfigInterface = ( + ericApp() + .getObject("EricServer") + .getServiceInterface("EditorConfig") + ) + editorConfig = editorConfigInterface.loadEditorConfig(fileName) + elif FileSystemUtilities.isPlainFileName(fileName): + editorConfig = editorconfig.get_properties(fileName) except editorconfig.EditorConfigError: EricMessageBox.warning( self, @@ -9790,16 +9980,6 @@ 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 - try: if option == "EOLMode": value = config["end_of_line"] @@ -9814,9 +9994,9 @@ elif option == "DefaultEncoding": value = config["charset"] elif option == "InsertFinalNewline": - value = Globals.toBool(config["insert_final_newline"]) + value = EricUtilities.toBool(config["insert_final_newline"]) elif option == "StripTrailingWhitespace": - value = Globals.toBool(config["trim_trailing_whitespace"]) + value = EricUtilities.toBool(config["trim_trailing_whitespace"]) elif option == "TabWidth": value = int(config["tab_width"]) elif option == "IndentWidth":